diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..e69de29b diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-full-snapshot-builder.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-full-snapshot-builder.ts index d8cdfd40..53abc535 100644 --- a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-full-snapshot-builder.ts +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-full-snapshot-builder.ts @@ -60,7 +60,7 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder payment_method: payment, subtotal_amount: allTotals.subtotalAmount.toObjectString(), - items_discount_amount: allTotals.itemDiscountAmount.toObjectString(), + items_discount_amount: allTotals.itemsDiscountAmount.toObjectString(), global_discount_percentage: proforma.globalDiscountPercentage.toObjectString(), global_discount_amount: allTotals.globalDiscountAmount.toObjectString(), diff --git a/modules/customer-invoices/src/api/domain/proformas/aggregates/proforma.aggregate.ts b/modules/customer-invoices/src/api/domain/proformas/aggregates/proforma.aggregate.ts index 0eb47202..9c09dd99 100644 --- a/modules/customer-invoices/src/api/domain/proformas/aggregates/proforma.aggregate.ts +++ b/modules/customer-invoices/src/api/domain/proformas/aggregates/proforma.aggregate.ts @@ -59,7 +59,7 @@ export interface IProformaCreateProps { export interface IProformaTotals { subtotalAmount: InvoiceAmount; - itemDiscountAmount: InvoiceAmount; + itemsDiscountAmount: InvoiceAmount; globalDiscountAmount: InvoiceAmount; totalDiscountAmount: InvoiceAmount; @@ -276,7 +276,7 @@ export class Proforma extends AggregateRoot implements IP return { subtotalAmount: this.toInvoiceAmount(itemsTotals.subtotalAmount), - itemDiscountAmount: this.toInvoiceAmount(itemsTotals.itemDiscountAmount), + itemsDiscountAmount: this.toInvoiceAmount(itemsTotals.itemDiscountAmount), globalDiscountAmount: this.toInvoiceAmount(itemsTotals.globalDiscountAmount), totalDiscountAmount: this.toInvoiceAmount(itemsTotals.totalDiscountAmount), diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-domain.mapper.ts index cf6849ae..eb44f3b6 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-domain.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-domain.mapper.ts @@ -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.itemDiscountAmount.value, - items_discount_amount_scale: allAmounts.itemDiscountAmount.scale, + items_discount_amount_value: allAmounts.itemsDiscountAmount.value, + items_discount_amount_scale: allAmounts.itemsDiscountAmount.scale, global_discount_percentage_value: source.globalDiscountPercentage.toPrimitive().value, global_discount_percentage_scale: source.globalDiscountPercentage.toPrimitive().scale, diff --git a/modules/factuges/src/api/application/mappers/create-proforma-from-factuges-input.mapper.ts b/modules/factuges/src/api/application/mappers/create-proforma-from-factuges-input.mapper.ts index 3d4b98b8..f8947b16 100644 --- a/modules/factuges/src/api/application/mappers/create-proforma-from-factuges-input.mapper.ts +++ b/modules/factuges/src/api/application/mappers/create-proforma-from-factuges-input.mapper.ts @@ -91,13 +91,13 @@ export type ProformaDraft = { notes: Maybe; languageCode: LanguageCode; currencyCode: CurrencyCode; - subtotalAmount: Maybe; + subtotalAmount: Maybe; globalDiscountPercentage: DiscountPercentage; - itemsDiscountAmount: Maybe; - taxableAmount: Maybe; + itemsDiscountAmount: Maybe; + taxableAmount: Maybe; taxes: ProformaItemTaxesProps; - taxesAmount: Maybe; - totalAmount: Maybe; + taxesAmount: Maybe; + totalAmount: Maybe; items: ProformaDraftItem[]; }; diff --git a/modules/factuges/src/api/application/use-cases/create-proforma-from-factuges.use-case.ts b/modules/factuges/src/api/application/use-cases/create-proforma-from-factuges.use-case.ts index c8c624e7..a589bb50 100644 --- a/modules/factuges/src/api/application/use-cases/create-proforma-from-factuges.use-case.ts +++ b/modules/factuges/src/api/application/use-cases/create-proforma-from-factuges.use-case.ts @@ -2,9 +2,12 @@ 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 { @@ -13,7 +16,14 @@ import { CustomerTaxes, type ICustomerCreateProps, } from "@erp/customers/api/domain"; -import { type Name, type PhoneNumber, type TextValue, UniqueID } from "@repo/rdx-ddd"; +import { + type Name, + type PhoneNumber, + type TextValue, + UniqueID, + ValidationErrorCollection, + type ValidationErrorDetail, +} from "@repo/rdx-ddd"; import { Maybe, Result } from "@repo/rdx-utils"; import type { Transaction } from "sequelize"; @@ -86,8 +96,8 @@ export class CreateProformaFromFactugesUseCase { companyId, transaction, }); - if (customerResult.isFailure) { - return Result.fail(customerResult.error); + if (paymentResult.isFailure) { + return Result.fail(paymentResult.error); } const payment = paymentResult.data; @@ -122,6 +132,13 @@ 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, { @@ -136,8 +153,6 @@ export class CreateProformaFromFactugesUseCase { const snapshot = readResult.data; - //const comparisonResults = this.compare() - const result = { customer_id: customer.id.toString(), proforma_id: snapshot.id.toString(), @@ -150,6 +165,148 @@ 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 { + 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; + 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; + actual: Maybe; + 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"]; customerId: UniqueID;