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 InvoiceRecipient, InvoiceStatus } from "@erp/customer-invoices/api/domain"; import type { CustomerPublicServices } from "@erp/customers/api"; import { type Customer, CustomerStatus, type CustomerTaxesProps, type ICustomerCreateProps, } from "@erp/customers/api/domain"; import { type Name, type PhoneNumber, type TextValue, UniqueID } from "@repo/rdx-ddd"; import { Maybe, Result } from "@repo/rdx-utils"; import type { IProformaCreatorParams } from "node_modules/@erp/customer-invoices/src/api/application"; import type { Transaction } from "sequelize"; import type { CreateProformaFromFactugesRequestDTO } from "../../../common"; import type { ICreateProformaFromFactugesInputMapper, IProformaFromFactuGESProps, } from "../mappers"; type CreateProformaFromFactugesUseCaseInput = { companyId: UniqueID; dto: CreateProformaFromFactugesRequestDTO; }; type CreateProformaFromFactugesUseCaseDeps = { customerServices: CustomerPublicServices; proformaServices: ProformaPublicServices; dtoMapper: ICreateProformaFromFactugesInputMapper; taxCatalog: JsonTaxCatalogProvider; transactionManager: ITransactionManager; }; 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, customerDraft, proformaDraft } = 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; // Crear la proforma para ese cliente const createPropsResult = this.buildProformaCreateProps(proformaDraft, customer.id, { companyId, transaction, }); if (createPropsResult.isFailure) { return Result.fail(createPropsResult.error); } const createResult = await this.proformaServices.createProforma( UniqueID.generateNewID(), createPropsResult.data, { companyId, transaction, } ); if (createResult.isFailure) { return Result.fail(createResult.error); } const proforma = createResult.data; const snapshot = { customer_id: customer.id.toString(), proforma_id: proforma.id.toString(), }; return Result.ok(snapshot); } catch (error: unknown) { return Result.fail(error as Error); } }); } private buildProformaCreateProps( proformaDraft: IProformaFromFactuGESProps["proformaDraft"], customerId: UniqueID, context: { companyId: UniqueID; transaction: Transaction; } ): Result { const { companyId } = context; const defaultStatus = InvoiceStatus.fromApproved(); const recipient = Maybe.none(); return Result.ok({ ...proformaDraft, companyId, customerId, status: defaultStatus, 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: IProformaFromFactuGESProps["customerLookup"], customerDraft: IProformaFromFactuGESProps["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 buildCustomerCreateProps( customerDraft: IProformaFromFactuGESProps["customerDraft"], context: { companyId: UniqueID; transaction: Transaction; } ): Result { const { companyId } = context; const status = CustomerStatus.createActive(); const ivaResult = Tax.createFromCode("iva_21", this.taxCatalog); if (ivaResult.isFailure) { return Result.fail(ivaResult.error); } const defaultTaxes: CustomerTaxesProps = { iva: Maybe.some(ivaResult.data), rec: Maybe.none(), retention: Maybe.none(), }; 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, }); } }