From 3a0262cbf757f5b224831bebc30d66e4d27f70ec Mon Sep 17 00:00:00 2001 From: david Date: Sun, 14 Jun 2026 14:11:17 +0200 Subject: [PATCH] . --- .../payment-methods/di/payment-methods.di.ts | 14 +- .../mappers/sequelize-domain-mapper.ts | 2 +- .../api/application/common/models/index.ts | 2 + .../invoice-payment-method-read.model.ts | 12 ++ .../invoice-tax-regime-full-read.model.ts | 8 + ...ma-to-issued-invoice-props-converter.di.ts | 8 +- .../issued-invoices/models/index.ts | 1 + .../models/proforma-issue-read.model.ts | 13 ++ ...forma-to-issued-invoice-props-converter.ts | 80 ++++----- .../proformas/di/proforma-issuer.di.ts | 19 ++- .../proformas/di/proforma-use-cases.di.ts | 9 + .../models/proforma-full-read.model.ts | 25 +-- .../proforma-repository.interface.ts | 6 + .../proformas/services/assemblers/index.ts | 1 + .../proforma-issue-read-model.assembler.ts | 61 +++++++ .../application/proformas/services/index.ts | 2 +- .../proformas/services/proforma-issuer.ts | 63 ++----- ...-charger.ts => proforma-status-changer.ts} | 3 +- .../change-status-proforma.use-case.ts | 4 +- .../use-cases/issue-proforma.use-case.ts | 71 +++++--- .../common/value-objects/invoice-status.vo.ts | 26 ++- .../aggregates/proforma.aggregate.ts | 52 +++--- .../api/domain/proformas/services/index.ts | 1 + .../proforma-manual-status-transitions.ts | 27 +++ .../proformas/di/proforma-public-services.ts | 2 +- .../proformas/di/proformas.di.ts | 8 +- .../proformas/di/proforrma-catalog-deps.di.ts | 12 +- .../sequelize-proforma-domain.mapper.ts | 5 +- .../sequelize-proforma-item-domain.mapper.ts | 160 ++++++++++++------ .../repositories/proforma.repository.ts | 37 +++- 30 files changed, 477 insertions(+), 257 deletions(-) create mode 100644 modules/customer-invoices/src/api/application/common/models/invoice-payment-method-read.model.ts create mode 100644 modules/customer-invoices/src/api/application/common/models/invoice-tax-regime-full-read.model.ts create mode 100644 modules/customer-invoices/src/api/application/issued-invoices/models/proforma-issue-read.model.ts create mode 100644 modules/customer-invoices/src/api/application/proformas/services/assemblers/proforma-issue-read-model.assembler.ts rename modules/customer-invoices/src/api/application/proformas/services/{proforma-status-charger.ts => proforma-status-changer.ts} (94%) create mode 100644 modules/customer-invoices/src/api/domain/proformas/services/proforma-manual-status-transitions.ts diff --git a/modules/catalogs/src/api/infrastructure/payment-methods/di/payment-methods.di.ts b/modules/catalogs/src/api/infrastructure/payment-methods/di/payment-methods.di.ts index 4d39790f..cccf84fa 100644 --- a/modules/catalogs/src/api/infrastructure/payment-methods/di/payment-methods.di.ts +++ b/modules/catalogs/src/api/infrastructure/payment-methods/di/payment-methods.di.ts @@ -2,6 +2,9 @@ import { type ModuleParams, type SetupParams, buildTransactionManager } from "@e import type { Sequelize } from "sequelize"; import { + type IPaymentMethodPublicFinder, + PaymentMethodPublicFinder, + PaymentMethodPublicModelMapper, buildPaymentMethodCreator, buildPaymentMethodDeleter, buildPaymentMethodFinder, @@ -11,8 +14,6 @@ import { buildPaymentMethodUpdater, } from "../../../application"; import type { IPaymentMethodRepository } from "../../../application/payment-methods/repositories"; -import type { IPaymentMethodFinder } from "../../../application/payment-methods/services"; -import { PaymentMethodFinder } from "../../../application/payment-methods/services"; import { CreatePaymentMethodUseCase, DeletePaymentMethodByIdUseCase, @@ -116,8 +117,13 @@ export const buildPaymentMethodsDependencies = ( export const buildPaymentMethodsPublicServices = ( _params: SetupParams, deps: PaymentMethodsInternalDeps -): { finder: IPaymentMethodFinder } => { +): { finder: IPaymentMethodPublicFinder } => { + const mapper = new PaymentMethodPublicModelMapper(); + return { - finder: new PaymentMethodFinder(deps.repository), + finder: new PaymentMethodPublicFinder({ + repository: deps.repository, + mapper, + }), }; }; diff --git a/modules/core/src/api/infrastructure/persistence/sequelize/mappers/sequelize-domain-mapper.ts b/modules/core/src/api/infrastructure/persistence/sequelize/mappers/sequelize-domain-mapper.ts index 4a01c84d..f1212aca 100644 --- a/modules/core/src/api/infrastructure/persistence/sequelize/mappers/sequelize-domain-mapper.ts +++ b/modules/core/src/api/infrastructure/persistence/sequelize/mappers/sequelize-domain-mapper.ts @@ -12,7 +12,7 @@ export abstract class SequelizeDomainMapper | Promise>; + ): Result; public mapToDomainCollection( raws: (TModel | TModelAttributes)[], diff --git a/modules/customer-invoices/src/api/application/common/models/index.ts b/modules/customer-invoices/src/api/application/common/models/index.ts index e69de29b..e69e3f7c 100644 --- a/modules/customer-invoices/src/api/application/common/models/index.ts +++ b/modules/customer-invoices/src/api/application/common/models/index.ts @@ -0,0 +1,2 @@ +export * from "./invoice-payment-method-read.model"; +export * from "./invoice-tax-regime-full-read.model"; diff --git a/modules/customer-invoices/src/api/application/common/models/invoice-payment-method-read.model.ts b/modules/customer-invoices/src/api/application/common/models/invoice-payment-method-read.model.ts new file mode 100644 index 00000000..ac667223 --- /dev/null +++ b/modules/customer-invoices/src/api/application/common/models/invoice-payment-method-read.model.ts @@ -0,0 +1,12 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import type { Maybe } from "@repo/rdx-utils"; + +/** + * Datos del método de pago que completan a la proforma / issued-invoice + */ + +export interface InvoicePaymentMethodReadModel { + id: UniqueID; + name: string; + description: Maybe; +} diff --git a/modules/customer-invoices/src/api/application/common/models/invoice-tax-regime-full-read.model.ts b/modules/customer-invoices/src/api/application/common/models/invoice-tax-regime-full-read.model.ts new file mode 100644 index 00000000..2ee4e8ae --- /dev/null +++ b/modules/customer-invoices/src/api/application/common/models/invoice-tax-regime-full-read.model.ts @@ -0,0 +1,8 @@ +/** + * Datos del régimen de pago que completan a la proforma / issued-invoice + */ + +export interface InvocieTaxRegimeFullReadModel { + code: string; + description: string; +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/di/proforma-to-issued-invoice-props-converter.di.ts b/modules/customer-invoices/src/api/application/issued-invoices/di/proforma-to-issued-invoice-props-converter.di.ts index 33b83f05..494d3bc6 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/di/proforma-to-issued-invoice-props-converter.di.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/di/proforma-to-issued-invoice-props-converter.di.ts @@ -1,10 +1,8 @@ -import { buildCatalogs } from "@erp/core/api"; - import { type IProformaToIssuedInvoiceConverter, ProformaToIssuedInvoiceConverter, } from "../services"; -export function buildProformaToIssuedInvoicePropsConverter(): IProformaToIssuedInvoiceConverter { - return new ProformaToIssuedInvoiceConverter(buildCatalogs()); -} +export const buildProformaToIssuedInvoicePropsConverter = (): IProformaToIssuedInvoiceConverter => { + return new ProformaToIssuedInvoiceConverter(); +}; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/models/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/models/index.ts index c056b01a..e8aa7528 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/models/index.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/models/index.ts @@ -1 +1,2 @@ export * from "./issued-invoice-summary"; +export * from "./proforma-issue-read.model"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/models/proforma-issue-read.model.ts b/modules/customer-invoices/src/api/application/issued-invoices/models/proforma-issue-read.model.ts new file mode 100644 index 00000000..43c9d073 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/models/proforma-issue-read.model.ts @@ -0,0 +1,13 @@ +import type { Proforma } from "../../../domain"; +import type { InvoicePaymentMethodReadModel } from "../../common/models"; + +/** + * Modelo de lectura usado exclusivamente durante la emisión de una proforma. + * + * Combina la proforma validada con los datos externos que deben materializarse + * como snapshot histórico dentro de la factura emitida. + */ +export interface ProformaIssueReadModel { + proforma: Proforma; + paymentMethod: InvoicePaymentMethodReadModel; +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/services/proforma-to-issued-invoice-props-converter.ts b/modules/customer-invoices/src/api/application/issued-invoices/services/proforma-to-issued-invoice-props-converter.ts index 85db04ab..cbc82264 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/services/proforma-to-issued-invoice-props-converter.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/services/proforma-to-issued-invoice-props-converter.ts @@ -1,8 +1,6 @@ // modules/customer-invoices/src/api/application/issued-invoices/services/proforma-to-issued-invoice-props-converter.ts -import type { JsonPaymentCatalogProvider } from "@erp/core"; -import type { ICatalogs } from "@erp/core/api"; -import { DomainError, UtcDate } from "@repo/rdx-ddd"; +import { UtcDate } from "@repo/rdx-ddd"; import { Maybe, Result } from "@repo/rdx-utils"; import { @@ -14,9 +12,10 @@ import { IssuedInvoiceTaxes, type Proforma, } from "../../../domain"; +import type { ProformaIssueReadModel } from "../models"; export interface IProformaToIssuedInvoiceConverter { - toCreateProps(proforma: Proforma): Result; + toCreateProps(source: ProformaIssueReadModel): Result; } /** @@ -27,29 +26,25 @@ export interface IProformaToIssuedInvoiceConverter { */ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoiceConverter { - private readonly paymentCatalog: JsonPaymentCatalogProvider; + public toCreateProps(source: ProformaIssueReadModel): Result { + const { proforma } = source; - constructor(catalogs: ICatalogs) { - this.paymentCatalog = catalogs.paymentCatalog; - } + const itemsResult = this.resolveItems(proforma); - public toCreateProps(proforma: Proforma): Result { - const itemsOrResult = this.resolveItems(proforma); - - if (itemsOrResult.isFailure) { - return Result.fail(itemsOrResult.error); + if (itemsResult.isFailure) { + return Result.fail(itemsResult.error); } - const taxesOrResult = this.resolveTaxes(proforma); + const taxesResult = this.resolveTaxes(proforma); - if (taxesOrResult.isFailure) { - return Result.fail(taxesOrResult.error); + if (taxesResult.isFailure) { + return Result.fail(taxesResult.error); } - const paymentOrResult = this.resolvePayment(proforma); + const paymentResult = this.resolvePayment(source); - if (paymentOrResult.isFailure) { - return Result.fail(paymentOrResult.error); + if (paymentResult.isFailure) { + return Result.fail(paymentResult.error); } const proformaTotals = proforma.totals(); @@ -74,17 +69,17 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic notes: proforma.notes, reference: proforma.reference, - paymentMethod: paymentOrResult.data, + paymentMethod: paymentResult.data, customerId: proforma.customerId, recipient: proforma.recipient.getOrUndefined()!, - items: itemsOrResult.data, + items: itemsResult.data, taxes: IssuedInvoiceTaxes.create({ currencyCode: proforma.currencyCode, languageCode: proforma.languageCode, - taxes: taxesOrResult.data, + taxes: taxesResult.data, }), subtotalAmount: proformaTotals.subtotalAmount, @@ -111,13 +106,13 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic const issuedItems: IssuedInvoiceItem[] = []; for (const item of proforma.items.getAll()) { - const itemOrResult = this.resolveItem(proforma, item); + const itemResult = this.resolveItem(proforma, item); - if (itemOrResult.isFailure) { - return Result.fail(itemOrResult.error); + if (itemResult.isFailure) { + return Result.fail(itemResult.error); } - issuedItems.push(itemOrResult.data); + issuedItems.push(itemResult.data); } return Result.ok(issuedItems); @@ -173,13 +168,13 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic const issuedTaxes: IssuedInvoiceTax[] = []; for (const tax of proforma.taxes().getAll()) { - const taxOrResult = this.resolveTax(tax); + const taxResult = this.resolveTax(tax); - if (taxOrResult.isFailure) { - return Result.fail(taxOrResult.error); + if (taxResult.isFailure) { + return Result.fail(taxResult.error); } - issuedTaxes.push(taxOrResult.data); + issuedTaxes.push(taxResult.data); } return Result.ok(issuedTaxes); @@ -203,34 +198,25 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic recAmount: tax.recAmount, retentionCode: tax.retentionCode, - retentionAmount: tax.retentionAmount, retentionPercentage: tax.retentionPercentage, + retentionAmount: tax.retentionAmount, taxesAmount: tax.taxesAmount, }); } - private resolvePayment(proforma: Proforma): Result { - const paymentId = proforma.paymentMethodId.unwrap(); - - const existingPaymentResult = this.paymentCatalog.findById(paymentId.toString()); - if (existingPaymentResult.isNone()) { - return Result.fail( - new DomainError("Missing payment method [ProformaToIssuedInvoiceConverter]") - ); - } - - const paymentMethodOrError = InvoicePaymentMethod.create( + private resolvePayment(source: ProformaIssueReadModel): Result { + const paymentMethodResult = InvoicePaymentMethod.create( { - name: existingPaymentResult.unwrap().description, + name: source.paymentMethod.name, }, - paymentId + source.paymentMethod.id ); - if (paymentMethodOrError.isFailure) { - return Result.fail(paymentMethodOrError.error); + if (paymentMethodResult.isFailure) { + return Result.fail(paymentMethodResult.error); } - return Result.ok(paymentMethodOrError.data); + return Result.ok(paymentMethodResult.data); } } diff --git a/modules/customer-invoices/src/api/application/proformas/di/proforma-issuer.di.ts b/modules/customer-invoices/src/api/application/proformas/di/proforma-issuer.di.ts index df17d7ad..a405a8f4 100644 --- a/modules/customer-invoices/src/api/application/proformas/di/proforma-issuer.di.ts +++ b/modules/customer-invoices/src/api/application/proformas/di/proforma-issuer.di.ts @@ -1,15 +1,20 @@ +import type { IPaymentMethodPublicFinder } from "@erp/catalogs/api"; + import type { IProformaToIssuedInvoiceConverter } from "../../issued-invoices"; -import type { IProformaRepository } from "../repositories"; -import { type IProformaIssuer, ProformaIssuer } from "../services"; +import { type IProformaIssuer, ProformaIssueReadModelAssembler, ProformaIssuer } from "../services"; export const buildProformaIssuer = (params: { proformaConverter: IProformaToIssuedInvoiceConverter; - repository: IProformaRepository; }): IProformaIssuer => { - const { proformaConverter, repository } = params; - return new ProformaIssuer({ - proformaConverter, - repository, + proformaConverter: params.proformaConverter, + }); +}; + +export const buildProformaIssueReadModelAssembler = (params: { + paymentMethodFinder: IPaymentMethodPublicFinder; +}) => { + return new ProformaIssueReadModelAssembler({ + paymentMethodFinder: params.paymentMethodFinder, }); }; diff --git a/modules/customer-invoices/src/api/application/proformas/di/proforma-use-cases.di.ts b/modules/customer-invoices/src/api/application/proformas/di/proforma-use-cases.di.ts index a3330ffd..909ce218 100644 --- a/modules/customer-invoices/src/api/application/proformas/di/proforma-use-cases.di.ts +++ b/modules/customer-invoices/src/api/application/proformas/di/proforma-use-cases.di.ts @@ -2,10 +2,12 @@ import type { ITransactionManager } from "@erp/core/api"; import type { IIssuedInvoicePublicServices } from "../../issued-invoices"; import type { ICreateProformaInputMapper, IUpdateProformaInputMapper } from "../mappers"; +import type { IProformaRepository } from "../repositories"; import type { IProformaCreator, IProformaFinder, IProformaFullReadModelAssembler, + IProformaIssueReadModelAssembler, IProformaIssuer, IProformaStatusChanger, IProformaUpdater, @@ -92,18 +94,25 @@ export function buildIssueProformaUseCase(deps: { }; finder: IProformaFinder; issuer: IProformaIssuer; + issueReadModelAssembler: IProformaIssueReadModelAssembler; + repository: IProformaRepository; transactionManager: ITransactionManager; }) { const { finder, issuer, + issueReadModelAssembler, + repository, transactionManager, publicServices: { issuedInvoiceServices }, } = deps; + return new IssueProformaUseCase({ issuedInvoiceServices, finder, issuer, + issueReadModelAssembler, + repository, transactionManager, }); } diff --git a/modules/customer-invoices/src/api/application/proformas/models/proforma-full-read.model.ts b/modules/customer-invoices/src/api/application/proformas/models/proforma-full-read.model.ts index 85305760..c2be60d6 100644 --- a/modules/customer-invoices/src/api/application/proformas/models/proforma-full-read.model.ts +++ b/modules/customer-invoices/src/api/application/proformas/models/proforma-full-read.model.ts @@ -1,23 +1,10 @@ import type { Maybe } from "@repo/rdx-utils"; import type { Proforma } from "../../../domain"; - -/** - * Datos del método de pago que completan a la proforma. - */ -export interface ProformaPaymentMethodReadModel { - id: string; - name: string; - description: Maybe; -} - -/** - * Datos del régimen de pago que completan a la proforma. - */ -export interface ProformaTaxRegimeFullReadModel { - code: string; - description: string; -} +import type { + InvocieTaxRegimeFullReadModel, + InvoicePaymentMethodReadModel, +} from "../../common/models"; /** * Modelo de una proforma con datos accesorios. @@ -27,6 +14,6 @@ export interface ProformaTaxRegimeFullReadModel { */ export interface ProformaFullReadModel { proforma: Proforma; - paymentMethod: Maybe; - taxRegime: Maybe; + paymentMethod: Maybe; + taxRegime: Maybe; } diff --git a/modules/customer-invoices/src/api/application/proformas/repositories/proforma-repository.interface.ts b/modules/customer-invoices/src/api/application/proformas/repositories/proforma-repository.interface.ts index 56cef017..74ed220b 100644 --- a/modules/customer-invoices/src/api/application/proformas/repositories/proforma-repository.interface.ts +++ b/modules/customer-invoices/src/api/application/proformas/repositories/proforma-repository.interface.ts @@ -40,4 +40,10 @@ export interface IProformaRepository { newStatus: InvoiceStatus, transaction: unknown ): Promise>; + + markAsIssuedByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction: unknown + ): Promise>; } diff --git a/modules/customer-invoices/src/api/application/proformas/services/assemblers/index.ts b/modules/customer-invoices/src/api/application/proformas/services/assemblers/index.ts index 6123aa1c..09171a17 100644 --- a/modules/customer-invoices/src/api/application/proformas/services/assemblers/index.ts +++ b/modules/customer-invoices/src/api/application/proformas/services/assemblers/index.ts @@ -1 +1,2 @@ export * from "./proforma-full-read-model.assembler"; +export * from "./proforma-issue-read-model.assembler"; diff --git a/modules/customer-invoices/src/api/application/proformas/services/assemblers/proforma-issue-read-model.assembler.ts b/modules/customer-invoices/src/api/application/proformas/services/assemblers/proforma-issue-read-model.assembler.ts new file mode 100644 index 00000000..ac4ea132 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/services/assemblers/proforma-issue-read-model.assembler.ts @@ -0,0 +1,61 @@ +import type { IPaymentMethodPublicFinder } from "@erp/catalogs/api"; +import type { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { Proforma } from "../../../../domain"; +import type { ProformaIssueReadModel } from "../../../issued-invoices"; + +export interface IProformaIssueReadModelAssembler { + assemble(params: { + companyId: UniqueID; + proforma: Proforma; + transaction?: unknown; + }): Promise>; +} + +/** + * Prepara los datos externos necesarios para emitir una proforma. + * + * Esta pieza materializa snapshots externos que pasarán a formar parte histórica + * de la factura emitida. + */ +export class ProformaIssueReadModelAssembler implements IProformaIssueReadModelAssembler { + public constructor( + private readonly deps: { + paymentMethodFinder: IPaymentMethodPublicFinder; + } + ) {} + + public async assemble(params: { + companyId: UniqueID; + proforma: Proforma; + transaction?: unknown; + }): Promise> { + if (params.proforma.paymentMethodId.isNone()) { + return Result.fail(new Error("Payment method is required to issue proforma")); + } + + const paymentMethodId = params.proforma.paymentMethodId.unwrap(); + + const paymentMethodResult = await this.deps.paymentMethodFinder.getByIdInCompany({ + companyId: params.companyId, + id: paymentMethodId, + transaction: params.transaction, + }); + + if (paymentMethodResult.isFailure) { + return Result.fail(paymentMethodResult.error); + } + + const paymentMethod = paymentMethodResult.data; + + return Result.ok({ + proforma: params.proforma, + paymentMethod: { + id: paymentMethod.id, + name: paymentMethod.name, + description: paymentMethod.description, + }, + }); + } +} diff --git a/modules/customer-invoices/src/api/application/proformas/services/index.ts b/modules/customer-invoices/src/api/application/proformas/services/index.ts index fd2211f1..a32f9629 100644 --- a/modules/customer-invoices/src/api/application/proformas/services/index.ts +++ b/modules/customer-invoices/src/api/application/proformas/services/index.ts @@ -8,5 +8,5 @@ export * from "./proforma-finder"; export * from "./proforma-issuer"; export * from "./proforma-number-generator.interface"; export * from "./proforma-public-services.interface"; -export * from "./proforma-status-charger"; +export * from "./proforma-status-changer"; export * from "./proforma-updater"; diff --git a/modules/customer-invoices/src/api/application/proformas/services/proforma-issuer.ts b/modules/customer-invoices/src/api/application/proformas/services/proforma-issuer.ts index c5fa3f5f..f6c06eb2 100644 --- a/modules/customer-invoices/src/api/application/proformas/services/proforma-issuer.ts +++ b/modules/customer-invoices/src/api/application/proformas/services/proforma-issuer.ts @@ -1,65 +1,34 @@ -import type { UniqueID } from "@repo/rdx-ddd"; +// modules/customer-invoices/src/api/application/proformas/services/proforma-issuer.ts import { Result } from "@repo/rdx-utils"; -import type { IIssuedInvoiceCreateProps, Proforma } from "../../../domain"; -import type { IProformaToIssuedInvoiceConverter } from "../../issued-invoices"; -import type { IProformaRepository } from "../repositories"; +import type { IIssuedInvoiceCreateProps } from "../../../domain"; +import type { + IProformaToIssuedInvoiceConverter, + ProformaIssueReadModel, +} from "../../issued-invoices"; export interface IProformaIssuerParams { - companyId: UniqueID; - proforma: Proforma; - issuedInvoiceId: UniqueID; - transaction: unknown; + source: ProformaIssueReadModel; } export interface IProformaIssuer { - issueProforma(params: IProformaIssuerParams): Promise>; + issueProforma(params: IProformaIssuerParams): Result; } -type ProformaIssuerDeps = { - proformaConverter: IProformaToIssuedInvoiceConverter; - repository: IProformaRepository; -}; - export class ProformaIssuer implements IProformaIssuer { - private readonly proformaConverter: IProformaToIssuedInvoiceConverter; - private readonly repository: IProformaRepository; + public constructor( + private readonly deps: { + proformaConverter: IProformaToIssuedInvoiceConverter; + } + ) {} - constructor(deps: ProformaIssuerDeps) { - this.proformaConverter = deps.proformaConverter; - this.repository = deps.repository; - } + public issueProforma(params: IProformaIssuerParams): Result { + const issueResult = params.source.proforma.markAsIssued(); - public async issueProforma( - params: IProformaIssuerParams - ): Promise> { - const { proforma, companyId, transaction } = params; - - // Cambiamos el estado de la proforma a 'issued' - const issueResult = proforma.issue(); if (issueResult.isFailure) { return Result.fail(issueResult.error); } - // Persistir - const updateStatusResult = await this.repository.updateStatusByIdInCompany( - companyId, - proforma.id, - proforma.status, - transaction - ); - - if (updateStatusResult.isFailure) { - return Result.fail(updateStatusResult.error); - } - - // Generamos las propiedades de la factura a partir de la proforma - const propsResult = this.proformaConverter.toCreateProps(proforma); - - if (propsResult.isFailure) { - return Result.fail(propsResult.error); - } - - return Result.ok(propsResult.data); + return this.deps.proformaConverter.toCreateProps(params.source); } } diff --git a/modules/customer-invoices/src/api/application/proformas/services/proforma-status-charger.ts b/modules/customer-invoices/src/api/application/proformas/services/proforma-status-changer.ts similarity index 94% rename from modules/customer-invoices/src/api/application/proformas/services/proforma-status-charger.ts rename to modules/customer-invoices/src/api/application/proformas/services/proforma-status-changer.ts index 6eb5d565..8995eb35 100644 --- a/modules/customer-invoices/src/api/application/proformas/services/proforma-status-charger.ts +++ b/modules/customer-invoices/src/api/application/proformas/services/proforma-status-changer.ts @@ -1,8 +1,7 @@ import type { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import type { Proforma } from "../../../domain"; -import { InvoiceStatus } from "../../../domain"; +import { InvoiceStatus, type Proforma } from "../../../domain"; import type { IProformaRepository } from "../repositories"; export interface IProformaStatusChanger { diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/change-status-proforma.use-case.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/change-status-proforma.use-case.ts index ce3ac72d..851ffbb3 100644 --- a/modules/customer-invoices/src/api/application/proformas/use-cases/change-status-proforma.use-case.ts +++ b/modules/customer-invoices/src/api/application/proformas/use-cases/change-status-proforma.use-case.ts @@ -35,13 +35,11 @@ export class ChangeStatusProformaUseCase { return Result.fail(proformaIdResult.error); } - const proformaId = proformaIdResult.data; - return this.deps.transactionManager.complete(async (transaction) => { try { const changeResult = await this.deps.statusChanger.changeStatus({ companyId, - id: proformaId, + id: proformaIdResult.data, newStatus: new_status!, transaction, }); diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/issue-proforma.use-case.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/issue-proforma.use-case.ts index 10d24f0e..12deab54 100644 --- a/modules/customer-invoices/src/api/application/proformas/use-cases/issue-proforma.use-case.ts +++ b/modules/customer-invoices/src/api/application/proformas/use-cases/issue-proforma.use-case.ts @@ -3,7 +3,12 @@ import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import type { IIssuedInvoicePublicServices } from "../../issued-invoices"; -import type { IProformaFinder, IProformaIssuer } from "../services"; +import type { IProformaRepository } from "../repositories"; +import type { + IProformaFinder, + IProformaIssueReadModelAssembler, + IProformaIssuer, +} from "../services"; type IssueProformaUseCaseInput = { companyId: UniqueID; @@ -11,13 +16,13 @@ type IssueProformaUseCaseInput = { }; /** - * Caso de uso: Conversión de una issuedinvoice a factura definitiva. + * Caso de uso: conversión de una proforma en factura definitiva. * - * - Recupera la proforma - * - Valida su estado ("approved") - * - Genera la factura definitiva (nueva entidad) - * - Marca la proforma como "issued" - * - Persiste ambas dentro de la misma transacción + * - Recupera la proforma. + * - Valida su emisión mediante dominio. + * - Crea la factura definitiva. + * - Marca la proforma como emitida. + * - Ejecuta todo dentro de una única transacción. */ export class IssueProformaUseCase { public constructor( @@ -25,6 +30,8 @@ export class IssueProformaUseCase { issuedInvoiceServices: IIssuedInvoicePublicServices; finder: IProformaFinder; issuer: IProformaIssuer; + issueReadModelAssembler: IProformaIssueReadModelAssembler; + repository: IProformaRepository; transactionManager: ITransactionManager; } ) {} @@ -32,10 +39,13 @@ export class IssueProformaUseCase { public execute(params: IssueProformaUseCaseInput) { const { proforma_id, companyId } = params; - const proformaIdOrError = UniqueID.create(proforma_id); - if (proformaIdOrError.isFailure) return Result.fail(proformaIdOrError.error); + const proformaIdResult = UniqueID.create(proforma_id); - const proformaId = proformaIdOrError.data; + if (proformaIdResult.isFailure) { + return Result.fail(proformaIdResult.error); + } + + const proformaId = proformaIdResult.data; return this.deps.transactionManager.complete(async (transaction) => { try { @@ -46,28 +56,36 @@ export class IssueProformaUseCase { transaction ); - if (proformaResult.isFailure) return Result.fail(proformaResult.error); + if (proformaResult.isFailure) { + return Result.fail(proformaResult.error); + } + const proforma = proformaResult.data; - // 2. Generamos la factura definitiva y la guardamos + // 2. Generamos la factura definitiva const issuedInvoiceId = UniqueID.generateNewID(); - const createPropsOrError = await this.deps.issuer.issueProforma({ + + const issueReadModelResult = await this.deps.issueReadModelAssembler.assemble({ companyId, - issuedInvoiceId, proforma, transaction, }); - if (createPropsOrError.isFailure) { - return Result.fail(createPropsOrError.error); + if (issueReadModelResult.isFailure) { + return Result.fail(issueReadModelResult.error); } - const createProps = createPropsOrError.data; + const createPropsResult = this.deps.issuer.issueProforma({ + source: issueReadModelResult.data, + }); + + if (createPropsResult.isFailure) { + return Result.fail(createPropsResult.error); + } - // Creamos y guardamos en persistencia la factura definitiva const invoiceResult = await this.deps.issuedInvoiceServices.createIssuedInvoice( issuedInvoiceId, - createProps, + createPropsResult.data, { companyId, transaction, @@ -78,12 +96,21 @@ export class IssueProformaUseCase { return Result.fail(invoiceResult.error); } - const dto = { + const markAsIssuedResult = await this.deps.repository.markAsIssuedByIdInCompany( + companyId, + proformaId, + transaction + ); + + if (markAsIssuedResult.isFailure) { + return Result.fail(markAsIssuedResult.error); + } + + return Result.ok({ issuedinvoice_id: issuedInvoiceId.toString(), proforma_id: proformaId.toString(), customer_id: proforma.customerId.toString(), - }; - return Result.ok(dto); + }); } catch (error: unknown) { return Result.fail(error as Error); } diff --git a/modules/customer-invoices/src/api/domain/common/value-objects/invoice-status.vo.ts b/modules/customer-invoices/src/api/domain/common/value-objects/invoice-status.vo.ts index 2285db8c..ae4ecabb 100644 --- a/modules/customer-invoices/src/api/domain/common/value-objects/invoice-status.vo.ts +++ b/modules/customer-invoices/src/api/domain/common/value-objects/invoice-status.vo.ts @@ -25,29 +25,19 @@ const INVOICE_TRANSITIONS: Record = { }; export class InvoiceStatus extends ValueObject { - private static readonly ALLOWED_STATUSES = ["draft", "sent", "approved", "rejected", "issued"]; + private static readonly ALLOWED_STATUSES = Object.values(INVOICE_STATUS); private static readonly FIELD = "invoiceStatus"; private static readonly ERROR_CODE = "INVALID_INVOICE_STATUS"; static create(value: string): Result { - if (!InvoiceStatus.ALLOWED_STATUSES.includes(value)) { + if (!InvoiceStatus.ALLOWED_STATUSES.includes(value as INVOICE_STATUS)) { const detail = `Estado de la factura no válido: ${value}`; return Result.fail( new DomainValidationError(InvoiceStatus.ERROR_CODE, InvoiceStatus.FIELD, detail) ); } - return Result.ok( - value === "rejected" - ? InvoiceStatus.rejected() - : value === "sent" - ? InvoiceStatus.sent() - : value === "issued" - ? InvoiceStatus.issued() - : value === "approved" - ? InvoiceStatus.approved() - : InvoiceStatus.draft() - ); + return Result.ok(new InvoiceStatus({ value: value as INVOICE_STATUS })); } public static draft(): InvoiceStatus { @@ -94,10 +84,18 @@ export class InvoiceStatus extends ValueObject { return INVOICE_TRANSITIONS[this.props.value].includes(nextStatus); } - public isIssued(): boolean { + isIssued(): boolean { return this.props.value === INVOICE_STATUS.ISSUED; } + isRejected(): boolean { + return this.props.value === INVOICE_STATUS.REJECTED; + } + + isSent(): boolean { + return this.props.value === INVOICE_STATUS.SENT; + } + toString() { return String(this.props.value); } 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 34624e0d..dc96198f 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 @@ -29,6 +29,7 @@ import { } from "../entities"; import { InvalidProformaTransitionError, ProformaItemMismatch } from "../errors"; import type { IProformaTaxTotals, ProformaCalculationContext } from "../services"; +import { canManuallyTransitionProformaStatus } from "../services"; import { ProformaItemTaxes } from "../value-objects"; export interface IProformaCreateProps { @@ -301,6 +302,12 @@ export class Proforma extends AggregateRoot implements IP return this.taxRegimeCode.isSome(); } + /** + * Cambia manualmente el estado de la proforma. + * + * No permite marcar una proforma como `issued`, porque esa transición implica + * crear una factura emitida y debe ejecutarse mediante el caso de uso de emisión. + */ public changeStatus(nextStatus: InvoiceStatus): Result { const currentStatus = this.status; @@ -308,17 +315,7 @@ export class Proforma extends AggregateRoot implements IP return Result.ok(false); } - if (nextStatus.toPrimitive() === INVOICE_STATUS.ISSUED) { - return Result.fail( - new InvalidProformaTransitionError( - currentStatus.toPrimitive(), - nextStatus.toPrimitive(), - this.id.toString() - ) - ); - } - - if (!currentStatus.canTransitionTo(nextStatus)) { + if (!canManuallyTransitionProformaStatus({ currentStatus, nextStatus })) { return Result.fail( new InvalidProformaTransitionError( currentStatus.toPrimitive(), @@ -333,21 +330,39 @@ export class Proforma extends AggregateRoot implements IP return Result.ok(true); } - public issue(): Result { + public markAsIssued(): Result { + const currentStatus = this.status; + + if (currentStatus.isIssued()) { + return Result.ok(); + } + // Antes de cambiar el estado de la proforma, // comprobamos que se cumplen las condiciones // necesarias. - if (!this.props.status.canTransitionTo("issued")) { + if (!currentStatus.isApproved()) { return Result.fail( - new DomainValidationError( - "INVALID_STATE", - "status", - "Proforma cannot be issued from current state" + new InvalidProformaTransitionError( + currentStatus.toPrimitive(), + INVOICE_STATUS.ISSUED, + this.id.toString() ) ); } + const validationResult = this.validateCanBeIssued(); + + if (validationResult.isFailure) { + return Result.fail(validationResult.error); + } + + this.props.status = InvoiceStatus.issued(); + + return Result.ok(); + } + + private validateCanBeIssued(): Result { if (this.series.isNone()) { return Result.fail( new DomainValidationError( @@ -444,12 +459,11 @@ export class Proforma extends AggregateRoot implements IP new DomainValidationError( "LINKED_INVOICE_NOT_ALLOWED", "linkedInvoiceId", - "Proforma cannot be linked to an invoice" + "Proforma is already linked to an invoice" ) ); } - this.props.status = InvoiceStatus.issued(); return Result.ok(); } diff --git a/modules/customer-invoices/src/api/domain/proformas/services/index.ts b/modules/customer-invoices/src/api/domain/proformas/services/index.ts index bf7459f9..d656ca85 100644 --- a/modules/customer-invoices/src/api/domain/proformas/services/index.ts +++ b/modules/customer-invoices/src/api/domain/proformas/services/index.ts @@ -1,4 +1,5 @@ export * from "./proforma-compare-tax-totals"; export * from "./proforma-compute-tax-groups"; export * from "./proforma-items-totals-calculator"; +export * from "./proforma-manual-status-transitions"; export * from "./proforma-taxes-calculator"; diff --git a/modules/customer-invoices/src/api/domain/proformas/services/proforma-manual-status-transitions.ts b/modules/customer-invoices/src/api/domain/proformas/services/proforma-manual-status-transitions.ts new file mode 100644 index 00000000..06100499 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/proformas/services/proforma-manual-status-transitions.ts @@ -0,0 +1,27 @@ +import { INVOICE_STATUS, type InvoiceStatus } from "../.."; + +/** + * Transiciones manuales permitidas para una proforma. + * + * No incluye `approved -> issued` porque emitir una proforma crea una factura emitida + * y debe pasar siempre por el caso de uso específico de emisión. + */ +const PROFORMA_MANUAL_STATUS_TRANSITIONS: Readonly< + Record +> = { + [INVOICE_STATUS.DRAFT]: [INVOICE_STATUS.SENT], + [INVOICE_STATUS.SENT]: [INVOICE_STATUS.APPROVED, INVOICE_STATUS.REJECTED], + [INVOICE_STATUS.APPROVED]: [INVOICE_STATUS.REJECTED, INVOICE_STATUS.DRAFT], + [INVOICE_STATUS.REJECTED]: [INVOICE_STATUS.DRAFT], + [INVOICE_STATUS.ISSUED]: [], +}; + +export function canManuallyTransitionProformaStatus(params: { + currentStatus: InvoiceStatus; + nextStatus: InvoiceStatus; +}): boolean { + const current = params.currentStatus.toPrimitive() as INVOICE_STATUS; + const next = params.nextStatus.toPrimitive() as INVOICE_STATUS; + + return PROFORMA_MANUAL_STATUS_TRANSITIONS[current].includes(next); +} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-public-services.ts b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-public-services.ts index e29953bb..d1943716 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-public-services.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-public-services.ts @@ -14,11 +14,11 @@ import { } from "../../../application"; import type { Proforma } from "../../../domain"; -import { resolveProformaCatalogsDeps } from "./proforma-catalog-deps.di"; import { buildProformaNumberGenerator } from "./proforma-number-generator.di"; import { buildProformaPersistenceMappers } from "./proforma-persistence-mappers.di"; import { buildProformaRepository } from "./proforma-repositories.di"; import type { ProformasInternalDeps } from "./proformas.di"; +import { resolveProformaCatalogsDeps } from "./proforrma-catalog-deps.di"; type ProformaServicesContext = { transaction: Transaction; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts b/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts index 059f993f..d4aac4d1 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts @@ -18,6 +18,7 @@ import { buildProformaCreator, buildProformaFinder, buildProformaInputMappers, + buildProformaIssueReadModelAssembler, buildProformaIssuer, buildProformaReadModelAssemblers, buildProformaSnapshotBuilders, @@ -104,7 +105,10 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter const issuer = buildProformaIssuer({ proformaConverter: proformaToIssuedInvoiceConverter, - repository, + }); + + const issueReadModelAssembler = buildProformaIssueReadModelAssembler({ + paymentMethodFinder: catalogs.paymentMethod.finder, }); const documentGeneratorPipeline = buildProformaDocumentService(params); @@ -159,6 +163,8 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter publicServices, finder, issuer, + issueReadModelAssembler, + repository, transactionManager, }), diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/di/proforrma-catalog-deps.di.ts b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforrma-catalog-deps.di.ts index 6ed7825b..00c3a289 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/di/proforrma-catalog-deps.di.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforrma-catalog-deps.di.ts @@ -15,24 +15,24 @@ type ProformaCatalogsDeps = { export function resolveProformaCatalogsDeps(params: ModuleParams): ProformaCatalogsDeps { const taxDefinition = - params.getService("catalogs:taxDefinition"); + params.getService("catalogs:taxDefinitions"); if (!taxDefinition?.finder) { - throw new Error("Missing public service: catalogs:taxDefinition.finder"); + throw new Error("Missing public service: catalogs:taxDefinitions.finder"); } const taxRegime = - params.getService("catalogs:taxRegime"); + params.getService("catalogs:taxRegimes"); if (!taxRegime?.finder) { - throw new Error("Missing public service: catalogs:taxRegime.finder"); + throw new Error("Missing public service: catalogs:taxRegimes.finder"); } const paymentMethod = - params.getService("catalogs:paymentMethod"); + params.getService("catalogs:paymentMethods"); if (!paymentMethod?.finder) { - throw new Error("Missing public service: catalogs:paymentMethod.finder"); + throw new Error("Missing public service: catalogs:paymentMethods.finder"); } return { 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 3c9fe0a9..faf4bee8 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 @@ -275,7 +275,7 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper< } } - public async mapToPersistence( + public mapToPersistence( source: Proforma, params?: MapperParamsType ): Result { @@ -350,7 +350,10 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper< notes: maybeToNullable(source.notes, (v) => v.toPrimitive()), payment_method_id: maybeToNullable(source.paymentMethodId, (value) => value.toPrimitive()), + payment_method_description: null, + tax_regime_code: maybeToNullable(source.taxRegimeCode, (value) => value), + tax_regime_description: null, subtotal_amount_value: allAmounts.subtotalAmount.value, subtotal_amount_scale: allAmounts.subtotalAmount.scale, diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-item-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-item-domain.mapper.ts index 5077951a..daaac792 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-item-domain.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-item-domain.mapper.ts @@ -1,8 +1,12 @@ +// modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-item-domain.mapper.ts import { DiscountPercentage, type MapperParamsType, SequelizeDomainMapper, Tax, + type TaxCalculationBehavior, + type TaxGroup, + TaxPercentage, } from "@erp/core/api"; import { UniqueID, @@ -12,7 +16,7 @@ import { maybeFromNullableResult, maybeToNullable, } from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; +import { Maybe, Result } from "@repo/rdx-utils"; import { type IProformaCreateProps, @@ -54,14 +58,14 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper< ); const description = extractOrPushError( - maybeFromNullableResult(raw.description, (v) => ItemDescription.create(v)), + maybeFromNullableResult(raw.description, (value) => ItemDescription.create(value)), `items[${index}].description`, errors ); const quantity = extractOrPushError( - maybeFromNullableResult(raw.quantity_value, (v) => - ItemQuantity.create({ value: v, scale: raw.quantity_scale }) + maybeFromNullableResult(raw.quantity_value, (value) => + ItemQuantity.create({ value, scale: raw.quantity_scale }) ), `items[${index}].quantity_value`, errors @@ -80,9 +84,9 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper< ); const itemDiscountPercentage = extractOrPushError( - maybeFromNullableResult(raw.item_discount_percentage_value, (v) => + maybeFromNullableResult(raw.item_discount_percentage_value, (value) => DiscountPercentage.create({ - value: v, + value, scale: raw.item_discount_percentage_scale, }) ), @@ -91,22 +95,41 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper< ); const iva = extractOrPushError( - maybeFromNullableResult(raw.iva_code, (code) => Tax.createFromCode(code, this._taxCatalog)), - `items[${index}].iva_code`, + this.mapTaxToDomain({ + code: raw.iva_code, + percentageValue: raw.iva_percentage_value, + percentageScale: raw.iva_percentage_scale, + group: "iva", + calculationBehavior: "additive", + fieldPath: `items[${index}].iva`, + }), + `items[${index}].iva`, errors ); const rec = extractOrPushError( - maybeFromNullableResult(raw.rec_code, (code) => Tax.createFromCode(code, this._taxCatalog)), - `items[${index}].rec_code`, + this.mapTaxToDomain({ + code: raw.rec_code, + percentageValue: raw.rec_percentage_value, + percentageScale: raw.rec_percentage_scale, + group: "surcharge", + calculationBehavior: "additive", + fieldPath: `items[${index}].rec`, + }), + `items[${index}].rec`, errors ); const retention = extractOrPushError( - maybeFromNullableResult(raw.retention_code, (code) => - Tax.createFromCode(code, this._taxCatalog) - ), - `items[${index}].retention_code`, + this.mapTaxToDomain({ + code: raw.retention_code, + percentageValue: raw.retention_percentage_value, + percentageScale: raw.retention_percentage_scale, + group: "retention", + calculationBehavior: "subtractive", + fieldPath: `items[${index}].retention`, + }), + `items[${index}].retention`, errors ); @@ -122,55 +145,94 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper< }; } + private mapTaxToDomain(params: { + code: string | null; + percentageValue: number | null; + percentageScale?: number; + group: TaxGroup; + calculationBehavior: TaxCalculationBehavior; + fieldPath: string; + }): Result, Error> { + if (params.code === null || params.code.trim() === "") { + return Result.ok(Maybe.none()); + } + + if (params.percentageValue === null) { + return Result.fail( + new Error(`${params.fieldPath}.percentage_value is required when tax code is present`) + ); + } + + const percentageResult = TaxPercentage.create({ + value: params.percentageValue, + }); + + if (percentageResult.isFailure) { + return Result.fail(percentageResult.error); + } + + const taxResult = Tax.create({ + code: params.code, + name: params.code, + rate: percentageResult.data, + group: params.group, + calculationBehavior: params.calculationBehavior, + }); + + if (taxResult.isFailure) { + return Result.fail(taxResult.error); + } + + return Result.ok(Maybe.some(taxResult.data)); + } + public mapToDomain( raw: CustomerInvoiceItemModel, params?: MapperParamsType ): Result { - const { errors, index } = params as { + const { errors } = params as { index: number; errors: ValidationErrorDetail[]; parent: Partial; }; - // 1) Valores escalares (atributos generales) const attributes = this.mapAttributesToDomain(raw, params); - // Si hubo errores de mapeo, devolvemos colección de validación if (errors.length > 0) { return Result.fail( new ValidationErrorCollection("Customer invoice item mapping failed [mapToDomain]", errors) ); } - // 2) Construcción del elemento de taxes const taxesResult = ProformaItemTaxes.create({ iva: attributes.iva!, rec: attributes.rec!, retention: attributes.retention!, }); - // 2) Construcción del elemento de dominio - const itemId = attributes.itemId!; - const newItem = ProformaItem.rehydrate( + if (taxesResult.isFailure) { + return Result.fail(taxesResult.error); + } + + const item = ProformaItem.rehydrate( { description: attributes.description!, quantity: attributes.quantity!, unitAmount: attributes.unitAmount!, - itemDiscountPercentage: attributes.itemDiscountPercentage!, taxes: taxesResult.data, }, - itemId + attributes.itemId! ); - return Result.ok(newItem); + return Result.ok(item); } public mapToPersistence( source: ProformaItem, params?: MapperParamsType ): Result { - const { errors, index, parent } = params as { + const { index, parent } = params as { index: number; parent: Proforma; errors: ValidationErrorDetail[]; @@ -187,34 +249,32 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper< invoice_id: parent.id.toPrimitive(), position: index, - description: maybeToNullable(source.description, (v) => v.toPrimitive()), + description: maybeToNullable(source.description, (value) => value.toPrimitive()), - quantity_value: maybeToNullable(source.quantity, (v) => v.toPrimitive().value), + quantity_value: maybeToNullable(source.quantity, (value) => value.toPrimitive().value), quantity_scale: - maybeToNullable(source.quantity, (v) => v.toPrimitive().scale) ?? + maybeToNullable(source.quantity, (value) => value.toPrimitive().scale) ?? ItemQuantity.DEFAULT_SCALE, - unit_amount_value: maybeToNullable(source.unitAmount, (v) => v.toPrimitive().value), + unit_amount_value: maybeToNullable(source.unitAmount, (value) => value.toPrimitive().value), unit_amount_scale: - maybeToNullable(source.unitAmount, (v) => v.toPrimitive().scale) ?? + maybeToNullable(source.unitAmount, (value) => value.toPrimitive().scale) ?? ItemAmount.DEFAULT_SCALE, subtotal_amount_value: allAmounts.subtotalAmount.value, subtotal_amount_scale: allAmounts.subtotalAmount.scale, - // item_discount_percentage_value: maybeToNullable( source.itemDiscountPercentage, - (v) => v.toPrimitive().value + (value) => value.toPrimitive().value ), item_discount_percentage_scale: - maybeToNullable(source.itemDiscountPercentage, (v) => v.toPrimitive().scale) ?? + maybeToNullable(source.itemDiscountPercentage, (value) => value.toPrimitive().scale) ?? DiscountPercentage.DEFAULT_SCALE, item_discount_amount_value: allAmounts.itemDiscountAmount.value, item_discount_amount_scale: allAmounts.itemDiscountAmount.scale, - // global_discount_percentage_value: parent.globalDiscountPercentage.toPrimitive().value, global_discount_percentage_scale: parent.globalDiscountPercentage.toPrimitive().scale ?? DiscountPercentage.DEFAULT_SCALE, @@ -222,52 +282,40 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper< global_discount_amount_value: allAmounts.globalDiscountAmount.value, global_discount_amount_scale: allAmounts.globalDiscountAmount.scale, - // total_discount_amount_value: allAmounts.totalDiscountAmount.value, total_discount_amount_scale: allAmounts.totalDiscountAmount.scale, - // taxable_amount_value: allAmounts.taxableAmount.value, taxable_amount_scale: allAmounts.taxableAmount.scale, - // IVA - iva_code: maybeToNullable(source.taxes.iva, (v) => v.code), - - iva_percentage_value: maybeToNullable(source.taxes.iva, (v) => v.percentage.value), + iva_code: maybeToNullable(source.taxes.iva, (value) => value.code), + iva_percentage_value: maybeToNullable(source.taxes.iva, (value) => value.percentage.value), iva_percentage_scale: - maybeToNullable(source.taxes.iva, (v) => v.percentage.scale) ?? Tax.DEFAULT_SCALE, - + maybeToNullable(source.taxes.iva, (value) => value.percentage.scale) ?? Tax.DEFAULT_SCALE, iva_amount_value: allAmounts.ivaAmount.value, iva_amount_scale: allAmounts.ivaAmount.scale, - // REC - rec_code: maybeToNullable(source.taxes.rec, (v) => v.code), - - rec_percentage_value: maybeToNullable(source.taxes.rec, (v) => v.percentage.value), + rec_code: maybeToNullable(source.taxes.rec, (value) => value.code), + rec_percentage_value: maybeToNullable(source.taxes.rec, (value) => value.percentage.value), rec_percentage_scale: - maybeToNullable(source.taxes.rec, (v) => v.percentage.scale) ?? Tax.DEFAULT_SCALE, - + maybeToNullable(source.taxes.rec, (value) => value.percentage.scale) ?? Tax.DEFAULT_SCALE, rec_amount_value: allAmounts.recAmount.value, rec_amount_scale: allAmounts.recAmount.scale, - // RET - retention_code: maybeToNullable(source.taxes.retention, (v) => v.code), - + retention_code: maybeToNullable(source.taxes.retention, (value) => value.code), retention_percentage_value: maybeToNullable( source.taxes.retention, - (v) => v.percentage.value + (value) => value.percentage.value ), retention_percentage_scale: - maybeToNullable(source.taxes.retention, (v) => v.percentage.scale) ?? Tax.DEFAULT_SCALE, - + maybeToNullable(source.taxes.retention, (value) => value.percentage.scale) ?? + Tax.DEFAULT_SCALE, retention_amount_value: allAmounts.retentionAmount.value, retention_amount_scale: allAmounts.retentionAmount.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, }); diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts index 8746a126..d62306f9 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts @@ -10,7 +10,7 @@ import { type Collection, Result } from "@repo/rdx-utils"; import type { FindOptions, InferAttributes, OrderItem, Sequelize, Transaction } from "sequelize"; import type { IProformaRepository, ProformaSummary } from "../../../../../application"; -import type { InvoiceStatus, Proforma } from "../../../../../domain"; +import { INVOICE_STATUS, type InvoiceStatus, type Proforma } from "../../../../../domain"; import { CustomerInvoiceItemModel, CustomerInvoiceModel, @@ -241,6 +241,41 @@ export class ProformaRepository } } + public async markAsIssuedByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction?: unknown + ): Promise> { + try { + const [affectedRows] = await CustomerInvoiceModel.update( + { + status: INVOICE_STATUS.ISSUED, + }, + { + where: { + id: id.toString(), + company_id: companyId.toString(), + is_proforma: true, + status: INVOICE_STATUS.APPROVED, + }, + transaction: transaction as Transaction, + } + ); + + if (affectedRows !== 1) { + return Result.fail( + new Error( + `Proforma ${id.toString()} could not be marked as issued because it is not approved or does not exist` + ) + ); + } + + return Result.ok(); + } catch (error: unknown) { + return Result.fail(error as Error); + } + } + /** * * Busca una factura por su identificador único.