diff --git a/modules/customer-invoices/src/api/application/customer-invoice-application.service.ts b/modules/customer-invoices/src/api/application/customer-invoice-application.service.ts index d147da98..e8cc3a26 100644 --- a/modules/customer-invoices/src/api/application/customer-invoice-application.service.ts +++ b/modules/customer-invoices/src/api/application/customer-invoice-application.service.ts @@ -1,7 +1,12 @@ import { Criteria } from "@repo/rdx-criteria/server"; import { UniqueID } from "@repo/rdx-ddd"; -import { Collection, Result } from "@repo/rdx-utils"; +import { Collection, Maybe, Result } from "@repo/rdx-utils"; import { Transaction } from "sequelize"; +import { + CustomerInvoiceNumber, + CustomerInvoiceSerie, + ICustomerInvoiceNumberGenerator, +} from "../domain"; import { CustomerInvoice, CustomerInvoicePatchProps, @@ -11,17 +16,50 @@ import { ICustomerInvoiceRepository } from "../domain/repositories"; import { CustomerInvoiceListDTO } from "../infrastructure"; export class CustomerInvoiceApplicationService { - constructor(private readonly repository: ICustomerInvoiceRepository) {} + constructor( + private readonly repository: ICustomerInvoiceRepository, + private readonly numberGenerator: ICustomerInvoiceNumberGenerator + ) {} /** - * Construye un nuevo agregado CustomerInvoice a partir de props validadas. + * Devuelve el siguiente nº para proformas + * + * @param companyId - Identificador de la empresa a la que pertenece la proforma. + * @param transaction - Transacción activa para la operación. + * @returns Result - El agregado construido o un error si falla la creación. + */ + async getNextProformaNumber( + companyId: UniqueID, + transaction: Transaction + ): Promise> { + return await this.numberGenerator.nextForCompany(companyId, Maybe.none(), transaction); + } + + /** + * Devuelve el siguiente nº para facturas (issue) * * @param companyId - Identificador de la empresa a la que pertenece la factura. - * @param props - Las propiedades ya validadas para crear la factura. - * @param invoiceId - Identificador UUID de la factura (opcional). + * @param series - Serie por la que buscar la última factura + * @param transaction - Transacción activa para la operación. + * @returns Result - El agregado construido o un error si falla la creación. + */ + async getNextIssueInvoiceNumber( + companyId: UniqueID, + series: Maybe, + transaction: Transaction + ): Promise> { + return await this.numberGenerator.nextForCompany(companyId, series, transaction); + } + + /** + * Construye una proforma a partir de props validadas. + * + * @param companyId - Identificador de la empresa a la que pertenece la proforma. + * @param props - Las propiedades ya validadas para crear la proforma. + * @param invoiceId - Identificador UUID de la proforma (opcional). * @returns Result - El agregado construido o un error si falla la creación. */ - buildInvoiceInCompany( + buildProformaInCompany( companyId: UniqueID, props: Omit, invoiceId?: UniqueID @@ -29,6 +67,28 @@ export class CustomerInvoiceApplicationService { return CustomerInvoice.create({ ...props, companyId }, invoiceId); } + /** + * Construye una factura issue a partir de una proforma. + * + * @param companyId - Identificador de la empresa a la que pertenece la factura. + * @param issueInvoiceId - Identificador UUID de la factura (opcional). + * @param proforma - La proforma de la cual se generará la issue + * @param pathcProps - otros props personalizados que se trasladarán a la issue + * @returns Result - El agregado construido o un error si falla la creación. + */ + buildIssueInvoiceInCompany( + companyId: UniqueID, + proforma: CustomerInvoice, + patchProps: CustomerInvoicePatchProps + ): Result { + const proformaProps = proforma.getIssuedInvoiceProps(); + return CustomerInvoice.create({ + ...proformaProps, + ...patchProps, + companyId, + }); + } + /** * Guarda una nueva factura y devuelve la factura guardada. * diff --git a/modules/customer-invoices/src/api/application/presenters/domain/customer-invoice.full.presenter.ts b/modules/customer-invoices/src/api/application/presenters/domain/customer-invoice.full.presenter.ts index 2154d648..bae3ece1 100644 --- a/modules/customer-invoices/src/api/application/presenters/domain/customer-invoice.full.presenter.ts +++ b/modules/customer-invoices/src/api/application/presenters/domain/customer-invoice.full.presenter.ts @@ -47,7 +47,7 @@ export class CustomerInvoiceFullPresenter extends Presenter< id: invoice.id.toString(), company_id: invoice.companyId.toString(), - invoice_number: toEmptyString(invoice.invoiceNumber, (value) => value.toString()), + invoice_number: invoice.invoiceNumber.toString(), status: invoice.status.toPrimitive(), series: toEmptyString(invoice.series, (value) => value.toString()), diff --git a/modules/customer-invoices/src/api/application/presenters/queries/list-customer-invoices.presenter.ts b/modules/customer-invoices/src/api/application/presenters/queries/list-customer-invoices.presenter.ts index 7d0922d0..aeb8d6cd 100644 --- a/modules/customer-invoices/src/api/application/presenters/queries/list-customer-invoices.presenter.ts +++ b/modules/customer-invoices/src/api/application/presenters/queries/list-customer-invoices.presenter.ts @@ -15,7 +15,7 @@ export class ListCustomerInvoicesPresenter extends Presenter { is_proforma: invoice.isProforma, customer_id: invoice.customerId.toString(), - invoice_number: toEmptyString(invoice.invoiceNumber, (value) => value.toString()), + invoice_number: invoice.invoiceNumber.toString(), status: invoice.status.toPrimitive(), series: toEmptyString(invoice.series, (value) => value.toString()), diff --git a/modules/customer-invoices/src/api/application/specs/index.ts b/modules/customer-invoices/src/api/application/specs/index.ts deleted file mode 100644 index e5c2b414..00000000 --- a/modules/customer-invoices/src/api/application/specs/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./status-invoice_is_approved.spec"; diff --git a/modules/customer-invoices/src/api/application/use-cases/create/create-customer-invoice.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/create/create-customer-invoice.use-case.ts index e59f12d4..e67b2206 100644 --- a/modules/customer-invoices/src/api/application/use-cases/create/create-customer-invoice.use-case.ts +++ b/modules/customer-invoices/src/api/application/use-cases/create/create-customer-invoice.use-case.ts @@ -1,7 +1,7 @@ import { JsonTaxCatalogProvider } from "@erp/core"; import { DuplicateEntityError, IPresenterRegistry, ITransactionManager } from "@erp/core/api"; import { UniqueID } from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; +import { Maybe, Result } from "@repo/rdx-utils"; import { Transaction } from "sequelize"; import { CreateCustomerInvoiceRequestDTO } from "../../../../common/dto"; import { CustomerInvoiceApplicationService } from "../../customer-invoice-application.service"; @@ -21,7 +21,7 @@ export class CreateCustomerInvoiceUseCase { private readonly taxCatalog: JsonTaxCatalogProvider ) {} - public execute(params: CreateCustomerInvoiceUseCaseInput) { + public async execute(params: CreateCustomerInvoiceUseCaseInput) { const { dto, companyId } = params; const presenter = this.presenterRegistry.getPresenter({ resource: "customer-invoice", @@ -37,17 +37,30 @@ export class CreateCustomerInvoiceUseCase { const { props, id } = dtoResult.data; - // 2) Construir entidad de dominio - const buildResult = this.service.buildInvoiceInCompany(companyId, props, id); - if (buildResult.isFailure) { - return Result.fail(buildResult.error); - } - - const newInvoice = buildResult.data; - // 3) Ejecutar bajo transacción: verificar duplicado → persistir → ensamblar vista return this.transactionManager.complete(async (transaction: Transaction) => { try { + // 2) Generar nuevo nº de proforma + const nextNumberResult = await this.service.getNextProformaNumber(companyId, transaction); + if (nextNumberResult.isFailure) { + return Result.fail(nextNumberResult.error); + } + + const newProformaNumber = nextNumberResult.data; + + // 3) Construir entidad de dominio + const proformaProps = { + ...props, + invoiceNumber: Maybe.some(newProformaNumber), + }; + + const buildResult = this.service.buildProformaInCompany(companyId, proformaProps, id); + if (buildResult.isFailure) { + return Result.fail(buildResult.error); + } + + const newInvoice = buildResult.data; + const existsGuard = await this.ensureNotExists(companyId, id, transaction); if (existsGuard.isFailure) { return Result.fail(existsGuard.error); diff --git a/modules/customer-invoices/src/api/application/use-cases/issue-customer-invoice.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/issue-customer-invoice.use-case.ts index 360e6562..ed9174ba 100644 --- a/modules/customer-invoices/src/api/application/use-cases/issue-customer-invoice.use-case.ts +++ b/modules/customer-invoices/src/api/application/use-cases/issue-customer-invoice.use-case.ts @@ -1,15 +1,24 @@ -import { EntityNotFoundError, ITransactionManager } from "@erp/core/api"; -import { UniqueID } from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; -import { CustomerInvoiceNumber } from "../../domain"; +import { ITransactionManager } from "@erp/core/api"; +import { UniqueID, UtcDate } from "@repo/rdx-ddd"; +import { Maybe, Result } from "@repo/rdx-utils"; +import { InvalidProformaStatusError } from "../../domain"; +import { StatusInvoiceIsApprovedSpecification } from "../../domain/specs"; import { CustomerInvoiceApplicationService } from "../customer-invoice-application.service"; -import { StatusInvoiceIsApprovedSpecification } from "../specs"; type IssueCustomerInvoiceUseCaseInput = { companyId: UniqueID; - invoice_id: string; + proforma_id: string; }; +/** + * Caso de uso: Conversión de una proforma a 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 + */ export class IssueCustomerInvoiceUseCase { constructor( private readonly service: CustomerInvoiceApplicationService, @@ -17,56 +26,78 @@ export class IssueCustomerInvoiceUseCase { ) {} public execute(params: IssueCustomerInvoiceUseCaseInput) { - const { invoice_id, companyId } = params; + const { proforma_id, companyId } = params; - const idOrError = UniqueID.create(invoice_id); + const idOrError = UniqueID.create(proforma_id); if (idOrError.isFailure) { return Result.fail(idOrError.error); } - const invoiceId = idOrError.data; + const proformaId = idOrError.data; return this.transactionManager.complete(async (transaction) => { try { - const invoiceResult = await this.service.getInvoiceByIdInCompany( + /** 1. Recuperamos la proforma */ + const proformaResult = await this.service.getInvoiceByIdInCompany( companyId, - invoiceId, + proformaId, transaction ); - if (invoiceResult.isFailure) { - return Result.fail(invoiceResult.error); + if (proformaResult.isFailure) { + return Result.fail(proformaResult.error); } - const invoiceProforma = invoiceResult.data; + const proforma = proformaResult.data; - const isOk = new StatusInvoiceIsApprovedSpecification().isSatisfiedBy(invoiceProforma); - - if (!isOk) { - return Result.fail( - new EntityNotFoundError("Customer invoice", "id", invoiceId.toString()) - ); + /** 2. Comprobamos que la proforma origen está aprovada para generar la factura */ + const isApprovedSpec = new StatusInvoiceIsApprovedSpecification(); + if (!(await isApprovedSpec.isSatisfiedBy(proforma))) { + return Result.fail(new InvalidProformaStatusError(proformaId.toString())); } - // La factura se puede emitir. - // Pedir el número de factura - const newInvoiceNumber = CustomerInvoiceNumber.create("xxx/001").data; - - // Asignamos el número de la factura - - const issuedInvoiceResult = invoiceProforma.issueInvoice(newInvoiceNumber); - if (issuedInvoiceResult.isFailure) { - return Result.fail( - new EntityNotFoundError("Customer invoice", "id", issuedInvoiceResult.error) - ); + /** 3. Generar nueva factura */ + const nextNumberResult = await this.service.getNextIssueInvoiceNumber( + companyId, + proforma.series, + transaction + ); + if (nextNumberResult.isFailure) { + return Result.fail(nextNumberResult.error); } - const issuedInvoice = issuedInvoiceResult.data; + const newIssueNumber = nextNumberResult.data; - this.service.updateInvoiceInCompany(companyId, issuedInvoice, transaction); + // props base obtenidas del agregado proforma + const issuedInvoiceOrError = this.service.buildIssueInvoiceInCompany(companyId, proforma, { + invoiceNumber: Maybe.some(newIssueNumber), + invoiceDate: UtcDate.today(), + }); - //return await this.service.IssueInvoiceByIdInCompany(companyId, invoiceId, transaction); + if (issuedInvoiceOrError.isFailure) { + return Result.fail(issuedInvoiceOrError.error); + } + + const issuedInvoice = issuedInvoiceOrError.data; + + /** 4. Persistencia */ + await this.service.createInvoiceInCompany(companyId, issuedInvoice, transaction); + + // actualizamos la proforma + const updatedProformaResult = proforma.asIssued(); + if (updatedProformaResult.isFailure) { + return Result.fail(updatedProformaResult.error); + } + + await this.service.updateInvoiceInCompany( + companyId, + updatedProformaResult.data, + transaction + ); + + /** 5. Resultado */ + return Result.ok(issuedInvoice); } catch (error: unknown) { return Result.fail(error as Error); } diff --git a/modules/customer-invoices/src/api/application/use-cases/report/reporter/templates/customer-invoice/template.hbs b/modules/customer-invoices/src/api/application/use-cases/report/reporter/templates/customer-invoice/template.hbs index 5e476aae..a8a3b756 100644 --- a/modules/customer-invoices/src/api/application/use-cases/report/reporter/templates/customer-invoice/template.hbs +++ b/modules/customer-invoices/src/api/application/use-cases/report/reporter/templates/customer-invoice/template.hbs @@ -175,7 +175,7 @@ {{description}} {{#if quantity}}{{quantity}}{{else}} {{/if}} {{#if unit_amount}}{{unit_amount}}{{else}} {{/if}} - {{#if subtotal_amount}}{{subtotal_amount}}{{else}} {{/if}} + {{#if total_amount}}{{total_amount}}{{else}} {{/if}} {{/each}} diff --git a/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts b/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts index f6c8b610..ba5b2a4a 100644 --- a/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts +++ b/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts @@ -24,8 +24,10 @@ export interface CustomerInvoiceProps { isProforma: boolean; status: CustomerInvoiceStatus; + proformaId: Maybe; + series: Maybe; - invoiceNumber: Maybe; + invoiceNumber: CustomerInvoiceNumber; invoiceDate: UtcDate; operationDate: Maybe; @@ -70,7 +72,9 @@ export interface ICustomerInvoice { getTaxes(): InvoiceTaxTotal[]; - issueInvoice(newInvoiceNumber: CustomerInvoiceNumber): Result; + asIssued(): Result; + + getIssuedInvoiceProps(): CustomerInvoiceProps; } export class CustomerInvoice @@ -143,6 +147,10 @@ export class CustomerInvoice return this.props.isProforma; } + public get proformaId(): Maybe { + return this.props.proformaId; + } + public get status(): CustomerInvoiceStatus { return this.props.status; } @@ -301,15 +309,21 @@ export class CustomerInvoice }; } - public issueInvoice(newInvoiceNumber: CustomerInvoiceNumber) { - return CustomerInvoice.create( - { - ...this.props, - status: CustomerInvoiceStatus.createIssued(), - isProforma: false, - invoiceNumber: Maybe.some(newInvoiceNumber), - }, - this.id - ); + public asIssued(): Result { + const newProps: CustomerInvoiceProps = { + ...this.props, + status: CustomerInvoiceStatus.createIssued(), + }; + return CustomerInvoice.create(newProps, this.id); + } + + public getIssuedInvoiceProps(): CustomerInvoiceProps { + return { + ...this.props, + isProforma: false, + proformaId: Maybe.some(this.id), + status: CustomerInvoiceStatus.createIssued(), + invoiceDate: UtcDate.today(), + }; } } diff --git a/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.test.ts b/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.test.ts deleted file mode 100644 index 7698626d..00000000 --- a/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { CurrencyCode, LanguageCode, MoneyValue, Percentage, Quantity } from "@repo/rdx-ddd"; -import { CustomerInvoiceItemDescription } from "../../value-objects"; -import { CustomerInvoiceItem } from "./customer-invoice-item"; - -describe("CustomerInvoiceItem", () => { - it("debería calcular correctamente el subtotal (unitPrice * quantity)", () => { - const props = { - description: CustomerInvoiceItemDescription.create("Producto A"), - quantity: Quantity.create({ amount: 200, scale: 2 }), - unitPrice: MoneyValue.create(50), - discount: Percentage.create(0), - languageCode: LanguageCode.create("es"), - currencyCode: CurrencyCode.create("EUR"), - }; - - const result = CustomerInvoiceItem.create(props); - - expect(result.isOk()).toBe(true); - const customerInvoiceItem = result.unwrap(); - expect(customerInvoiceItem.subtotalPrice.value).toBe(100); // 50 * 2 - }); - - it("debería calcular correctamente el total con descuento", () => { - const props = { - description: new CustomerInvoiceItemDescription("Producto B"), - quantity: new Quantity(3), - unitPrice: new MoneyValue(30), - discount: new Percentage(10), // 10% - }; - - const result = CustomerInvoiceItem.create(props); - - expect(result.isOk()).toBe(true); - const customerInvoiceItem = result.unwrap(); - expect(customerInvoiceItem.totalPrice.value).toBe(81); // (30 * 3) - 10% de (30 * 3) - }); - - it("debería devolver los valores correctos de las propiedades", () => { - const props = { - description: new CustomerInvoiceItemDescription("Producto C"), - quantity: new Quantity(1), - unitPrice: new MoneyValue(100), - discount: new Percentage(5), - }; - - const result = CustomerInvoiceItem.create(props); - - expect(result.isOk()).toBe(true); - const customerInvoiceItem = result.unwrap(); - expect(customerInvoiceItem.description.value).toBe("Producto C"); - expect(customerInvoiceItem.quantity.value).toBe(1); - expect(customerInvoiceItem.unitPrice.value).toBe(100); - expect(customerInvoiceItem.discount.value).toBe(5); - }); - - it("debería manejar correctamente un descuento del 0%", () => { - const props = { - description: new CustomerInvoiceItemDescription("Producto D"), - quantity: new Quantity(4), - unitPrice: new MoneyValue(25), - discount: new Percentage(0), - }; - - const result = CustomerInvoiceItem.create(props); - - expect(result.isOk()).toBe(true); - const customerInvoiceItem = result.unwrap(); - expect(customerInvoiceItem.totalPrice.value).toBe(100); // 25 * 4 - }); - - it("debería manejar correctamente un descuento del 100%", () => { - const props = { - description: new CustomerInvoiceItemDescription("Producto E"), - quantity: new Quantity(2), - unitPrice: new MoneyValue(50), - discount: new Percentage(100), - }; - - const result = CustomerInvoiceItem.create(props); - - expect(result.isOk()).toBe(true); - const customerInvoiceItem = result.unwrap(); - expect(customerInvoiceItem.totalPrice.value).toBe(0); // (50 * 2) - 100% de (50 * 2) - }); -}); diff --git a/modules/customer-invoices/src/api/domain/errors/index.ts b/modules/customer-invoices/src/api/domain/errors/index.ts index b8c78e2f..45c0ddec 100644 --- a/modules/customer-invoices/src/api/domain/errors/index.ts +++ b/modules/customer-invoices/src/api/domain/errors/index.ts @@ -1 +1,2 @@ export * from "./customer-invoice-id-already-exits-error"; +export * from "./invalid-proforma-status-error"; diff --git a/modules/customer-invoices/src/api/domain/errors/invalid-proforma-status-error.ts b/modules/customer-invoices/src/api/domain/errors/invalid-proforma-status-error.ts new file mode 100644 index 00000000..970b4af8 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/errors/invalid-proforma-status-error.ts @@ -0,0 +1,11 @@ +import { DomainError } from "@repo/rdx-ddd"; + +export class InvalidProformaStatusError extends DomainError { + constructor(id: string, options?: ErrorOptions) { + super(`Error. Proforma with id '${id}' has invalid status.`, options); + this.name = "InvalidProformaStatusError"; + } +} + +export const isInvalidProformaStatusError = (e: unknown): e is InvalidProformaStatusError => + e instanceof InvalidProformaStatusError; diff --git a/modules/customer-invoices/src/api/domain/index.ts b/modules/customer-invoices/src/api/domain/index.ts index 62b47e36..06917160 100644 --- a/modules/customer-invoices/src/api/domain/index.ts +++ b/modules/customer-invoices/src/api/domain/index.ts @@ -2,4 +2,6 @@ export * from "./aggregates"; export * from "./entities"; export * from "./errors"; export * from "./repositories"; +export * from "./services"; +export * from "./specs"; export * from "./value-objects"; diff --git a/modules/customer-invoices/src/api/domain/services/customer-invoice-number-generator.interface.ts b/modules/customer-invoices/src/api/domain/services/customer-invoice-number-generator.interface.ts new file mode 100644 index 00000000..4808a802 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/services/customer-invoice-number-generator.interface.ts @@ -0,0 +1,21 @@ +import { UniqueID } from "@repo/rdx-ddd"; +import { Maybe, Result } from "@repo/rdx-utils"; +import { CustomerInvoiceNumber, CustomerInvoiceSerie } from "../value-objects"; + +/** + * Servicio de dominio que define cómo se genera el siguiente número de factura. + */ +export interface ICustomerInvoiceNumberGenerator { + /** + * Devuelve el siguiente número de factura disponible para una empresa dentro de una "serie" de factura. + * + * @param companyId - Identificador de la empresa + * @param serie - Serie por la que buscar la última factura + * @param transaction - Transacción activa + */ + nextForCompany( + companyId: UniqueID, + series: Maybe, + transaction: any + ): Promise>; +} diff --git a/modules/customer-invoices/src/api/domain/services/index.ts b/modules/customer-invoices/src/api/domain/services/index.ts new file mode 100644 index 00000000..e34a6999 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/services/index.ts @@ -0,0 +1 @@ +export * from "./customer-invoice-number-generator.interface"; diff --git a/modules/customer-invoices/src/api/domain/specs/index.ts b/modules/customer-invoices/src/api/domain/specs/index.ts new file mode 100644 index 00000000..37fb90f8 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/specs/index.ts @@ -0,0 +1 @@ +export * from "./status-invoice-is-approved.specification"; diff --git a/modules/customer-invoices/src/api/application/specs/status-invoice_is_approved.spec.ts b/modules/customer-invoices/src/api/domain/specs/status-invoice-is-approved.specification.ts similarity index 85% rename from modules/customer-invoices/src/api/application/specs/status-invoice_is_approved.spec.ts rename to modules/customer-invoices/src/api/domain/specs/status-invoice-is-approved.specification.ts index a33a3b7d..87189689 100644 --- a/modules/customer-invoices/src/api/application/specs/status-invoice_is_approved.spec.ts +++ b/modules/customer-invoices/src/api/domain/specs/status-invoice-is-approved.specification.ts @@ -1,5 +1,5 @@ import { CompositeSpecification } from "@repo/rdx-ddd"; -import { CustomerInvoice } from "../../domain"; +import { CustomerInvoice } from "../aggregates"; export class StatusInvoiceIsApprovedSpecification extends CompositeSpecification { public async isSatisfiedBy(invoice: CustomerInvoice): Promise { diff --git a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-number.ts b/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-number.ts index 917bf399..acb8f116 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-number.ts +++ b/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-number.ts @@ -7,7 +7,7 @@ interface ICustomerInvoiceNumberProps { } export class CustomerInvoiceNumber extends ValueObject { - private static readonly MAX_LENGTH = 255; + private static readonly MAX_LENGTH = 12; private static readonly FIELD = "invoiceNumber"; private static readonly ERROR_CODE = "INVALID_INVOICE_NUMBER"; diff --git a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-serie.ts b/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-serie.ts index c7e7dd08..6a0ae141 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-serie.ts +++ b/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-serie.ts @@ -7,7 +7,7 @@ interface ICustomerInvoiceSerieProps { } export class CustomerInvoiceSerie extends ValueObject { - private static readonly MAX_LENGTH = 255; + private static readonly MAX_LENGTH = 10; private static readonly FIELD = "invoiceSeries"; private static readonly ERROR_CODE = "INVALID_INVOICE_SERIE"; diff --git a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-status.ts b/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-status.ts index 13b9f208..5fa59b4d 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-status.ts +++ b/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-status.ts @@ -11,8 +11,9 @@ export enum INVOICE_STATUS { APPROVED = "approved", // <- Proforma REJECTED = "rejected", // <- Proforma - // issued <- (si is_proforma === true) => Es una proforma (histórica) - // issued <- (si is_proforma === false) => Factura y enviada a Veri*Factu + // status === issued <- (si is_proforma === true) => Es una proforma (histórica) + // status === issued <- (si is_proforma === false) => Factura y enviará/enviada a Veri*Factu + ISSUED = "issued", } export class CustomerInvoiceStatus extends ValueObject { diff --git a/modules/customer-invoices/src/api/infrastructure/dependencies.ts b/modules/customer-invoices/src/api/infrastructure/dependencies.ts index 8636fe96..1318aecc 100644 --- a/modules/customer-invoices/src/api/infrastructure/dependencies.ts +++ b/modules/customer-invoices/src/api/infrastructure/dependencies.ts @@ -1,35 +1,32 @@ // modules/invoice/infrastructure/invoice-dependencies.factory.ts +import { JsonTaxCatalogProvider, SpainTaxCatalogProvider } from "@erp/core"; import type { IMapperRegistry, IPresenterRegistry, ModuleParams } from "@erp/core/api"; - import { InMemoryMapperRegistry, InMemoryPresenterRegistry, SequelizeTransactionManager, } from "@erp/core/api"; - import { CreateCustomerInvoiceUseCase, + CustomerInvoiceApplicationService, CustomerInvoiceFullPresenter, CustomerInvoiceItemsFullPresenter, + CustomerInvoiceItemsReportPersenter, CustomerInvoiceReportHTMLPresenter, CustomerInvoiceReportPDFPresenter, CustomerInvoiceReportPresenter, GetCustomerInvoiceUseCase, + IssueCustomerInvoiceUseCase, ListCustomerInvoicesPresenter, ListCustomerInvoicesUseCase, RecipientInvoiceFullPresenter, ReportCustomerInvoiceUseCase, UpdateCustomerInvoiceUseCase, } from "../application"; - -import { JsonTaxCatalogProvider, SpainTaxCatalogProvider } from "@erp/core"; -import { - CustomerInvoiceApplicationService, - CustomerInvoiceItemsReportPersenter, -} from "../application"; import { CustomerInvoiceDomainMapper, CustomerInvoiceListMapper } from "./mappers"; import { CustomerInvoiceRepository } from "./sequelize"; +import { SequelizeInvoiceNumberGenerator } from "./services"; export type CustomerInvoiceDeps = { transactionManager: SequelizeTransactionManager; @@ -47,6 +44,7 @@ export type CustomerInvoiceDeps = { update: () => UpdateCustomerInvoiceUseCase; //delete: () => DeleteCustomerInvoiceUseCase; report: () => ReportCustomerInvoiceUseCase; + issue: () => IssueCustomerInvoiceUseCase; }; getService: (name: string) => any; listServices: () => string[]; @@ -73,7 +71,8 @@ export function buildCustomerInvoiceDependencies(params: ModuleParams): Customer // Repository & Services const repo = new CustomerInvoiceRepository({ mapperRegistry, database }); - const service = new CustomerInvoiceApplicationService(repo); + const numberGenerator = new SequelizeInvoiceNumberGenerator(); + const service = new CustomerInvoiceApplicationService(repo, numberGenerator); // Presenter Registry const presenterRegistry = new InMemoryPresenterRegistry(); @@ -162,6 +161,7 @@ export function buildCustomerInvoiceDependencies(params: ModuleParams): Customer // delete: () => new DeleteCustomerInvoiceUseCase(service, transactionManager), report: () => new ReportCustomerInvoiceUseCase(service, transactionManager, presenterRegistry), + issue: () => new IssueCustomerInvoiceUseCase(service, transactionManager), }, listServices, getService, diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/issue-customer-invoice.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/issue-customer-invoice.controller.ts index 9d7a96d3..6ba8cb9b 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/issue-customer-invoice.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/issue-customer-invoice.controller.ts @@ -1,21 +1,25 @@ -import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; +import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; import { IssueCustomerInvoiceUseCase } from "../../../application"; export class IssueCustomerInvoiceController extends ExpressController { - public constructor( - private readonly useCase: IssueCustomerInvoiceUseCase - /* private readonly presenter: any */ - ) { + public constructor(private readonly useCase: IssueCustomerInvoiceUseCase) { super(); // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); } - async executeImpl() { - const tenantId = this.getTenantId()!; // garantizado por tenantGuard - const { id } = this.req.params; + async executeImpl(): Promise { + const companyId = this.getTenantId(); // garantizado por tenantGuard + if (!companyId) { + return this.forbiddenError("Tenant ID not found"); + } - const result = await this.useCase.execute({ id, tenantId }); + const { proforma_id } = this.req.params; + if (!proforma_id) { + return this.invalidInputError("Proforma ID missing"); + } + + const result = await this.useCase.execute({ proforma_id, companyId }); return result.match( (data) => this.ok(data), diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/update-customer-invoice.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/update-customer-invoice.controller.ts index ad1f8d9d..6651cbb6 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/update-customer-invoice.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/update-customer-invoice.controller.ts @@ -1,4 +1,4 @@ -import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; +import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; import { UpdateCustomerInvoiceByIdRequestDTO } from "../../../../common/dto"; import { UpdateCustomerInvoiceUseCase } from "../../../application"; @@ -14,10 +14,15 @@ export class UpdateCustomerInvoiceController extends ExpressController { if (!companyId) { return this.forbiddenError("Tenant ID not found"); } - const { invoice_id } = this.req.params; + + const { proforma_id } = this.req.params; + if (!proforma_id) { + return this.invalidInputError("Proforma ID missing"); + } + const dto = this.req.body as UpdateCustomerInvoiceByIdRequestDTO; - const result = await this.useCase.execute({ invoice_id, companyId, dto }); + const result = await this.useCase.execute({ invoice_id: proforma_id, companyId, dto }); return result.match( (data) => this.ok(data), diff --git a/modules/customer-invoices/src/api/infrastructure/express/customer-invoices.routes.ts b/modules/customer-invoices/src/api/infrastructure/express/customer-invoices.routes.ts index 8543ff7a..d7c09420 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/customer-invoices.routes.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/customer-invoices.routes.ts @@ -19,6 +19,7 @@ import { ReportCustomerInvoiceController, UpdateCustomerInvoiceController, } from "./controllers"; +import { IssueCustomerInvoiceController } from "./controllers/issue-customer-invoice.controller"; export const customerInvoicesRouter = (params: ModuleParams) => { const { app, baseRoutePath, logger } = params as { @@ -84,7 +85,7 @@ export const customerInvoicesRouter = (params: ModuleParams) => { ); router.put( - "/:invoice_id", + "/:proforma_id", //checkTabContext, validateRequest(UpdateCustomerInvoiceByIdParamsRequestSchema, "params"), @@ -117,18 +118,21 @@ export const customerInvoicesRouter = (params: ModuleParams) => { const controller = new ReportCustomerInvoiceController(useCase); return controller.execute(req, res, next); } - ); /*router.put( - "/:invoice_id/issue", + ); + + router.put( + "/:proforma_id/issue", //checkTabContext, - validateRequest(XXX, "params"), - validateRequest(XXX, "body"), + + /*validateRequest(XXX, "params"), + validateRequest(XXX, "body"),*/ + (req: Request, res: Response, next: NextFunction) => { const useCase = deps.build.issue(); const controller = new IssueCustomerInvoiceController(useCase); return controller.execute(req, res, next); } -); -*/ + ); app.use(`${baseRoutePath}/customer-invoices`, router); }; 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 cb91f7ed..4c821fc9 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 @@ -70,6 +70,12 @@ export class CustomerInvoiceDomainMapper 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", @@ -83,7 +89,7 @@ export class CustomerInvoiceDomainMapper ); const invoiceNumber = extractOrPushError( - maybeFromNullableVO(source.invoice_number, (v) => CustomerInvoiceNumber.create(v)), + CustomerInvoiceNumber.create(source.invoice_number), "invoice_number", errors ); @@ -172,6 +178,7 @@ export class CustomerInvoiceDomainMapper companyId, customerId, isProforma, + proformaId, status, series, invoiceNumber, @@ -204,13 +211,13 @@ export class CustomerInvoiceDomainMapper ...params, }); - if (recipientResult.isFailure) { + /*if (recipientResult.isFailure) { errors.push({ path: "recipient", message: recipientResult.error.message, }); - } + }*/ // 3) Items (colección) const itemsResults = this._itemsMapper.mapToDomainCollection( @@ -223,12 +230,12 @@ export class CustomerInvoiceDomainMapper } ); - if (itemsResults.isFailure) { + /*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. @@ -254,6 +261,7 @@ export class CustomerInvoiceDomainMapper companyId: attributes.companyId!, isProforma: attributes.isProforma, + proformaId: attributes.proformaId!, status: attributes.status!, series: attributes.series!, invoiceNumber: attributes.invoiceNumber!, @@ -333,7 +341,7 @@ export class CustomerInvoiceDomainMapper const allAmounts = source.getAllAmounts(); // 4) Cliente - const recipient = this._mapRecipientToPersistence(source, { + const recipient = this._recipientMapper.mapToPersistence(source.recipient, { errors, parent: source, ...params, @@ -346,16 +354,17 @@ export class CustomerInvoiceDomainMapper ); } - const invoiceValues: CustomerInvoiceCreationAttributes = { + 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: toNullable(source.invoiceNumber, (v) => v.toPrimitive()), + invoice_number: source.invoiceNumber.toPrimitive(), invoice_date: source.invoiceDate.toPrimitive(), operation_date: toNullable(source.operationDate, (v) => v.toPrimitive()), @@ -397,47 +406,8 @@ export class CustomerInvoiceDomainMapper items, }; - return Result.ok(invoiceValues); - } - - protected _mapRecipientToPersistence(source: CustomerInvoice, params?: MapperParamsType) { - const { errors } = params as { - errors: ValidationErrorDetail[]; - }; - - const hasRecipient = source.hasRecipient; - const recipient = source.recipient?.getOrUndefined(); - - if (!source.isProforma && !hasRecipient) { - errors.push({ - path: "recipient", - message: "[CustomerInvoiceDomainMapper] Issued customer invoice w/o recipient data", - }); - } - - const recipientValues = { - customer_tin: !source.isProforma ? recipient?.tin.toPrimitive() : null, - customer_name: !source.isProforma ? recipient?.name.toPrimitive() : null, - customer_street: !source.isProforma - ? toNullable(recipient?.street, (v) => v.toPrimitive()) - : null, - customer_street2: !source.isProforma - ? toNullable(recipient?.street2, (v) => v.toPrimitive()) - : null, - customer_city: !source.isProforma - ? toNullable(recipient?.city, (v) => v.toPrimitive()) - : null, - customer_province: !source.isProforma - ? toNullable(recipient?.province, (v) => v.toPrimitive()) - : null, - customer_postal_code: !source.isProforma - ? toNullable(recipient?.postalCode, (v) => v.toPrimitive()) - : null, - customer_country: !source.isProforma - ? toNullable(recipient?.country, (v) => v.toPrimitive()) - : null, - }; - - return recipientValues; + return Result.ok( + invoiceValues as CustomerInvoiceCreationAttributes + ); } } 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..6f0c3858 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 @@ -1,20 +1,20 @@ +import { MapperParamsType } from "@erp/core/api"; import { City, Country, + extractOrPushError, + maybeFromNullableVO, Name, PostalCode, Province, Street, TINNumber, + toNullable, ValidationErrorCollection, ValidationErrorDetail, - extractOrPushError, - maybeFromNullableVO, } from "@repo/rdx-ddd"; - -import { MapperParamsType } from "@erp/core/api"; import { Maybe, Result } from "@repo/rdx-utils"; -import { CustomerInvoiceProps, InvoiceRecipient } from "../../../domain"; +import { CustomerInvoice, CustomerInvoiceProps, InvoiceRecipient } from "../../../domain"; import { CustomerInvoiceModel } from "../../sequelize"; export class InvoiceRecipientDomainMapper { @@ -114,4 +114,64 @@ export class InvoiceRecipientDomainMapper { return Result.ok(Maybe.some(createResult.data)); } + + /** + * Mapea los datos del destinatario (recipient) de una factura de cliente + * al formato esperado por la capa de persistencia. + * + * Reglas: + * - Si la factura es proforma (`isProforma === true`), todos los campos de recipient son `null`. + * - Si la factura no es proforma (`isProforma === false`), debe existir `recipient`. + * En caso contrario, se agrega un error de validación. + */ + mapToPersistence(source: Maybe, params?: MapperParamsType) { + const { errors, parent } = params as { + parent: CustomerInvoice; + errors: ValidationErrorDetail[]; + }; + + const { isProforma, hasRecipient } = parent; + + // Validación: facturas emitidas deben tener destinatario. + if (!isProforma && !hasRecipient) { + errors.push({ + path: "recipient", + message: "[CustomerInvoiceDomainMapper] Issued customer invoice without recipient data", + }); + } + + // Si hay errores previos, devolvemos fallo de validación inmediatamente. + if (errors.length > 0) { + return Result.fail( + new ValidationErrorCollection("Customer invoice mapping to persistence failed", errors) + ); + } + + // Si es proforma o no hay destinatario, todos los campos deben ser null. + if (isProforma || source.isNone()) { + return { + customer_tin: null, + customer_name: null, + customer_street: null, + customer_street2: null, + customer_city: null, + customer_province: null, + customer_postal_code: null, + customer_country: null, + }; + } + + const recipient = source.unwrap(); + + return { + customer_tin: recipient.tin.toPrimitive(), + customer_name: recipient.name.toPrimitive(), + customer_street: toNullable(recipient.street, (v) => v.toPrimitive()), + customer_street2: toNullable(recipient.street2, (v) => v.toPrimitive()), + customer_city: toNullable(recipient.city, (v) => v.toPrimitive()), + customer_province: toNullable(recipient.province, (v) => v.toPrimitive()), + customer_postal_code: toNullable(recipient.postalCode, (v) => v.toPrimitive()), + customer_country: toNullable(recipient.country, (v) => v.toPrimitive()), + }; + } } diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/queries/customer-invoice.list.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/queries/customer-invoice.list.mapper.ts index 433be285..9780d7e1 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/queries/customer-invoice.list.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/queries/customer-invoice.list.mapper.ts @@ -27,7 +27,7 @@ export type CustomerInvoiceListDTO = { companyId: UniqueID; isProforma: boolean; - invoiceNumber: Maybe; + invoiceNumber: CustomerInvoiceNumber; status: CustomerInvoiceStatus; series: Maybe; @@ -152,7 +152,7 @@ export class CustomerInvoiceListMapper ); const invoiceNumber = extractOrPushError( - maybeFromNullableVO(raw.invoice_number, (value) => CustomerInvoiceNumber.create(value)), + CustomerInvoiceNumber.create(raw.invoice_number), "invoice_number", errors ); diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice.model.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice.model.ts index d80f4ee0..e28f2928 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice.model.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice.model.ts @@ -45,7 +45,7 @@ export class CustomerInvoiceModel extends Model< declare proforma_id: CreationOptional; // ID de la proforma origen de la factura declare series: CreationOptional; - declare invoice_number: CreationOptional; + declare invoice_number: CreationOptional; declare invoice_date: CreationOptional; declare operation_date: CreationOptional; declare language_code: CreationOptional; @@ -183,7 +183,7 @@ export default (database: Sequelize) => { proforma_id: { type: DataTypes.UUID, allowNull: true, - defaultValue: true, + defaultValue: null, }, status: { @@ -193,13 +193,13 @@ export default (database: Sequelize) => { }, series: { - type: new DataTypes.STRING(), + type: new DataTypes.STRING(10), allowNull: true, defaultValue: null, }, invoice_number: { - type: new DataTypes.STRING(), + type: new DataTypes.STRING(12), allowNull: true, defaultValue: null, }, @@ -404,10 +404,22 @@ export default (database: Sequelize) => { name: "idx_invoices_company_date", fields: ["company_id", "deleted_at", { name: "invoice_date", order: "DESC" }], }, + + { + name: "idx_invoice_company_series_number", + fields: ["company_id", "series", "invoice_number"], + unique: true, + }, // <- para consulta get + { name: "idx_invoice_date", fields: ["invoice_date"] }, // <- para ordenación - { name: "idx_company_idx", fields: ["id", "company_id"], unique: true }, // <- para consulta get - { name: "idx_proforma_id", fields: ["proforma_id"], unique: false }, - { name: "idx_factuges", fields: ["factuges_id"], unique: false }, // <- para el proceso python + + { name: "idx_invoice_company_id", fields: ["id", "company_id"], unique: true }, // <- para consulta get + + { name: "idx_invoice_proforma_id", fields: ["proforma_id"], unique: false }, // <- para localizar factura por medio de proforma + + { name: "idx_invoice_factuges", fields: ["factuges_id"], unique: false }, // <- para el proceso python + + // Para búsquedas simples { name: "ft_customer_invoice", type: "FULLTEXT", diff --git a/modules/customer-invoices/src/api/infrastructure/services/index.ts b/modules/customer-invoices/src/api/infrastructure/services/index.ts new file mode 100644 index 00000000..04236857 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/services/index.ts @@ -0,0 +1 @@ +export * from "./sequelize-invoice-number-generator"; diff --git a/modules/customer-invoices/src/api/infrastructure/services/sequelize-invoice-number-generator.ts b/modules/customer-invoices/src/api/infrastructure/services/sequelize-invoice-number-generator.ts new file mode 100644 index 00000000..ad54dd25 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/services/sequelize-invoice-number-generator.ts @@ -0,0 +1,68 @@ +import { UniqueID } from "@repo/rdx-ddd"; +import { Maybe, Result } from "@repo/rdx-utils"; +import { literal, Transaction, WhereOptions } from "sequelize"; +import { + CustomerInvoiceNumber, + CustomerInvoiceSerie, + ICustomerInvoiceNumberGenerator, +} from "../../domain"; +import { CustomerInvoiceModel } from "../sequelize"; + +/** + * Generador de números de factura + */ +export class SequelizeInvoiceNumberGenerator implements ICustomerInvoiceNumberGenerator { + public async nextForCompany( + companyId: UniqueID, + series: Maybe, + transaction: Transaction + ): Promise> { + const where: WhereOptions = { + company_id: companyId.toString(), + is_proforma: false, + }; + + series.match( + (serieVO) => { + where.series = serieVO.toString(); + }, + () => { + where.series = null; + } + ); + + try { + const lastInvoice = await CustomerInvoiceModel.findOne({ + attributes: ["invoice_number"], + where, + // Orden numérico real: CAST(... AS UNSIGNED) + order: [literal("CAST(invoice_number AS UNSIGNED) DESC")], + transaction, + raw: true, + // Bloqueo opcional para evitar carreras si estás dentro de una TX + lock: transaction.LOCK.UPDATE, // requiere InnoDB y TX abierta + }); + + let nextValue = "00001"; // valor inicial por defecto + + if (lastInvoice) { + const current = Number(lastInvoice.invoice_number); + const next = Number.isFinite(current) && current > 0 ? current + 1 : 1; + nextValue = String(next).padStart(5, "0"); + } + + const numberResult = CustomerInvoiceNumber.create(nextValue); + if (numberResult.isFailure) { + return Result.fail(numberResult.error); + } + + return Result.ok(numberResult.data); + } catch (error) { + return Result.fail( + new Error( + `Error generating invoice number for company ${companyId}: ${(error as Error).message}` + ) + ); + } + } +} diff --git a/modules/customers/src/api/infrastructure/mappers/domain/customer.mapper.ts b/modules/customers/src/api/infrastructure/mappers/domain/customer.mapper.ts index e610b8fb..0332d7e1 100644 --- a/modules/customers/src/api/infrastructure/mappers/domain/customer.mapper.ts +++ b/modules/customers/src/api/infrastructure/mappers/domain/customer.mapper.ts @@ -4,25 +4,25 @@ import { Country, CurrencyCode, EmailAddress, + extractOrPushError, LanguageCode, + maybeFromNullableVO, Name, PhoneNumber, PostalAddress, PostalCode, Province, Street, - TINNumber, TaxCode, TextValue, - URLAddress, + TINNumber, + toNullable, UniqueID, + URLAddress, ValidationErrorCollection, ValidationErrorDetail, - extractOrPushError, - maybeFromNullableVO, - toNullable, } from "@repo/rdx-ddd"; -import { Collection, Result, isNullishOrEmpty } from "@repo/rdx-utils"; +import { Collection, isNullishOrEmpty, Result } from "@repo/rdx-utils"; import { Customer, CustomerProps, CustomerStatus } from "../../../domain"; import { CustomerCreationAttributes, CustomerModel } from "../../sequelize";