import type { JsonTaxCatalogProvider } from "@erp/core"; import { type ITransactionManager, 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 { type Customer, CustomerStatus, CustomerTaxes, type ICustomerCreateProps, } from "@erp/customers/api/domain"; 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"; import type { CreateProformaFromFactugesRequestDTO } from "../../../common"; import type { FactugesProformaPayload, ICreateProformaFromFactugesInputMapper } from "../mappers"; import paymentsCatalog from "./payments.json"; type FakePaymentMethod = { id: UniqueID; description: string; factuges_id: string; }; type CreateProformaFromFactugesUseCaseInput = { companyId: UniqueID; dto: CreateProformaFromFactugesRequestDTO; }; type CreateProformaFromFactugesUseCaseDeps = { customerServices: CustomerPublicServices; proformaServices: ProformaPublicServices; dtoMapper: ICreateProformaFromFactugesInputMapper; taxCatalog: JsonTaxCatalogProvider; transactionManager: ITransactionManager; }; type CreateProformaProps = Parameters["1"]; export class CreateProformaFromFactugesUseCase { private readonly dtoMapper: ICreateProformaFromFactugesInputMapper; private readonly customerServices: CustomerPublicServices; private readonly proformaServices: ProformaPublicServices; private readonly taxCatalog: JsonTaxCatalogProvider; private readonly transactionManager: ITransactionManager; constructor(deps: CreateProformaFromFactugesUseCaseDeps) { this.customerServices = deps.customerServices; this.proformaServices = deps.proformaServices; this.dtoMapper = deps.dtoMapper; this.taxCatalog = deps.taxCatalog; this.transactionManager = deps.transactionManager; } public async execute(params: CreateProformaFromFactugesUseCaseInput) { const { dto, companyId } = params; // 1) Mapear DTO → props const mappedPropsResult = this.dtoMapper.map(dto, { companyId }); if (mappedPropsResult.isFailure) { return Result.fail(mappedPropsResult.error); } const { customerLookup, paymentLookup, customerDraft, proformaDraft, paymentDraft } = mappedPropsResult.data; return this.transactionManager.complete(async (transaction: Transaction) => { try { const customerResult = await this.resolveCustomer(customerLookup, customerDraft, { companyId, transaction, }); if (customerResult.isFailure) { return Result.fail(customerResult.error); } const customer = customerResult.data; const paymentResult = await this.resolvePayment(paymentLookup, paymentDraft, { companyId, transaction, }); if (paymentResult.isFailure) { return Result.fail(paymentResult.error); } const payment = paymentResult.data; // Crear la proforma para ese cliente const createPropsResult = this.buildProformaCreateProps({ proformaDraft, payment, customerId: customer.id, context: { companyId, transaction, }, }); if (createPropsResult.isFailure) { return Result.fail(createPropsResult.error); } const newId = UniqueID.generateNewID(); const createResult = await this.proformaServices.createProforma( newId, createPropsResult.data, { companyId, transaction, } ); if (createResult.isFailure) { 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, { companyId, transaction, } ); if (readResult.isFailure) { return Result.fail(readResult.error); } const snapshot = readResult.data; const result = { customer_id: customer.id.toString(), proforma_id: snapshot.id.toString(), }; return Result.ok(result); } catch (error: unknown) { return Result.fail(error as Error); } }); } /** * 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(); 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} FactuGES: ${expected.formattedValue}. Calculado: ${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; payment: FakePaymentMethod; context: { companyId: UniqueID; transaction: Transaction; }; }): Result { const { proformaDraft, payment, customerId, context } = deps; const { companyId } = context; const defaultStatus = InvoiceStatus.approved(); const recipient = Maybe.none(); const paymentMethod = Maybe.some( InvoicePaymentMethod.create({ paymentDescription: payment.description }, payment.id).data ); return Result.ok({ ...proformaDraft, companyId, customerId, status: defaultStatus, paymentMethod, recipient, }); } /** * Resuelve un cliente existente o lo crea si todavía no existe. * * Motivo: * - Centraliza la política "find or create" del caso de uso. * - Evita duplicar lógica de control y branching en `execute`. * - Separa los datos de búsqueda de los datos necesarios para alta. * * @param customerLookup - Datos mínimos para localizar un cliente existente. * @param customerDraft - Datos necesarios para crear el cliente si no existe. * @param context - Contexto transaccional y de compañía. * @returns `Result` con el cliente resuelto o el error producido. */ private async resolveCustomer( customerLookup: FactugesProformaPayload["customerLookup"], customerDraft: FactugesProformaPayload["customerDraft"], context: { companyId: UniqueID; transaction: Transaction; } ): Promise> { const { companyId, transaction } = context; const existingCustomerResult = await this.customerServices.findCustomerByTIN( customerLookup.tin, { companyId, transaction } ); if (existingCustomerResult.isSuccess) { return Result.ok(existingCustomerResult.data); } if (!isEntityNotFoundError(existingCustomerResult.error)) { return Result.fail(existingCustomerResult.error); } const createPropsResult = this.buildCustomerCreateProps(customerDraft, context); if (createPropsResult.isFailure) { return Result.fail(createPropsResult.error); } return this.customerServices.createCustomer(UniqueID.generateNewID(), createPropsResult.data, { companyId, transaction, }); } private async resolvePayment( paymentLookup: FactugesProformaPayload["paymentLookup"], paymentDraft: FactugesProformaPayload["paymentDraft"], context: { companyId: UniqueID; transaction: Transaction; } ): Promise> { const { companyId, transaction } = context; const existingPaymentResult = paymentsCatalog.find( (payment) => payment.factuges_id === paymentLookup.factuges_id && payment.company_id === companyId.toString() ); if (existingPaymentResult) { return Result.ok({ id: UniqueID.create(existingPaymentResult.id).data, description: existingPaymentResult.description, factuges_id: existingPaymentResult.factuges_id, }); } return Result.fail(new Error("Forma de pago no existe!!!")); } private buildCustomerCreateProps( customerDraft: FactugesProformaPayload["customerDraft"], context: { companyId: UniqueID; transaction: Transaction; } ): Result { const { companyId } = context; const status = CustomerStatus.createActive(); const defaultTaxes = CustomerTaxes.fromKey("iva_21;#;#", this.taxCatalog); if (defaultTaxes.isFailure) { return Result.fail(defaultTaxes.error); } const tin = Maybe.some(customerDraft.tin); const tradeName = Maybe.none(); const reference = Maybe.none(); const fax = Maybe.none(); const legalRecord = Maybe.none(); return Result.ok({ ...customerDraft, companyId, status, tin, tradeName, reference, fax, legalRecord, defaultTaxes: defaultTaxes.data, }); } }