Compare commits

..

No commits in common. "964565a6fead8a119a1841515e47cf43b5b858db" and "a4615e8bc44a23eea8c6b1240f913f4ec0a9ad45" have entirely different histories.

8 changed files with 107 additions and 398 deletions

View File

View File

@ -60,7 +60,7 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder
payment_method: payment,
subtotal_amount: allTotals.subtotalAmount.toObjectString(),
items_discount_amount: allTotals.itemsDiscountAmount.toObjectString(),
items_discount_amount: allTotals.itemDiscountAmount.toObjectString(),
global_discount_percentage: proforma.globalDiscountPercentage.toObjectString(),
global_discount_amount: allTotals.globalDiscountAmount.toObjectString(),

View File

@ -59,7 +59,7 @@ export interface IProformaCreateProps {
export interface IProformaTotals {
subtotalAmount: InvoiceAmount;
itemsDiscountAmount: InvoiceAmount;
itemDiscountAmount: InvoiceAmount;
globalDiscountAmount: InvoiceAmount;
totalDiscountAmount: InvoiceAmount;
@ -276,7 +276,7 @@ export class Proforma extends AggregateRoot<InternalProformaProps> implements IP
return {
subtotalAmount: this.toInvoiceAmount(itemsTotals.subtotalAmount),
itemsDiscountAmount: this.toInvoiceAmount(itemsTotals.itemDiscountAmount),
itemDiscountAmount: this.toInvoiceAmount(itemsTotals.itemDiscountAmount),
globalDiscountAmount: this.toInvoiceAmount(itemsTotals.globalDiscountAmount),
totalDiscountAmount: this.toInvoiceAmount(itemsTotals.totalDiscountAmount),

View File

@ -170,7 +170,7 @@ export class ProformaItem extends DomainEntity<InternalProformaItemProps> implem
// Todos a 4 decimales
public isValued(): boolean {
return this.props.quantity.isSome() && this.props.unitAmount.isSome();
return this.props.quantity.isSome() || this.props.unitAmount.isSome();
}
public subtotalAmount(): ItemAmount {

View File

@ -339,8 +339,8 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
subtotal_amount_value: allAmounts.subtotalAmount.value,
subtotal_amount_scale: allAmounts.subtotalAmount.scale,
items_discount_amount_value: allAmounts.itemsDiscountAmount.value,
items_discount_amount_scale: allAmounts.itemsDiscountAmount.scale,
items_discount_amount_value: allAmounts.itemDiscountAmount.value,
items_discount_amount_scale: allAmounts.itemDiscountAmount.scale,
global_discount_percentage_value: source.globalDiscountPercentage.toPrimitive().value,
global_discount_percentage_scale: source.globalDiscountPercentage.toPrimitive().scale,

View File

@ -1,7 +1,7 @@
import type { JsonTaxCatalogProvider } from "@erp/core";
import { DiscountPercentage, Tax } from "@erp/core/api";
import {
InvoiceAmount,
type IProformaItemCreateProps,
InvoiceSerie,
ItemAmount,
ItemDescription,
@ -39,86 +39,73 @@ import type {
CreateProformaItemFromFactugesRequestDTO,
} from "../../../common";
export type ProformaCustomerLookup = {
tin: TINNumber;
};
export interface IProformaFromFactuGESProps {
customerLookup: {
tin: TINNumber;
};
export type ProformaPaymentLookup = {
factuges_id: string;
};
paymentLookup: {
factuges_id: string;
};
export type ProformaCustomerDraft = {
isCompany: boolean;
name: Name;
tin: TINNumber;
address: PostalAddressProps;
emailPrimary: Maybe<EmailAddress>;
emailSecondary: Maybe<EmailAddress>;
phonePrimary: Maybe<PhoneNumber>;
phoneSecondary: Maybe<PhoneNumber>;
mobilePrimary: Maybe<PhoneNumber>;
mobileSecondary: Maybe<PhoneNumber>;
website: Maybe<URLAddress>;
languageCode: LanguageCode;
currencyCode: CurrencyCode;
};
customerDraft: {
//reference: Maybe<Name>;
export type ProformaDraftItem = {
position: string;
description: Maybe<ItemDescription>;
quantity: Maybe<ItemQuantity>;
unitAmount: Maybe<ItemAmount>;
subtotalAmount: Maybe<ItemAmount>;
itemDiscountPercentage: Maybe<DiscountPercentage>;
itemDiscountAmount: Maybe<ItemAmount>;
globalDiscountPercentage: DiscountPercentage;
globalDiscountAmount: Maybe<ItemAmount>;
totalDiscountAmount: Maybe<ItemAmount>;
taxableAmount: Maybe<ItemAmount>;
taxes: ProformaItemTaxesProps;
taxesAmount: Maybe<ItemAmount>;
totalAmount: Maybe<ItemAmount>;
languageCode: LanguageCode;
currencyCode: CurrencyCode;
};
isCompany: boolean;
name: Name;
//tradeName: Maybe<Name>;
tin: TINNumber;
export type ProformaDraft = {
series: Maybe<InvoiceSerie>;
invoiceDate: UtcDate;
operationDate: Maybe<UtcDate>;
reference: Maybe<string>;
description: Maybe<string>;
notes: Maybe<TextValue>;
languageCode: LanguageCode;
currencyCode: CurrencyCode;
subtotalAmount: Maybe<InvoiceAmount>;
globalDiscountPercentage: DiscountPercentage;
itemsDiscountAmount: Maybe<InvoiceAmount>;
taxableAmount: Maybe<InvoiceAmount>;
taxes: ProformaItemTaxesProps;
taxesAmount: Maybe<InvoiceAmount>;
totalAmount: Maybe<InvoiceAmount>;
items: ProformaDraftItem[];
};
address: PostalAddressProps;
export type ProformaPaymentDraft = {
factuges_id: string;
description: string;
};
emailPrimary: Maybe<EmailAddress>;
emailSecondary: Maybe<EmailAddress>;
export type FactugesProformaPayload = {
customerLookup: ProformaCustomerLookup;
paymentLookup: ProformaPaymentLookup;
customerDraft: ProformaCustomerDraft;
proformaDraft: ProformaDraft;
paymentDraft: ProformaPaymentDraft;
};
phonePrimary: Maybe<PhoneNumber>;
phoneSecondary: Maybe<PhoneNumber>;
mobilePrimary: Maybe<PhoneNumber>;
mobileSecondary: Maybe<PhoneNumber>;
//fax: Maybe<PhoneNumber>;
website: Maybe<URLAddress>;
//legalRecord: Maybe<TextValue>;
//defaultTaxes: CustomerTaxesProps;
languageCode: LanguageCode;
currencyCode: CurrencyCode;
};
proformaDraft: {
series: Maybe<InvoiceSerie>;
invoiceDate: UtcDate;
operationDate: Maybe<UtcDate>;
reference: Maybe<string>;
description: Maybe<string>;
notes: Maybe<TextValue>;
languageCode: LanguageCode;
currencyCode: CurrencyCode;
items: IProformaItemCreateProps[];
globalDiscountPercentage: DiscountPercentage;
};
paymentDraft: {
factuges_id: string;
description: string;
};
}
export interface ICreateProformaFromFactugesInputMapper {
map(
dto: CreateProformaFromFactugesRequestDTO,
params: { companyId: UniqueID }
): Result<FactugesProformaPayload>;
): Result<IProformaFromFactuGESProps>;
}
export class CreateProformaFromFactugesInputMapper
@ -133,7 +120,7 @@ export class CreateProformaFromFactugesInputMapper
public map(
dto: CreateProformaFromFactugesRequestDTO,
params: { companyId: UniqueID }
): Result<FactugesProformaPayload> {
): Result<IProformaFromFactuGESProps> {
try {
const errors: ValidationErrorDetail[] = [];
const { companyId } = params;
@ -185,7 +172,7 @@ export class CreateProformaFromFactugesInputMapper
currencyCode: CurrencyCode;
errors: ValidationErrorDetail[];
}
): ProformaPaymentDraft {
): IProformaFromFactuGESProps["paymentDraft"] {
const errors: ValidationErrorDetail[] = [];
const { companyId } = params;
@ -202,7 +189,7 @@ export class CreateProformaFromFactugesInputMapper
currencyCode: CurrencyCode;
errors: ValidationErrorDetail[];
}
): ProformaDraft {
): IProformaFromFactuGESProps["proformaDraft"] {
const errors: ValidationErrorDetail[] = [];
const { companyId } = params;
@ -243,7 +230,7 @@ export class CreateProformaFromFactugesInputMapper
);
const description = extractOrPushError(
maybeFromNullableResult(dto.description, (value) => Result.ok(String(value))),
maybeFromNullableResult(dto.reference, (value) => Result.ok(String(value))),
"description",
errors
);
@ -254,6 +241,12 @@ export class CreateProformaFromFactugesInputMapper
errors
);
const globalDiscountPercentage = extractOrPushError(
DiscountPercentage.create({ value: Number(dto.global_discount_percentage_value) }),
"global_discount_percentage_value",
params.errors
);
const languageCode = extractOrPushError(
LanguageCode.create(dto.customer.language_code),
"language_code",
@ -262,59 +255,6 @@ export class CreateProformaFromFactugesInputMapper
const currencyCode = CurrencyCode.fromEUR();
const subtotalAmount = extractOrPushError(
maybeFromNullableResult(dto.subtotal_amount_value, (value) =>
InvoiceAmount.create({ value: Number(value) })
),
"subtotal_amount_value",
params.errors
);
const globalDiscountPercentage = extractOrPushError(
DiscountPercentage.create({ value: Number(dto.global_discount_percentage_value) }),
"global_discount_percentage_value",
params.errors
);
const itemsDiscountAmount = extractOrPushError(
maybeFromNullableResult(dto.discount_amount_value, (value) =>
InvoiceAmount.create({ value: Number(value) })
),
"discount_amount_value",
params.errors
);
const taxableAmount = extractOrPushError(
maybeFromNullableResult(dto.taxable_amount_value, (value) =>
InvoiceAmount.create({ value: Number(value) })
),
"taxable_amount_value",
params.errors
);
// TODO: Determinar cómo calcular los impuestos de la cabecera de la proforma
const taxes: ProformaItemTaxesProps = {
iva: Maybe.none(),
retention: Maybe.none(),
rec: Maybe.none(),
};
const taxesAmount = extractOrPushError(
maybeFromNullableResult(dto.taxes_amount_value, (value) =>
InvoiceAmount.create({ value: Number(value) })
),
"taxes_amount_value",
params.errors
);
const totalAmount = extractOrPushError(
maybeFromNullableResult(dto.total_amount_value, (value) =>
InvoiceAmount.create({ value: Number(value) })
),
"total_amount_value",
params.errors
);
const itemsProps = this.mapItemsProps(dto, {
languageCode: languageCode!,
currencyCode: currencyCode,
@ -322,7 +262,7 @@ export class CreateProformaFromFactugesInputMapper
errors,
});
const props: ProformaDraft = {
const props: IProformaFromFactuGESProps["proformaDraft"] = {
//companyId,
//status: defaultStatus,
@ -342,16 +282,9 @@ export class CreateProformaFromFactugesInputMapper
languageCode: languageCode!,
currencyCode: currencyCode!,
subtotalAmount: subtotalAmount!,
globalDiscountPercentage: globalDiscountPercentage!,
itemsDiscountAmount: itemsDiscountAmount!,
taxableAmount: taxableAmount!,
taxes: taxes,
taxesAmount: taxesAmount!,
totalAmount: totalAmount!,
items: itemsProps,
items: itemsProps, // ← IProformaItemProps[]
};
return props;
@ -364,7 +297,7 @@ export class CreateProformaFromFactugesInputMapper
currencyCode: CurrencyCode;
errors: ValidationErrorDetail[];
}
): ProformaCustomerDraft {
): IProformaFromFactuGESProps["customerDraft"] {
const { errors, currencyCode } = params;
const isCompany = dto.is_company === "1";
@ -464,7 +397,7 @@ export class CreateProformaFromFactugesInputMapper
errors,
});*/
const customerProps: ProformaCustomerDraft = {
const customerProps: IProformaFromFactuGESProps["customerDraft"] = {
//companyId,
//status: status!,
@ -507,12 +440,10 @@ export class CreateProformaFromFactugesInputMapper
globalDiscountPercentage: DiscountPercentage;
errors: ValidationErrorDetail[];
}
): ProformaDraftItem[] {
const itemsProps: ProformaDraftItem[] = [];
): IProformaItemCreateProps[] {
const itemsProps: IProformaItemCreateProps[] = [];
dto.items.forEach((item, index) => {
const position = String(item.position);
const description = extractOrPushError(
maybeFromNullableResult(item.description, (value) => ItemDescription.create(value)),
`items[${index}].description`,
@ -523,27 +454,19 @@ export class CreateProformaFromFactugesInputMapper
maybeFromNullableResult(item.quantity_value, (value) =>
ItemQuantity.create({ value: Number(value) })
),
`items[${index}].quantity_value`,
"items[$index].quantity_value",
params.errors
);
const unitAmount = extractOrPushError(
maybeFromNullableResult(item.unit_amount_value, (value) =>
maybeFromNullableResult(item.unit_value, (value) =>
ItemAmount.create({ value: Number(value) })
),
`items[${index}].unit_amount_value`,
`items[${index}].unit_value`,
params.errors
);
const subtotalAmount = extractOrPushError(
maybeFromNullableResult(item.subtotal_amount_value, (value) =>
ItemAmount.create({ value: Number(value) })
),
`items[${index}].subtotal_amount_value`,
params.errors
);
const itemDiscountPercentage = extractOrPushError(
const discountPercentage = extractOrPushError(
maybeFromNullableResult(item.item_discount_percentage_value, (value) =>
DiscountPercentage.create({ value: Number(value) })
),
@ -551,93 +474,35 @@ export class CreateProformaFromFactugesInputMapper
params.errors
);
const itemDiscountAmount = extractOrPushError(
maybeFromNullableResult(item.item_discount_amount_value, (value) =>
ItemAmount.create({ value: Number(value) })
),
`items[${index}].item_discount_amount_value`,
params.errors
);
const globalDiscountAmount = extractOrPushError(
maybeFromNullableResult(item.global_discount_amount_value, (value) =>
ItemAmount.create({ value: Number(value) })
),
`items[${index}].global_discount_amount_value`,
params.errors
);
const totalDiscountAmount = extractOrPushError(
maybeFromNullableResult(item.total_discount_amount_value, (value) =>
ItemAmount.create({ value: Number(value) })
),
`items[${index}].total_discount_amount_value`,
params.errors
);
const taxableAmount = extractOrPushError(
maybeFromNullableResult(item.taxable_amount_value, (value) =>
ItemAmount.create({ value: Number(value) })
),
`items[${index}].taxable_amount_value`,
params.errors
);
const taxesAmount = extractOrPushError(
maybeFromNullableResult(item.taxes_amount_value, (value) =>
ItemAmount.create({ value: Number(value) })
),
`items[${index}].taxes_amount_value`,
params.errors
);
const totalAmount = extractOrPushError(
maybeFromNullableResult(item.total_amount_value, (value) =>
ItemAmount.create({ value: Number(value) })
),
`items[${index}].total_amount_value`,
params.errors
);
const taxes = this.mapItemTaxesProps(item, {
const taxes = this.mapItempTaxesProps(item, {
itemIndex: index,
errors: params.errors,
});
const _item: ProformaDraftItem = {
position,
this.throwIfValidationErrors(params.errors);
itemsProps.push({
description: description!,
quantity: quantity!,
unitAmount: unitAmount!,
subtotalAmount: subtotalAmount!,
itemDiscountPercentage: itemDiscountPercentage!,
itemDiscountAmount: itemDiscountAmount!,
globalDiscountPercentage: params.globalDiscountPercentage,
globalDiscountAmount: globalDiscountAmount!,
totalDiscountAmount: totalDiscountAmount!,
itemDiscountPercentage: discountPercentage!,
taxableAmount: taxableAmount!,
taxes,
taxesAmount: taxesAmount!,
totalAmount: totalAmount!,
globalDiscountPercentage: params.globalDiscountPercentage,
languageCode: params.languageCode,
currencyCode: params.currencyCode,
};
itemsProps.push(_item);
});
});
this.throwIfValidationErrors(params.errors);
return itemsProps;
}
/* Devuelve las propiedades de los impustos de una línea de detalle */
private mapItemTaxesProps(
private mapItempTaxesProps(
itemDTO: CreateProformaItemFromFactugesRequestDTO,
params: { itemIndex: number; errors: ValidationErrorDetail[] }
): ProformaItemTaxesProps {

View File

@ -2,12 +2,9 @@ import type { JsonTaxCatalogProvider } from "@erp/core";
import { type ITransactionManager, Tax, isEntityNotFoundError } from "@erp/core/api";
import type { ProformaPublicServices } from "@erp/customer-invoices/api";
import {
type InvoiceAmount,
InvoicePaymentMethod,
type InvoiceRecipient,
InvoiceStatus,
type ItemAmount,
type Proforma,
} from "@erp/customer-invoices/api/domain";
import type { CustomerPublicServices } from "@erp/customers/api";
import {
@ -16,19 +13,15 @@ import {
CustomerTaxes,
type ICustomerCreateProps,
} from "@erp/customers/api/domain";
import {
type Name,
type PhoneNumber,
type TextValue,
UniqueID,
ValidationErrorCollection,
type ValidationErrorDetail,
} from "@repo/rdx-ddd";
import { type Name, type PhoneNumber, type TextValue, UniqueID } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { CreateProformaFromFactugesRequestDTO } from "../../../common";
import type { FactugesProformaPayload, ICreateProformaFromFactugesInputMapper } from "../mappers";
import type {
ICreateProformaFromFactugesInputMapper,
IProformaFromFactuGESProps,
} from "../mappers";
import paymentsCatalog from "./payments.json";
@ -96,8 +89,8 @@ export class CreateProformaFromFactugesUseCase {
companyId,
transaction,
});
if (paymentResult.isFailure) {
return Result.fail(paymentResult.error);
if (customerResult.isFailure) {
return Result.fail(customerResult.error);
}
const payment = paymentResult.data;
@ -132,13 +125,6 @@ export class CreateProformaFromFactugesUseCase {
return Result.fail(createResult.error);
}
// Valida que los datos de entrada coincidan con el snapshot
const proforma = createResult.data;
const validationResult = this.validateDraftAgainstProforma(proformaDraft, proforma);
if (validationResult.isFailure) {
return Result.fail(validationResult.error);
}
const readResult = await this.proformaServices.getProformaSnapshotById(
createResult.data.id,
{
@ -165,150 +151,8 @@ export class CreateProformaFromFactugesUseCase {
});
}
/**
* Valida que las magnitudes importadas del borrador coincidan con la proforma
* generada por el dominio.
*
* Motivo:
* - Detecta divergencias entre el payload legacy y los cálculos reales del dominio.
* - Actúa como validación de reconciliación, no como sustituto de las invariantes del agregado.
*/
private validateDraftAgainstProforma(
proformaDraft: FactugesProformaPayload["proformaDraft"],
proforma: Proforma
): Result<void, Error> {
const errors: ValidationErrorDetail[] = [];
const proformaTotals = proforma.totals();
console.log(proformaTotals);
if (proformaDraft.items.length !== proforma.items.size()) {
errors.push({
path: "items",
message: "La cantidad de ítems de la proforma no coincide con los datos de entrada.",
});
}
this.validateOptionalExpectedAmount({
expected: proformaDraft.subtotalAmount,
actual: proformaTotals.subtotalAmount,
path: "subtotalAmount",
message: "El subtotal de la proforma no coincide con los datos de entrada.",
errors,
});
this.validateOptionalExpectedAmount({
expected: proformaDraft.taxableAmount,
actual: proformaTotals.taxableAmount,
path: "taxableAmount",
message: "La base imponible de la proforma no coincide con los datos de entrada.",
errors,
});
this.validateOptionalExpectedAmount({
expected: proformaDraft.taxesAmount,
actual: proformaTotals.taxesAmount,
path: "taxesAmount",
message: "La suma de impuestos de la proforma no coincide con los datos de entrada.",
errors,
});
this.validateOptionalExpectedAmount({
expected: proformaDraft.totalAmount,
actual: proformaTotals.totalAmount,
path: "totalAmount",
message: "El total de la proforma no coincide con los datos de entrada.",
errors,
});
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection(
"La proforma generada no coincide con las magnitudes validadas del borrador importado.",
errors
)
);
}
return Result.ok();
}
private validateOptionalExpectedAmount(params: {
expected: Maybe<InvoiceAmount | ItemAmount>;
actual: InvoiceAmount | ItemAmount;
path: string;
message: string;
errors: ValidationErrorDetail[];
}): void {
const { expected, actual, path, message, errors } = params;
if (expected.isNone()) {
return;
}
const expectedAmount = expected.unwrap();
if (!actual.equalsTo(expectedAmount)) {
errors.push({
path,
message: this.buildAmountMismatchMessage({
baseMessage: message,
expected: expectedAmount,
actual,
}),
});
}
}
private buildAmountMismatchMessage(params: {
baseMessage: string;
expected: InvoiceAmount | ItemAmount;
actual: InvoiceAmount | ItemAmount;
}): string {
const { baseMessage, expected, actual } = params;
return `${baseMessage} Esperado: ${expected.formattedValue}. Actual: ${actual.formattedValue}.`;
}
/**
* Valida un importe opcional esperado contra un importe real también opcional.
*
* Motivo:
* - Algunos campos pueden faltar tanto en el payload importado como en
* la proyección o snapshot generado.
* - Si el esperado existe pero el real no, se considera discrepancia.
*/
private validateOptionalMaybeAmount(params: {
expected: Maybe<InvoiceAmount | ItemAmount>;
actual: Maybe<InvoiceAmount | ItemAmount>;
path: string;
message: string;
errors: ValidationErrorDetail[];
}): void {
const { expected, actual, path, message, errors } = params;
if (expected.isNone()) {
return;
}
if (actual.isNone()) {
errors.push({
path,
message,
});
return;
}
if (!actual.unwrap().equals(expected.unwrap())) {
errors.push({
path,
message,
});
}
}
private buildProformaCreateProps(deps: {
proformaDraft: FactugesProformaPayload["proformaDraft"];
proformaDraft: IProformaFromFactuGESProps["proformaDraft"];
customerId: UniqueID;
payment: FakePaymentMethod;
context: {
@ -349,8 +193,8 @@ export class CreateProformaFromFactugesUseCase {
* @returns `Result` con el cliente resuelto o el error producido.
*/
private async resolveCustomer(
customerLookup: FactugesProformaPayload["customerLookup"],
customerDraft: FactugesProformaPayload["customerDraft"],
customerLookup: IProformaFromFactuGESProps["customerLookup"],
customerDraft: IProformaFromFactuGESProps["customerDraft"],
context: {
companyId: UniqueID;
transaction: Transaction;
@ -383,8 +227,8 @@ export class CreateProformaFromFactugesUseCase {
}
private async resolvePayment(
paymentLookup: FactugesProformaPayload["paymentLookup"],
paymentDraft: FactugesProformaPayload["paymentDraft"],
paymentLookup: IProformaFromFactuGESProps["paymentLookup"],
paymentDraft: IProformaFromFactuGESProps["paymentDraft"],
context: {
companyId: UniqueID;
transaction: Transaction;
@ -410,7 +254,7 @@ export class CreateProformaFromFactugesUseCase {
}
private buildCustomerCreateProps(
customerDraft: FactugesProformaPayload["customerDraft"],
customerDraft: IProformaFromFactuGESProps["customerDraft"],
context: {
companyId: UniqueID;
transaction: Transaction;

View File

@ -5,9 +5,9 @@ export const CreateProformaItemFromFactugesRequestSchema = z.object({
position: z.string(),
description: z.string().default(""),
quantity_value: NumericStringSchema.default(""), // Ya viene escalado
unit_amount_value: NumericStringSchema.default(""),
unit_value: NumericStringSchema.default(""),
subtotal_amount_value: NumericStringSchema.default(""),
subtotal_amuount_value: NumericStringSchema.default(""),
item_discount_percentage_value: NumericStringSchema.default(""),
item_discount_amount_value: NumericStringSchema.default(""),
@ -17,7 +17,7 @@ export const CreateProformaItemFromFactugesRequestSchema = z.object({
total_discount_amount_value: NumericStringSchema.default(""),
taxable_amount_value: NumericStringSchema.default(""),
total_amount_value: NumericStringSchema.default(""),
total_value: NumericStringSchema.default(""),
iva_code: z.string().default(""),
iva_percentage_value: NumericStringSchema.default(""),