diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice-item.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice-item.mapper.ts index 9fbbb5cc..2a6e80ef 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice-item.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice-item.mapper.ts @@ -8,7 +8,6 @@ import { toNullable, } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import { InferCreationAttributes } from "sequelize"; import { CustomerInvoice, CustomerInvoiceItem, @@ -62,15 +61,13 @@ export class CustomerInvoiceItemDomainMapper ); const description = extractOrPushError( - maybeFromNullableVO(source.description, (value) => - CustomerInvoiceItemDescription.create(value) - ), + maybeFromNullableVO(source.description, (v) => CustomerInvoiceItemDescription.create(v)), `items[${index}].description`, errors ); const quantity = extractOrPushError( - maybeFromNullableVO(source.quantity_value, (value) => ItemQuantity.create({ value })), + maybeFromNullableVO(source.quantity_value, (v) => ItemQuantity.create({ value: v })), `items[${index}].quantity`, errors ); @@ -84,8 +81,8 @@ export class CustomerInvoiceItemDomainMapper ); const discountPercentage = extractOrPushError( - maybeFromNullableVO(source.discount_percentage_value, (value) => - ItemDiscount.create({ value }) + maybeFromNullableVO(source.discount_percentage_value, (v) => + ItemDiscount.create({ value: v }) ), `items[${index}].discount_percentage`, errors @@ -107,9 +104,8 @@ export class CustomerInvoiceItemDomainMapper source: CustomerInvoiceItemModel, params?: MapperParamsType ): Result { - const { errors, index, requireIncludes } = params as { + const { errors, index } = params as { index: number; - requireIncludes: boolean; errors: ValidationErrorDetail[]; attributes: Partial; }; @@ -117,17 +113,7 @@ export class CustomerInvoiceItemDomainMapper // 1) Valores escalares (atributos generales) const attributes = this.mapAttributesToDomain(source, params); - // 2) Comprobar relaciones - if (requireIncludes) { - if (!source.taxes) { - errors.push({ - path: `items[${index}].taxes`, - message: "Taxes not included in query (requireIncludes=true)", - }); - } - } - - // 3) Taxes (colección a nivel de item/línea) + // 2) Taxes (colección a nivel de item/línea) const taxesResults = this._taxesMapper.mapToDomainCollection( source.taxes, source.taxes.length, @@ -144,10 +130,9 @@ export class CustomerInvoiceItemDomainMapper }); } - // 5) Construcción del elemento de dominio - const taxes = ItemTaxes.create(taxesResults.data.getAll()); + // 3) Construcción del elemento de dominio const createResult = CustomerInvoiceItem.create( { languageCode: attributes.languageCode!, @@ -175,7 +160,7 @@ export class CustomerInvoiceItemDomainMapper public mapToPersistence( source: CustomerInvoiceItem, params?: MapperParamsType - ): Result, Error> { + ): Result { const { errors, index, parent } = params as { index: number; parent: CustomerInvoice; @@ -201,10 +186,12 @@ export class CustomerInvoiceItemDomainMapper description: toNullable(source.description, (v) => v.toPrimitive()), quantity_value: toNullable(source.quantity, (v) => v.toPrimitive().value), - quantity_scale: toNullable(source.quantity, (v) => v.toPrimitive().scale), + quantity_scale: + toNullable(source.quantity, (v) => v.toPrimitive().scale) ?? ItemQuantity.DEFAULT_SCALE, unit_amount_value: toNullable(source.unitAmount, (v) => v.toPrimitive().value), - unit_amount_scale: toNullable(source.unitAmount, (v) => v.toPrimitive().scale), + unit_amount_scale: + toNullable(source.unitAmount, (v) => v.toPrimitive().scale) ?? ItemAmount.DEFAULT_SCALE, subtotal_amount_value: allAmounts.subtotalAmount.value, subtotal_amount_scale: allAmounts.subtotalAmount.scale, @@ -213,10 +200,9 @@ export class CustomerInvoiceItemDomainMapper source.discountPercentage, (v) => v.toPrimitive().value ), - discount_percentage_scale: toNullable( - source.discountPercentage, - (v) => v.toPrimitive().scale - ), + discount_percentage_scale: + toNullable(source.discountPercentage, (v) => v.toPrimitive().scale) ?? + ItemDiscount.DEFAULT_SCALE, discount_amount_value: allAmounts.discountAmount.value, discount_amount_scale: allAmounts.discountAmount.scale, diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice.mapper.ts index c78d9dc9..838b8ce0 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice.mapper.ts @@ -12,7 +12,7 @@ import { maybeFromNullableVO, toNullable, } from "@repo/rdx-ddd"; -import { Collection, Maybe, Result } from "@repo/rdx-utils"; +import { Collection, Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils"; import { CustomerInvoice, CustomerInvoiceItems, @@ -23,9 +23,9 @@ import { InvoicePaymentMethod, } from "../../../domain"; import { CustomerInvoiceCreationAttributes, CustomerInvoiceModel } from "../../sequelize"; -import { CustomerInvoiceItemDomainMapper as CustomerInvoiceItemFullMapper } from "./customer-invoice-item.mapper"; -import { InvoiceRecipientDomainMapper as InvoiceRecipientFullMapper } from "./invoice-recipient.mapper"; -import { TaxesDomainMapper as TaxesFullMapper } from "./invoice-taxes.mapper"; +import { CustomerInvoiceItemDomainMapper } from "./customer-invoice-item.mapper"; +import { InvoiceRecipientDomainMapper } from "./invoice-recipient.mapper"; +import { TaxesDomainMapper } from "./invoice-taxes.mapper"; export interface ICustomerInvoiceDomainMapper extends ISequelizeDomainMapper< @@ -42,59 +42,16 @@ export class CustomerInvoiceDomainMapper > implements ICustomerInvoiceDomainMapper { - private _itemsMapper: CustomerInvoiceItemFullMapper; - private _recipientMapper: InvoiceRecipientFullMapper; - private _taxesMapper: TaxesFullMapper; + private _itemsMapper: CustomerInvoiceItemDomainMapper; + private _recipientMapper: InvoiceRecipientDomainMapper; + private _taxesMapper: TaxesDomainMapper; constructor(params: MapperParamsType) { super(); - this._itemsMapper = new CustomerInvoiceItemFullMapper(params); // Instanciar el mapper de items - this._recipientMapper = new InvoiceRecipientFullMapper(); - this._taxesMapper = new TaxesFullMapper(params); - } - - private _mapPaymentMethodToDomain(source: CustomerInvoiceModel, params?: MapperParamsType) { - const { errors } = params as { - errors: ValidationErrorDetail[]; - }; - - const paymentId = extractOrPushError( - maybeFromNullableVO(source.payment_method_id, (value) => UniqueID.create(value)), - "payment_method_id", - errors - ); - - const paymentDescription = extractOrPushError( - maybeFromNullableVO(source.payment_method_description, (value) => Result.ok(String(value))), - "payment_method_description", - errors - ); - - if (errors.length > 0) { - return Result.fail(new ValidationErrorCollection("Invoice payment mapping failed", errors)); - } - - if (paymentDescription!.isNone() || paymentId!.isNone()) { - return Result.ok(Maybe.none()); - } - - const paymentResult = InvoicePaymentMethod.create( - { - paymentDescription: paymentDescription?.getOrUndefined()!, - }, - paymentId?.getOrUndefined()! - ); - - if (paymentResult.isFailure) { - return Result.fail( - new ValidationErrorCollection("Invoice payment method creation failed", [ - { path: "paymentMethod", message: paymentResult.error.message }, - ]) - ); - } - - return Result.ok(Maybe.some(paymentResult.data)); + this._itemsMapper = new CustomerInvoiceItemDomainMapper(params); // Instanciar el mapper de items + this._recipientMapper = new InvoiceRecipientDomainMapper(); + this._taxesMapper = new TaxesDomainMapper(params); } private _mapAttributesToDomain(source: CustomerInvoiceModel, params?: MapperParamsType) { @@ -120,17 +77,18 @@ export class CustomerInvoiceDomainMapper ); const series = extractOrPushError( - maybeFromNullableVO(source.series, (value) => CustomerInvoiceSerie.create(value)), - "serie", + maybeFromNullableVO(source.series, (v) => CustomerInvoiceSerie.create(v)), + "series", errors ); const invoiceNumber = extractOrPushError( - maybeFromNullableVO(source.invoice_number, (value) => CustomerInvoiceNumber.create(value)), + maybeFromNullableVO(source.invoice_number, (v) => CustomerInvoiceNumber.create(v)), "invoice_number", errors ); + // Fechas const invoiceDate = extractOrPushError( UtcDate.createFromISO(source.invoice_date), "invoice_date", @@ -138,11 +96,25 @@ export class CustomerInvoiceDomainMapper ); const operationDate = extractOrPushError( - maybeFromNullableVO(source.operation_date, (value) => UtcDate.createFromISO(value)), + maybeFromNullableVO(source.operation_date, (v) => UtcDate.createFromISO(v)), "operation_date", errors ); + // Idioma / divisa + const languageCode = extractOrPushError( + LanguageCode.create(source.language_code), + "language_code", + errors + ); + + const currencyCode = extractOrPushError( + CurrencyCode.create(source.currency_code), + "currency_code", + errors + ); + + // Textos opcionales const reference = extractOrPushError( maybeFromNullableVO(source.reference, (value) => Result.ok(String(value))), "reference", @@ -161,22 +133,35 @@ export class CustomerInvoiceDomainMapper errors ); - const languageCode = extractOrPushError( - LanguageCode.create(source.language_code), - "language_code", - errors - ); + // Método de pago (VO opcional con id + descripción) + let paymentMethod = Maybe.none(); - const currencyCode = extractOrPushError( - CurrencyCode.create(source.currency_code), - "currency_code", - errors - ); + if (!isNullishOrEmpty(source.payment_method_id)) { + const paymentId = extractOrPushError( + UniqueID.create(String(source.payment_method_id)), + "paymentMethod.id", + errors + ); + const paymentVO = extractOrPushError( + InvoicePaymentMethod.create( + { paymentDescription: String(source.payment_method_description ?? "") }, + paymentId ?? undefined + ), + "payment_method_description", + errors + ); + + if (paymentVO) { + paymentMethod = Maybe.some(paymentVO); + } + } + + // % descuento (VO) const discountPercentage = extractOrPushError( Percentage.create({ - value: source.discount_percentage_value, - scale: source.discount_percentage_scale, + value: Number(source.discount_percentage_value ?? 0), + scale: Number(source.discount_percentage_scale ?? 2), }), "discount_percentage_value", errors @@ -198,6 +183,7 @@ export class CustomerInvoiceDomainMapper languageCode, currencyCode, discountPercentage, + paymentMethod, }; } @@ -244,33 +230,8 @@ export class CustomerInvoiceDomainMapper }); } - // 4) Taxes (colección a nivel factura) - const taxesResults = this._taxesMapper.mapToDomainCollection( - source.taxes, - source.taxes.length, - { - errors, - attributes, - ...params, - } - ); - - if (taxesResults.isFailure) { - errors.push({ - path: "taxes", - message: taxesResults.error.message, - }); - } - - // Payment method - const paymentMethodResult = this._mapPaymentMethodToDomain(source, { errors, ...params }); - - if (paymentMethodResult.isFailure) { - errors.push({ - path: "paymentMethod", - message: paymentMethodResult.error.message, - }); - } + // Nota: los impuestos a nivel factura (tabla customer_invoice_taxes) se derivan de los items. + // El agregado expone un getter `taxes` (derivado). No se incluye en las props. // 5) Si hubo errores de mapeo, devolvemos colección de validación if (errors.length > 0) { @@ -280,7 +241,6 @@ export class CustomerInvoiceDomainMapper // 6) Construcción del agregado (Dominio) const recipient = recipientResult.data; - const paymentMethod = paymentMethodResult.data; const items = CustomerInvoiceItems.create({ languageCode: attributes.languageCode!, @@ -310,7 +270,7 @@ export class CustomerInvoiceDomainMapper discountPercentage: attributes.discountPercentage!, - paymentMethod: paymentMethod!, + paymentMethod: attributes.paymentMethod!, items, }; @@ -350,7 +310,7 @@ export class CustomerInvoiceDomainMapper const items = itemsResult.data; - // 1) Taxes + // 2) Taxes const taxesResult = this._taxesMapper.mapToPersistenceArray(new Collection(source.taxes), { errors, @@ -378,31 +338,28 @@ export class CustomerInvoiceDomainMapper // 7) Si hubo errores de mapeo, devolvemos colección de validación if (errors.length > 0) { - return Result.fail( - new ValidationErrorCollection("Customer invoice mapping to persistence failed", errors) - ); + return Result.fail(new ValidationErrorCollection(errors)); } const invoiceValues: CustomerInvoiceCreationAttributes = { + // Identificación id: source.id.toPrimitive(), company_id: source.companyId.toPrimitive(), + // Flags / estado / serie / número is_proforma: source.isProforma, status: source.status.toPrimitive(), - series: toNullable(source.series, (series) => series.toPrimitive()), - invoice_number: toNullable(source.invoiceNumber, (invoiceNumber) => - invoiceNumber.toPrimitive() - ), + series: toNullable(source.series, (v) => v.toPrimitive()), + invoice_number: toNullable(source.invoiceNumber, (v) => v.toPrimitive()), + invoice_date: source.invoiceDate.toPrimitive(), - operation_date: toNullable(source.operationDate, (operationDate) => - operationDate.toPrimitive() - ), + operation_date: toNullable(source.operationDate, (v) => v.toPrimitive()), language_code: source.languageCode.toPrimitive(), currency_code: source.currencyCode.toPrimitive(), reference: toNullable(source.reference, (reference) => reference), description: toNullable(source.description, (description) => description), - notes: toNullable(source.notes, (notes) => notes.toPrimitive()), + notes: toNullable(source.notes, (v) => v.toPrimitive()), subtotal_amount_value: allAmounts.subtotalAmount.value, subtotal_amount_scale: allAmounts.subtotalAmount.scale, @@ -430,6 +387,7 @@ export class CustomerInvoiceDomainMapper customer_id: source.customerId.toPrimitive(), ...recipient, + taxes, items, }; @@ -442,9 +400,10 @@ export class CustomerInvoiceDomainMapper errors: ValidationErrorDetail[]; }; - const recipient = source.recipient.getOrUndefined(); + const hasRecipient = source.hasRecipient; + const recipient = source.recipient!.getOrUndefined(); - if (!source.isProforma && !recipient) { + if (!source.isProforma && !hasRecipient) { errors.push({ path: "recipient", message: "[CustomerInvoiceDomainMapper] Issued customer invoice w/o recipient data", diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/domain/invoice-recipient.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/domain/invoice-recipient.mapper.ts index 411b9c63..a9c534b5 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/domain/invoice-recipient.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/domain/invoice-recipient.mapper.ts @@ -106,9 +106,7 @@ export class InvoiceRecipientDomainMapper { if (createResult.isFailure) { return Result.fail( - new ValidationErrorCollection("Invoice recipient entity creation failed", [ - { path: "recipient", message: createResult.error.message }, - ]) + new ValidationErrorCollection([{ path: "recipient", message: createResult.error.message }]) ); } diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/domain/item-taxes.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/domain/item-taxes.mapper.ts index 28621515..59033b0f 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/domain/item-taxes.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/domain/item-taxes.mapper.ts @@ -68,7 +68,7 @@ export class ItemTaxesDomainMapper const createResult = Tax.create(tax!); if (createResult.isFailure) { return Result.fail( - new ValidationErrorCollection("Invoice item tax creation failed", [ + new ValidationErrorCollection([ { path: `taxes[${index}]`, message: createResult.error.message }, ]) );