import { ISequelizeDomainMapper, MapperParamsType, SequelizeDomainMapper } from "@erp/core/api"; import { CurrencyCode, extractOrPushError, LanguageCode, maybeFromNullableVO, Percentage, TextValue, toNullable, UniqueID, UtcDate, ValidationErrorCollection, ValidationErrorDetail, } from "@repo/rdx-ddd"; import { Collection, isNullishOrEmpty, Maybe, Result } from "@repo/rdx-utils"; import { CustomerInvoice, CustomerInvoiceItems, CustomerInvoiceNumber, CustomerInvoiceProps, CustomerInvoiceSerie, CustomerInvoiceStatus, InvoicePaymentMethod, } from "../../../domain"; import { CustomerInvoiceCreationAttributes, CustomerInvoiceModel } from "../../sequelize"; import { CustomerInvoiceItemDomainMapper } from "./customer-invoice-item.mapper"; import { InvoiceRecipientDomainMapper } from "./invoice-recipient.mapper"; import { TaxesDomainMapper } from "./invoice-taxes.mapper"; export interface ICustomerInvoiceDomainMapper extends ISequelizeDomainMapper< CustomerInvoiceModel, CustomerInvoiceCreationAttributes, CustomerInvoice > {} export class CustomerInvoiceDomainMapper extends SequelizeDomainMapper< CustomerInvoiceModel, CustomerInvoiceCreationAttributes, CustomerInvoice > implements ICustomerInvoiceDomainMapper { private _itemsMapper: CustomerInvoiceItemDomainMapper; private _recipientMapper: InvoiceRecipientDomainMapper; private _taxesMapper: TaxesDomainMapper; constructor(params: MapperParamsType) { super(); 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) { const { errors } = params as { errors: ValidationErrorDetail[]; }; const invoiceId = extractOrPushError(UniqueID.create(source.id), "id", errors); const companyId = extractOrPushError(UniqueID.create(source.company_id), "company_id", errors); const customerId = extractOrPushError( UniqueID.create(source.customer_id), "customer_id", errors ); const isProforma = Boolean(source.is_proforma); const proformaId = extractOrPushError( maybeFromNullableVO(source.proforma_id, (v) => UniqueID.create(v)), "proforma_id", errors ); const status = extractOrPushError( CustomerInvoiceStatus.create(source.status), "status", errors ); const series = extractOrPushError( maybeFromNullableVO(source.series, (v) => CustomerInvoiceSerie.create(v)), "series", errors ); const invoiceNumber = extractOrPushError( CustomerInvoiceNumber.create(source.invoice_number), "invoice_number", errors ); // Fechas const invoiceDate = extractOrPushError( UtcDate.createFromISO(source.invoice_date), "invoice_date", errors ); const operationDate = extractOrPushError( 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", errors ); const description = extractOrPushError( maybeFromNullableVO(source.description, (value) => Result.ok(String(value))), "description", errors ); const notes = extractOrPushError( maybeFromNullableVO(source.notes, (value) => TextValue.create(value)), "notes", errors ); // Método de pago (VO opcional con id + descripción) let paymentMethod = Maybe.none(); 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: Number(source.discount_percentage_value ?? 0), scale: Number(source.discount_percentage_scale ?? 2), }), "discount_percentage_value", errors ); return { invoiceId, companyId, customerId, isProforma, proformaId, status, series, invoiceNumber, invoiceDate, operationDate, reference, description, notes, languageCode, currencyCode, discountPercentage, paymentMethod, }; } public mapToDomain( source: CustomerInvoiceModel, params?: MapperParamsType ): Result { try { const errors: ValidationErrorDetail[] = []; // 1) Valores escalares (atributos generales) const attributes = this._mapAttributesToDomain(source, { errors, ...params }); // 2) Recipient (snapshot en la factura o include) const recipientResult = this._recipientMapper.mapToDomain(source, { errors, attributes, ...params, }); /*if (recipientResult.isFailure) { errors.push({ path: "recipient", message: recipientResult.error.message, }); }*/ // 3) Items (colección) const itemsResults = this._itemsMapper.mapToDomainCollection( source.items, source.items.length, { errors, attributes, ...params, } ); /*if (itemsResults.isFailure) { errors.push({ path: "items", message: itemsResults.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) { return Result.fail( new ValidationErrorCollection("Customer invoice mapping failed [mapToDomain]", errors) ); } // 6) Construcción del agregado (Dominio) const recipient = recipientResult.data; const items = CustomerInvoiceItems.create({ languageCode: attributes.languageCode!, currencyCode: attributes.currencyCode!, items: itemsResults.data.getAll(), }); const invoiceProps: CustomerInvoiceProps = { companyId: attributes.companyId!, isProforma: attributes.isProforma, proformaId: attributes.proformaId!, status: attributes.status!, series: attributes.series!, invoiceNumber: attributes.invoiceNumber!, invoiceDate: attributes.invoiceDate!, operationDate: attributes.operationDate!, customerId: attributes.customerId!, recipient: recipient, reference: attributes.reference!, description: attributes.description!, notes: attributes.notes!, languageCode: attributes.languageCode!, currencyCode: attributes.currencyCode!, discountPercentage: attributes.discountPercentage!, paymentMethod: attributes.paymentMethod!, items, }; const createResult = CustomerInvoice.create(invoiceProps, attributes.invoiceId); if (createResult.isFailure) { return Result.fail( new ValidationErrorCollection("Customer invoice entity creation failed", [ { path: "invoice", message: createResult.error.message }, ]) ); } return Result.ok(createResult.data); } catch (err: unknown) { return Result.fail(err as Error); } } public mapToPersistence( source: CustomerInvoice, params?: MapperParamsType ): Result { const errors: ValidationErrorDetail[] = []; // 1) Items const itemsResult = this._itemsMapper.mapToPersistenceArray(source.items, { errors, parent: source, ...params, }); if (itemsResult.isFailure) { errors.push({ path: "items", message: itemsResult.error.message, }); } // 2) Taxes const taxesResult = this._taxesMapper.mapToPersistenceArray(new Collection(source.getTaxes()), { errors, parent: source, ...params, }); if (taxesResult.isFailure) { errors.push({ path: "taxes", message: taxesResult.error.message, }); } // 3) Cliente const recipient = this._recipientMapper.mapToPersistence(source.recipient, { errors, parent: source, ...params, }); // 4) 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) ); } const items = itemsResult.data; const taxes = taxesResult.data; const allAmounts = source.getAllAmounts(); // Da los totales ya calculados const invoiceValues: Partial = { // Identificación id: source.id.toPrimitive(), company_id: source.companyId.toPrimitive(), // Flags / estado / serie / número is_proforma: source.isProforma, proforma_id: toNullable(source.proformaId, (v) => v.toPrimitive()), status: source.status.toPrimitive(), series: toNullable(source.series, (v) => v.toPrimitive()), invoice_number: source.invoiceNumber.toPrimitive(), invoice_date: source.invoiceDate.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, (v) => v.toPrimitive()), subtotal_amount_value: allAmounts.subtotalAmount.value, subtotal_amount_scale: allAmounts.subtotalAmount.scale, discount_percentage_value: source.discountPercentage.toPrimitive().value, discount_percentage_scale: source.discountPercentage.toPrimitive().scale, discount_amount_value: allAmounts.headerDiscountAmount.value, discount_amount_scale: allAmounts.headerDiscountAmount.scale, taxable_amount_value: allAmounts.taxableAmount.value, taxable_amount_scale: allAmounts.taxableAmount.scale, taxes_amount_value: allAmounts.taxesAmount.value, taxes_amount_scale: allAmounts.taxesAmount.scale, total_amount_value: allAmounts.totalAmount.value, total_amount_scale: allAmounts.totalAmount.scale, payment_method_id: toNullable(source.paymentMethod, (payment) => payment.toObjectString().id), payment_method_description: toNullable( source.paymentMethod, (payment) => payment.toObjectString().payment_description ), customer_id: source.customerId.toPrimitive(), ...recipient, taxes, items, }; return Result.ok( invoiceValues as CustomerInvoiceCreationAttributes ); } }