From 5f08cfaa15166376a91bdb8c1f69b9440db01c02 Mon Sep 17 00:00:00 2001 From: david Date: Wed, 12 Nov 2025 15:48:57 +0100 Subject: [PATCH] Facturas de cliente --- .../api/application/presenters/presenter.ts | 4 +- .../taxes/json-tax-catalog.provider.ts | 5 +- .../catalogs/taxes/tax-catalog.provider.ts | 5 +- .../customer-invoice-application.service.ts | 109 ++++++-- .../change-status-proforma.use-case.ts | 2 +- .../proformas/delete-proforma.use-case.ts | 23 +- .../proformas/issue-proforma.use-case.ts | 4 +- .../templates/customer-invoice/logo_acana.jpg | Bin 0 -> 25539 bytes .../{logo1.jpg => logo_rodax.jpg} | Bin .../templates/customer-invoice/template.hbs | 60 ++-- .../customer-invoice/template_rodax.hbs | 260 ++++++++++++++++++ .../proformas/update-proforma/index.ts | 2 +- ...se-case.ts => update-proforma.use-case.ts} | 6 +- .../src/api/domain/errors/index.ts | 1 + .../proforma-cannot-be-deleted-error.ts | 12 + .../customer-invoice-repository.interface.ts | 9 +- .../issue-customer-invoice-domain-service.ts | 10 +- ...roforma-customer-invoice-domain-service.ts | 1 + ...tomer-invoice-is-proforma.specification.ts | 3 +- .../src/api/domain/specs/index.ts | 1 + .../status-invoice-is-draft.specification.ts | 11 + .../src/api/infrastructure/dependencies.ts | 41 +-- .../proformas/delete-proforma.controller.ts | 8 +- .../proformas/update-proforma.controller.ts | 4 +- .../customer-invoices-api-error-mapper.ts | 13 +- .../express/proformas.routes.ts | 32 ++- .../sequelize/customer-invoice.repository.ts | 31 ++- .../delete-proforma-by-id.request.dto.ts | 6 +- .../src/common/dto/request/proformas/index.ts | 1 + .../issue-proforma-by-id.request.dto.ts | 5 + 30 files changed, 512 insertions(+), 157 deletions(-) create mode 100644 modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/logo_acana.jpg rename modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/{logo1.jpg => logo_rodax.jpg} (100%) create mode 100644 modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/template_rodax.hbs rename modules/customer-invoices/src/api/application/use-cases/proformas/update-proforma/{update-customer-invoice.use-case.ts => update-proforma.use-case.ts} (94%) create mode 100644 modules/customer-invoices/src/api/domain/errors/proforma-cannot-be-deleted-error.ts create mode 100644 modules/customer-invoices/src/api/domain/specs/status-invoice-is-draft.specification.ts create mode 100644 modules/customer-invoices/src/common/dto/request/proformas/issue-proforma-by-id.request.dto.ts diff --git a/modules/core/src/api/application/presenters/presenter.ts b/modules/core/src/api/application/presenters/presenter.ts index de570619..1c0abafe 100644 --- a/modules/core/src/api/application/presenters/presenter.ts +++ b/modules/core/src/api/application/presenters/presenter.ts @@ -1,5 +1,5 @@ -import { IPresenterRegistry } from "./presenter-registry.interface"; -import { IPresenter, IPresenterOutputParams } from "./presenter.interface"; +import type { IPresenter, IPresenterOutputParams } from "./presenter.interface"; +import type { IPresenterRegistry } from "./presenter-registry.interface"; export abstract class Presenter implements IPresenter diff --git a/modules/core/src/common/catalogs/taxes/json-tax-catalog.provider.ts b/modules/core/src/common/catalogs/taxes/json-tax-catalog.provider.ts index 6830da9f..ba561568 100644 --- a/modules/core/src/common/catalogs/taxes/json-tax-catalog.provider.ts +++ b/modules/core/src/common/catalogs/taxes/json-tax-catalog.provider.ts @@ -1,8 +1,9 @@ // --- Adaptador que carga el catálogo JSON en memoria e indexa por code --- import { Maybe } from "@repo/rdx-utils"; -import { TaxCatalogProvider } from "./tax-catalog.provider"; -import { TaxCatalogType, TaxItemType, TaxLookupItems } from "./tax-catalog-types"; + +import type { TaxCatalogProvider } from "./tax-catalog.provider"; +import type { TaxCatalogType, TaxItemType, TaxLookupItems } from "./tax-catalog-types"; export class JsonTaxCatalogProvider implements TaxCatalogProvider { // Índice por código normalizado diff --git a/modules/core/src/common/catalogs/taxes/tax-catalog.provider.ts b/modules/core/src/common/catalogs/taxes/tax-catalog.provider.ts index 902048e9..949c6fb5 100644 --- a/modules/core/src/common/catalogs/taxes/tax-catalog.provider.ts +++ b/modules/core/src/common/catalogs/taxes/tax-catalog.provider.ts @@ -1,7 +1,6 @@ -// --- Puerto (interfaz) para resolver tasas desde un catálogo --- +import type { Maybe } from "@repo/rdx-utils"; // Usa tu implementación real de Maybe -import { Maybe } from "@repo/rdx-utils"; // Usa tu implementación real de Maybe -import { TaxCatalogType, TaxItemType, TaxLookupItems } from "./tax-catalog-types"; +import type { TaxCatalogType, TaxItemType, TaxLookupItems } from "./tax-catalog-types"; export interface TaxCatalogProvider { /** diff --git a/modules/customer-invoices/src/api/application/services/customer-invoice-application.service.ts b/modules/customer-invoices/src/api/application/services/customer-invoice-application.service.ts index 81f334cc..4c972141 100644 --- a/modules/customer-invoices/src/api/application/services/customer-invoice-application.service.ts +++ b/modules/customer-invoices/src/api/application/services/customer-invoice-application.service.ts @@ -3,11 +3,14 @@ import type { UniqueID } from "@repo/rdx-ddd"; import { type Collection, Maybe, Result } from "@repo/rdx-utils"; import type { Transaction } from "sequelize"; -import type { - CustomerInvoiceNumber, - CustomerInvoiceSerie, - CustomerInvoiceStatus, - ICustomerInvoiceNumberGenerator, +import { + CustomerInvoiceIsProformaSpecification, + type CustomerInvoiceNumber, + type CustomerInvoiceSerie, + type CustomerInvoiceStatus, + type ICustomerInvoiceNumberGenerator, + ProformaCannotBeDeletedError, + StatusInvoiceIsDraftSpecification, } from "../../domain"; import { CustomerInvoice, @@ -69,6 +72,27 @@ export class CustomerInvoiceApplicationService { return CustomerInvoice.create({ ...props, companyId }, proformaId); } + /** + * Guarda una nueva factura y devuelve la factura guardada. + * + * @param companyId - Identificador de la empresa a la que pertenece la factura. + * @param proforma - La factura a guardar. + * @param transaction - Transacción activa para la operación. + * @returns Result - La factura guardada o un error si falla la operación. + */ + async createIssueInvoiceInCompany( + companyId: UniqueID, + invoice: CustomerInvoice, + transaction: Transaction + ): Promise> { + const result = await this.repository.create(invoice, transaction); + if (result.isFailure) { + return Result.fail(result.error); + } + + return this.getIssueInvoiceByIdInCompany(companyId, invoice.id, transaction); + } + /** * Guarda una nueva proforma y devuelve la proforma guardada. * @@ -77,7 +101,7 @@ export class CustomerInvoiceApplicationService { * @param transaction - Transacción activa para la operación. * @returns Result - La proforma guardada o un error si falla la operación. */ - async createInvoiceInCompany( + async createProformaInCompany( companyId: UniqueID, proforma: CustomerInvoice, transaction: Transaction @@ -113,7 +137,27 @@ export class CustomerInvoiceApplicationService { /** * - * Comprueba si existe o no en persistencia una factura con el ID proporcionado + * Comprueba si existe o no en persistencia una proforma con el ID proporcionado + * + * @param companyId - Identificador de la empresa a la que pertenece la factura. + * @param proformaId - Identificador UUID de la factura. + * @param transaction - Transacción activa para la operación. + * @returns Result - Existe la factura o no. + */ + + async existsProformaByIdInCompany( + companyId: UniqueID, + proformaId: UniqueID, + transaction?: Transaction + ): Promise> { + return this.repository.existsByIdInCompany(companyId, proformaId, transaction, { + is_proforma: true, + }); + } + + /** + * + * Comprueba si existe o no en persistencia una proforma con el ID proporcionado * * @param companyId - Identificador de la empresa a la que pertenece la factura. * @param invoiceId - Identificador UUID de la factura. @@ -121,12 +165,14 @@ export class CustomerInvoiceApplicationService { * @returns Result - Existe la factura o no. */ - async existsByIdInCompany( + async existsIssueInvoiceByIdInCompany( companyId: UniqueID, invoiceId: UniqueID, transaction?: Transaction ): Promise> { - return this.repository.existsByIdInCompany(companyId, invoiceId, transaction); + return this.repository.existsByIdInCompany(companyId, invoiceId, transaction, { + is_proforma: false, + }); } /** @@ -199,13 +245,13 @@ export class CustomerInvoiceApplicationService { changes: CustomerInvoicePatchProps, transaction?: Transaction ): Promise> { - const invoiceResult = await this.getProformaByIdInCompany(companyId, proformaId, transaction); + const proformaResult = await this.getProformaByIdInCompany(companyId, proformaId, transaction); - if (invoiceResult.isFailure) { - return Result.fail(invoiceResult.error); + if (proformaResult.isFailure) { + return Result.fail(proformaResult.error); } - const updated = invoiceResult.data.update(changes); + const updated = proformaResult.data.update(changes); if (updated.isFailure) { return Result.fail(updated.error); @@ -215,27 +261,48 @@ export class CustomerInvoiceApplicationService { } /** - * Elimina (o marca como eliminada) una factura según su ID. + * Elimina (o marca como eliminada) una proforma según su ID. * - * @param companyId - Identificador de la empresa a la que pertenece la factura. - * @param invoiceId - Identificador UUID de la factura. + * @param companyId - Identificador de la empresa a la que pertenece la proforma. + * @param proformaId - Identificador UUID de la proforma. * @param transaction - Transacción activa para la operación. * @returns Result - Resultado de la operación. */ - async deleteInvoiceByIdInCompany( + async deleteProformaByIdInCompany( companyId: UniqueID, - invoiceId: UniqueID, + proformaId: UniqueID, transaction?: Transaction ): Promise> { - return this.repository.deleteByIdInCompany(companyId, invoiceId, transaction); + // 1) Buscar la proforma + const proformaResult = await this.getProformaByIdInCompany(companyId, proformaId, transaction); + + if (proformaResult.isFailure) return Result.fail(proformaResult.error); + const proforma = proformaResult.data; + + // 2) Validar: es proforma + const isProforma = new CustomerInvoiceIsProformaSpecification(); + if (!(await isProforma.isSatisfiedBy(proforma))) { + return Result.fail(new ProformaCannotBeDeletedError(proformaId.toString(), "not a proforma")); + } + + // 3) Validar: estado draft + const isDraft = new StatusInvoiceIsDraftSpecification(); + if (!(await isDraft.isSatisfiedBy(proforma))) { + return Result.fail( + new ProformaCannotBeDeletedError(proformaId.toString(), "status is not 'draft'") + ); + } + + // 4) Borrar la proforma (baja lógica) + return this.repository.deleteProformaByIdInCompany(companyId, proformaId, transaction); } /** * * Actualiza el "status" de una proforma * - * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. - * @param proformaId - UUID de la factura a eliminar. + * @param companyId - Identificador UUID de la empresa a la que pertenece la proforma. + * @param proformaId - UUID de la proforma a actualizar. * @param newStatus - nuevo estado * @param transaction - Transacción activa para la operación. * @returns Result diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/change-status-proforma.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/proformas/change-status-proforma.use-case.ts index 22227546..f3229c39 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/change-status-proforma.use-case.ts +++ b/modules/customer-invoices/src/api/application/use-cases/proformas/change-status-proforma.use-case.ts @@ -37,7 +37,7 @@ export class ChangeStatusProformaUseCase { return this.transactionManager.complete(async (transaction) => { try { /** 1. Recuperamos la proforma */ - const proformaResult = await this.service.getInvoiceByIdInCompany( + const proformaResult = await this.service.getProformaByIdInCompany( companyId, proformaId, transaction diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/delete-proforma.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/proformas/delete-proforma.use-case.ts index 7b6b3f59..cfe4e17e 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/delete-proforma.use-case.ts +++ b/modules/customer-invoices/src/api/application/use-cases/proformas/delete-proforma.use-case.ts @@ -1,4 +1,4 @@ -import { EntityNotFoundError, type ITransactionManager } from "@erp/core/api"; +import type { ITransactionManager } from "@erp/core/api"; import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; @@ -19,32 +19,15 @@ export class DeleteProformaUseCase { const { proforma_id, companyId } = params; 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 existsCheck = await this.service.existsByIdInCompany( - companyId, - invoiceId, - transaction - ); - - if (existsCheck.isFailure) { - return Result.fail(existsCheck.error); - } - - const invoiceExists = existsCheck.data; - - if (!invoiceExists) { - return Result.fail(new EntityNotFoundError("Proforma", "id", invoiceId.toString())); - } - - return await this.service.deleteInvoiceByIdInCompany(companyId, invoiceId, transaction); + return await this.service.deleteProformaByIdInCompany(companyId, proformaId, transaction); } catch (error: unknown) { return Result.fail(error as Error); } diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/issue-proforma.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/proformas/issue-proforma.use-case.ts index 98e57f4d..9bb5780f 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/issue-proforma.use-case.ts +++ b/modules/customer-invoices/src/api/application/use-cases/proformas/issue-proforma.use-case.ts @@ -50,7 +50,7 @@ export class IssueProformaInvoiceUseCase { return this.transactionManager.complete(async (transaction) => { try { /** 1. Recuperamos la proforma */ - const proformaResult = await this.service.getInvoiceByIdInCompany( + const proformaResult = await this.service.getProformaByIdInCompany( companyId, proformaId, transaction @@ -74,7 +74,7 @@ export class IssueProformaInvoiceUseCase { if (issuedInvoiceOrError.isFailure) return Result.fail(issuedInvoiceOrError.error); /** 5. Guardar la nueva factura */ - const saveInvoiceResult = await this.service.createInvoiceInCompany( + const saveInvoiceResult = await this.service.createIssueInvoiceInCompany( companyId, issuedInvoiceOrError.data, transaction diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/logo_acana.jpg b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/logo_acana.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c21c8b296b9065a9df21b3b6affa22d8d15c67b1 GIT binary patch literal 25539 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 literal 0 HcmV?d00001 diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/logo1.jpg b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/logo_rodax.jpg similarity index 100% rename from modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/logo1.jpg rename to modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/logo_rodax.jpg diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/template.hbs b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/template.hbs index c4f9ff1f..63aad43e 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/template.hbs +++ b/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/templates/customer-invoice/template.hbs @@ -8,7 +8,7 @@ Factura F26200 + + + + +
+ +
+ +
+
+ +
+ +
+ +
+ + TOTAL: {{total_amount}} +
+
+
+ + + + + + + + + + + + + + {{#each items}} + + + + + + + + {{/each}} + +
ConceptoCantidadPrecio unidadImporte total
{{description}}{{#if quantity}}{{quantity}}{{else}} {{/if}}{{#if unit_amount}}{{unit_amount}}{{else}} {{/if}}{{#if taxable_amount}}{{taxable_amount}}{{else}} {{/if}}
+
+ +
+ +
+ {{#if payment_method}} +
+

Forma de pago: {{payment_method}}

+
+ {{else}} + + {{/if}} + {{#if notes}} +
+

Notas: {{notes}}

+
+ {{else}} + + {{/if}} + +
+ +
+ + + {{#if discount_percentage}} + + + + + + + + + + + {{else}} + + {{/if}} + + + + + + {{#each taxes}} + + + + + + {{/each}} + + + + + + +
Importe neto {{subtotal_amount}}
Descuento {{discount_percentage}} {{discount_amount.value}}
Base imponible {{taxable_amount}}
{{tax_name}} {{taxes_amount}}
+ Total factura +   + {{total_amount}}
+
+
+
+ + +
+ +
+ + + + \ No newline at end of file diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/update-proforma/index.ts b/modules/customer-invoices/src/api/application/use-cases/proformas/update-proforma/index.ts index 002aceac..5af0ad37 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/update-proforma/index.ts +++ b/modules/customer-invoices/src/api/application/use-cases/proformas/update-proforma/index.ts @@ -1 +1 @@ -export * from "./update-customer-invoice.use-case"; +export * from "./update-proforma.use-case"; diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/update-proforma/update-customer-invoice.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/proformas/update-proforma/update-proforma.use-case.ts similarity index 94% rename from modules/customer-invoices/src/api/application/use-cases/proformas/update-proforma/update-customer-invoice.use-case.ts rename to modules/customer-invoices/src/api/application/use-cases/proformas/update-proforma/update-proforma.use-case.ts index 077fe38b..6bd13536 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/update-proforma/update-customer-invoice.use-case.ts +++ b/modules/customer-invoices/src/api/application/use-cases/proformas/update-proforma/update-proforma.use-case.ts @@ -10,20 +10,20 @@ import type { CustomerInvoiceApplicationService } from "../../../services/custom import { mapDTOToUpdateCustomerInvoicePatchProps } from "./map-dto-to-update-customer-invoice-props"; -type UpdateCustomerInvoiceUseCaseInput = { +type UpdateProformaUseCaseInput = { companyId: UniqueID; proforma_id: string; dto: UpdateProformaByIdRequestDTO; }; -export class UpdateCustomerInvoiceUseCase { +export class UpdateProformaUseCase { constructor( private readonly service: CustomerInvoiceApplicationService, private readonly transactionManager: ITransactionManager, private readonly presenterRegistry: IPresenterRegistry ) {} - public execute(params: UpdateCustomerInvoiceUseCaseInput) { + public execute(params: UpdateProformaUseCaseInput) { const { companyId, proforma_id, dto } = params; const idOrError = UniqueID.create(proforma_id); diff --git a/modules/customer-invoices/src/api/domain/errors/index.ts b/modules/customer-invoices/src/api/domain/errors/index.ts index 14e406c5..5d141a19 100644 --- a/modules/customer-invoices/src/api/domain/errors/index.ts +++ b/modules/customer-invoices/src/api/domain/errors/index.ts @@ -2,3 +2,4 @@ export * from "./customer-invoice-id-already-exits-error"; export * from "./entity-is-not-proforma-error"; export * from "./invalid-proforma-transition-error"; export * from "./proforma-cannot-be-converted-to-invoice-error"; +export * from "./proforma-cannot-be-deleted-error"; diff --git a/modules/customer-invoices/src/api/domain/errors/proforma-cannot-be-deleted-error.ts b/modules/customer-invoices/src/api/domain/errors/proforma-cannot-be-deleted-error.ts new file mode 100644 index 00000000..4bbc3655 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/errors/proforma-cannot-be-deleted-error.ts @@ -0,0 +1,12 @@ +// domain/errors/proforma-cannot-be-deleted.error.ts +import { DomainError } from "@repo/rdx-ddd"; + +/** Proforma solo se puede borrar si está en 'draft' */ +export class ProformaCannotBeDeletedError extends DomainError { + constructor(id: string, reason?: string, options?: ErrorOptions) { + super(`Proforma '${id}' cannot be deleted${reason ? `: ${reason}` : ""}.`, options); + this.name = "ProformaCannotBeDeletedError"; + } +} +export const isProformaCannotBeDeletedError = (e: unknown): e is ProformaCannotBeDeletedError => + e instanceof ProformaCannotBeDeletedError; diff --git a/modules/customer-invoices/src/api/domain/repositories/customer-invoice-repository.interface.ts b/modules/customer-invoices/src/api/domain/repositories/customer-invoice-repository.interface.ts index e0887634..8462f5f1 100644 --- a/modules/customer-invoices/src/api/domain/repositories/customer-invoice-repository.interface.ts +++ b/modules/customer-invoices/src/api/domain/repositories/customer-invoice-repository.interface.ts @@ -36,7 +36,8 @@ export interface ICustomerInvoiceRepository { existsByIdInCompany( companyId: UniqueID, id: UniqueID, - transaction?: unknown + transaction: unknown, + options: unknown ): Promise>; /** @@ -71,14 +72,14 @@ export interface ICustomerInvoiceRepository { /** * - * Elimina o marca como eliminada una factura dentro de una empresa. + * Elimina o marca como eliminada una proforma dentro de una empresa. * * @param companyId - ID de la empresa. - * @param id - UUID de la factura a eliminar. + * @param id - UUID de la proforma a eliminar. * @param transaction - Transacción activa para la operación. * @returns Result */ - deleteByIdInCompany( + deleteProformaByIdInCompany( companyId: UniqueID, id: UniqueID, transaction: unknown diff --git a/modules/customer-invoices/src/api/domain/services/issue-customer-invoice-domain-service.ts b/modules/customer-invoices/src/api/domain/services/issue-customer-invoice-domain-service.ts index 0ce7f88c..7eaec3ea 100644 --- a/modules/customer-invoices/src/api/domain/services/issue-customer-invoice-domain-service.ts +++ b/modules/customer-invoices/src/api/domain/services/issue-customer-invoice-domain-service.ts @@ -1,12 +1,13 @@ -import { UtcDate } from "@repo/rdx-ddd"; +import type { UtcDate } from "@repo/rdx-ddd"; import { Maybe, Result } from "@repo/rdx-utils"; + import { CustomerInvoice } from "../aggregates"; import { EntityIsNotProformaError, ProformaCannotBeConvertedToInvoiceError } from "../errors"; import { CustomerInvoiceIsProformaSpecification, ProformaCanTranstionToIssuedSpecification, } from "../specs"; -import { CustomerInvoiceNumber, CustomerInvoiceStatus } from "../value-objects"; +import { type CustomerInvoiceNumber, CustomerInvoiceStatus } from "../value-objects"; /** * Servicio de dominio que encapsula la lógica de emisión de factura definitiva desde una proforma. @@ -15,11 +16,6 @@ export class IssueCustomerInvoiceDomainService { private readonly isProformaSpec = new CustomerInvoiceIsProformaSpecification(); private readonly isApprovedSpec = new ProformaCanTranstionToIssuedSpecification(); - public linkWithProforma( - invoice: CustomerInvoice, - proforma: CustomerInvoice - ): Result {} - /** * Convierte una proforma en factura definitiva. * diff --git a/modules/customer-invoices/src/api/domain/services/proforma-customer-invoice-domain-service.ts b/modules/customer-invoices/src/api/domain/services/proforma-customer-invoice-domain-service.ts index 9eb0a25b..a92927ca 100644 --- a/modules/customer-invoices/src/api/domain/services/proforma-customer-invoice-domain-service.ts +++ b/modules/customer-invoices/src/api/domain/services/proforma-customer-invoice-domain-service.ts @@ -1,4 +1,5 @@ import { Result } from "@repo/rdx-utils"; + import { CustomerInvoice } from "../aggregates"; import { EntityIsNotProformaError, InvalidProformaTransitionError } from "../errors"; import { CustomerInvoiceIsProformaSpecification } from "../specs"; diff --git a/modules/customer-invoices/src/api/domain/specs/customer-invoice-is-proforma.specification.ts b/modules/customer-invoices/src/api/domain/specs/customer-invoice-is-proforma.specification.ts index 640854b5..3c169d57 100644 --- a/modules/customer-invoices/src/api/domain/specs/customer-invoice-is-proforma.specification.ts +++ b/modules/customer-invoices/src/api/domain/specs/customer-invoice-is-proforma.specification.ts @@ -1,5 +1,6 @@ import { CompositeSpecification } from "@repo/rdx-ddd"; -import { CustomerInvoice } from "../aggregates"; + +import type { CustomerInvoice } from "../aggregates"; export class CustomerInvoiceIsProformaSpecification extends CompositeSpecification { public async isSatisfiedBy(proforma: CustomerInvoice): Promise { diff --git a/modules/customer-invoices/src/api/domain/specs/index.ts b/modules/customer-invoices/src/api/domain/specs/index.ts index 59eb6146..550b9ec5 100644 --- a/modules/customer-invoices/src/api/domain/specs/index.ts +++ b/modules/customer-invoices/src/api/domain/specs/index.ts @@ -1,2 +1,3 @@ export * from "./customer-invoice-is-proforma.specification"; export * from "./proforma-can-transtion-to-issued.specification"; +export * from "./status-invoice-is-draft.specification"; diff --git a/modules/customer-invoices/src/api/domain/specs/status-invoice-is-draft.specification.ts b/modules/customer-invoices/src/api/domain/specs/status-invoice-is-draft.specification.ts new file mode 100644 index 00000000..f2701af8 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/specs/status-invoice-is-draft.specification.ts @@ -0,0 +1,11 @@ +// domain/specifications/status-invoice-is-draft.specification.ts +import { CompositeSpecification } from "@repo/rdx-ddd"; + +import type { CustomerInvoice } from "../aggregates"; + +/** Verifica que el estado es borrador */ +export class StatusInvoiceIsDraftSpecification extends CompositeSpecification { + public async isSatisfiedBy(invoice: CustomerInvoice): Promise { + return invoice.status.isDraft(); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/dependencies.ts b/modules/customer-invoices/src/api/infrastructure/dependencies.ts index f11fdf3a..8dd35659 100644 --- a/modules/customer-invoices/src/api/infrastructure/dependencies.ts +++ b/modules/customer-invoices/src/api/infrastructure/dependencies.ts @@ -19,13 +19,14 @@ import { CustomerInvoiceReportPDFPresenter, CustomerInvoiceReportPresenter, CustomerInvoiceTaxesReportPresenter, + DeleteProformaUseCase, GetProformaUseCase, IssueProformaInvoiceUseCase, ListCustomerInvoicesPresenter, ListProformasUseCase, RecipientInvoiceFullPresenter, ReportProformaUseCase, - UpdateCustomerInvoiceUseCase, + UpdateProformaUseCase, } from "../application"; import { CustomerInvoiceDomainMapper, CustomerInvoiceListMapper } from "./mappers"; @@ -42,14 +43,14 @@ export type CustomerInvoiceDeps = { taxes: JsonTaxCatalogProvider; }; useCases: { - list: () => ListProformasUseCase; - get: () => GetProformaUseCase; - create: () => CreateProformaUseCase; - update: () => UpdateCustomerInvoiceUseCase; - //delete: () => DeleteCustomerInvoiceUseCase; - report: () => ReportProformaUseCase; - issue: () => IssueProformaInvoiceUseCase; - changeStatus: () => ChangeStatusProformaUseCase; + list_proformas: () => ListProformasUseCase; + get_proforma: () => GetProformaUseCase; + create_proforma: () => CreateProformaUseCase; + update_proforma: () => UpdateProformaUseCase; + delete_proforma: () => DeleteProformaUseCase; + report_proforma: () => ReportProformaUseCase; + issue_proforma: () => IssueProformaInvoiceUseCase; + changeStatus_proforma: () => ChangeStatusProformaUseCase; }; }; @@ -123,16 +124,20 @@ export function buildCustomerInvoiceDependencies(params: ModuleParams): Customer }, ]); - const useCases = { - list: () => new ListProformasUseCase(appService, transactionManager, presenterRegistry), - get: () => new GetProformaUseCase(appService, transactionManager, presenterRegistry), - create: () => + const useCases: CustomerInvoiceDeps["useCases"] = { + list_proformas: () => + new ListProformasUseCase(appService, transactionManager, presenterRegistry), + get_proforma: () => new GetProformaUseCase(appService, transactionManager, presenterRegistry), + create_proforma: () => new CreateProformaUseCase(appService, transactionManager, presenterRegistry, catalogs.taxes), - update: () => - new UpdateCustomerInvoiceUseCase(appService, transactionManager, presenterRegistry), - report: () => new ReportProformaUseCase(appService, transactionManager, presenterRegistry), - issue: () => new IssueProformaInvoiceUseCase(appService, transactionManager, presenterRegistry), - changeStatus: () => new ChangeStatusProformaUseCase(appService, transactionManager), + update_proforma: () => + new UpdateProformaUseCase(appService, transactionManager, presenterRegistry), + delete_proforma: () => new DeleteProformaUseCase(appService, transactionManager), + report_proforma: () => + new ReportProformaUseCase(appService, transactionManager, presenterRegistry), + issue_proforma: () => + new IssueProformaInvoiceUseCase(appService, transactionManager, presenterRegistry), + changeStatus_proforma: () => new ChangeStatusProformaUseCase(appService, transactionManager), }; return { diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/delete-proforma.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/delete-proforma.controller.ts index 5f345906..f92f3a0c 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/delete-proforma.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/delete-proforma.controller.ts @@ -4,10 +4,7 @@ import type { DeleteProformaUseCase } from "../../../../application"; import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper"; export class DeleteProformaController extends ExpressController { - public constructor( - private readonly useCase: DeleteProformaUseCase - /* private readonly presenter: any */ - ) { + public constructor(private readonly useCase: DeleteProformaUseCase) { super(); this.errorMapper = customerInvoicesApiErrorMapper; @@ -22,6 +19,9 @@ export class DeleteProformaController extends ExpressController { } const { proforma_id } = this.req.params; + if (!proforma_id) { + return this.invalidInputError("Proforma ID missing"); + } const result = await this.useCase.execute({ proforma_id, companyId }); diff --git a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/update-proforma.controller.ts b/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/update-proforma.controller.ts index 33985b29..eb25ea71 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/update-proforma.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/controllers/proformas/update-proforma.controller.ts @@ -1,11 +1,11 @@ import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; import type { UpdateProformaByIdRequestDTO } from "../../../../../common/dto"; -import type { UpdateCustomerInvoiceUseCase } from "../../../../application"; +import type { UpdateProformaUseCase } from "../../../../application"; import { customerInvoicesApiErrorMapper } from "../../customer-invoices-api-error-mapper"; export class UpdateProformaController extends ExpressController { - public constructor(private readonly useCase: UpdateCustomerInvoiceUseCase) { + public constructor(private readonly useCase: UpdateProformaUseCase) { super(); this.errorMapper = customerInvoicesApiErrorMapper; diff --git a/modules/customer-invoices/src/api/infrastructure/express/customer-invoices-api-error-mapper.ts b/modules/customer-invoices/src/api/infrastructure/express/customer-invoices-api-error-mapper.ts index 721175fd..718c1aa6 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/customer-invoices-api-error-mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/customer-invoices-api-error-mapper.ts @@ -15,6 +15,7 @@ import { isCustomerInvoiceIdAlreadyExistsError, isEntityIsNotProformaError, isProformaCannotBeConvertedToInvoiceError, + isProformaCannotBeDeletedError, } from "../../domain"; // Crea una regla específica (prioridad alta para sobreescribir mensajes) @@ -47,8 +48,18 @@ const proformaConversionRule: ErrorToApiRule = { ), }; +const proformaCannotBeDeletedRule: ErrorToApiRule = { + priority: 120, + matches: (e) => isProformaCannotBeDeletedError(e), + build: (e) => + new ValidationApiError( + (e as ProformaCannotBeConvertedToInvoiceError).message || "Proforma cannot be deleted." + ), +}; + // Cómo aplicarla: crea una nueva instancia del mapper con la regla extra export const customerInvoicesApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default() .register(invoiceDuplicateRule) .register(entityIsNotProformaError) - .register(proformaConversionRule); + .register(proformaConversionRule) + .register(proformaCannotBeDeletedRule); diff --git a/modules/customer-invoices/src/api/infrastructure/express/proformas.routes.ts b/modules/customer-invoices/src/api/infrastructure/express/proformas.routes.ts index 9a22d31f..add75c8f 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/proformas.routes.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/proformas.routes.ts @@ -4,7 +4,9 @@ import { ChangeStatusProformaByIdParamsRequestSchema, ChangeStatusProformaByIdRequestSchema, CreateProformaRequestSchema, + DeleteProformaByIdParamsRequestSchema, GetProformaByIdRequestSchema, + IssueProformaByIdParamsRequestSchema, ListProformasRequestSchema, ReportProformaByIdRequestSchema, UpdateProformaByIdParamsRequestSchema, @@ -19,12 +21,13 @@ import { buildCustomerInvoiceDependencies } from "../dependencies"; import { ChangeStatusProformaController, CreateProformaController, + DeleteProformaController, GetProformaController, + IssueProformaController, ListProformasController, ReportProformaController, UpdateProformaController, } from "./controllers/proformas"; -import { IssueProformaController } from "./controllers/proformas/issue-proforma.controller"; export const proformasRouter = (params: ModuleParams) => { const { app, baseRoutePath, logger } = params as { @@ -60,7 +63,7 @@ export const proformasRouter = (params: ModuleParams) => { //checkTabContext, validateRequest(ListProformasRequestSchema, "params"), async (req: Request, res: Response, next: NextFunction) => { - const useCase = deps.useCases.list(); + const useCase = deps.useCases.list_proformas(); const controller = new ListProformasController(useCase /*, deps.presenters.list */); return controller.execute(req, res, next); } @@ -71,7 +74,7 @@ export const proformasRouter = (params: ModuleParams) => { //checkTabContext, validateRequest(GetProformaByIdRequestSchema, "params"), (req: Request, res: Response, next: NextFunction) => { - const useCase = deps.useCases.get(); + const useCase = deps.useCases.get_proforma(); const controller = new GetProformaController(useCase); return controller.execute(req, res, next); } @@ -83,7 +86,7 @@ export const proformasRouter = (params: ModuleParams) => { validateRequest(CreateProformaRequestSchema, "body"), (req: Request, res: Response, next: NextFunction) => { - const useCase = deps.useCases.create(); + const useCase = deps.useCases.create_proforma(); const controller = new CreateProformaController(useCase); return controller.execute(req, res, next); } @@ -96,30 +99,30 @@ export const proformasRouter = (params: ModuleParams) => { validateRequest(UpdateProformaByIdParamsRequestSchema, "params"), validateRequest(UpdateProformaByIdRequestSchema, "body"), (req: Request, res: Response, next: NextFunction) => { - const useCase = deps.useCases.update(); + const useCase = deps.useCases.update_proforma(); const controller = new UpdateProformaController(useCase); return controller.execute(req, res, next); } ); - /*router.delete( + router.delete( "/:proforma_id", //checkTabContext, - validateRequest(DeleteCustomerInvoiceByIdRequestSchema, "params"), + validateRequest(DeleteProformaByIdParamsRequestSchema, "params"), (req: Request, res: Response, next: NextFunction) => { - const useCase = deps.useCases.delete(); - const controller = new DeleteCustomerInvoiceController(useCase); + const useCase = deps.useCases.delete_proforma(); + const controller = new DeleteProformaController(useCase); return controller.execute(req, res, next); } - );*/ + ); router.get( "/:proforma_id/report", //checkTabContext, validateRequest(ReportProformaByIdRequestSchema, "params"), (req: Request, res: Response, next: NextFunction) => { - const useCase = deps.useCases.report(); + const useCase = deps.useCases.report_proforma(); const controller = new ReportProformaController(useCase); return controller.execute(req, res, next); } @@ -133,7 +136,7 @@ export const proformasRouter = (params: ModuleParams) => { validateRequest(ChangeStatusProformaByIdRequestSchema, "body"), (req: Request, res: Response, next: NextFunction) => { - const useCase = deps.useCases.changeStatus(); + const useCase = deps.useCases.changeStatus_proforma(); const controller = new ChangeStatusProformaController(useCase); return controller.execute(req, res, next); } @@ -143,11 +146,10 @@ export const proformasRouter = (params: ModuleParams) => { "/:proforma_id/issue", //checkTabContext, - /*validateRequest(XXX, "params"), - validateRequest(XXX, "body"),*/ + validateRequest(IssueProformaByIdParamsRequestSchema, "params"), (req: Request, res: Response, next: NextFunction) => { - const useCase = deps.useCases.issue(); + const useCase = deps.useCases.issue_proforma(); const controller = new IssueProformaController(useCase); return controller.execute(req, res, next); } diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts index f3f1776e..9c4421ab 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts @@ -166,16 +166,23 @@ export class CustomerInvoiceRepository * @param companyId - Identificador UUID de la empresa a la que pertenece la factura. * @param id - Identificador UUID de la factura. * @param transaction - Transacción activa para la operación. + * @param options - Opciones adicionales para la consulta (Sequelize FindOptions) * @returns Result */ async existsByIdInCompany( companyId: UniqueID, id: UniqueID, - transaction?: Transaction + transaction: Transaction, + options: FindOptions> = {} ): Promise> { try { const count = await CustomerInvoiceModel.count({ - where: { id: id.toString(), company_id: companyId.toString() }, + ...options, + where: { + id: id.toString(), + company_id: companyId.toString(), + ...(options.where ?? {}), + }, transaction, }); return Result.ok(Boolean(count > 0)); @@ -191,7 +198,7 @@ export class CustomerInvoiceRepository * @param companyId - Identificador UUID de la empresa a la que pertenece la factura. * @param id - UUID de la factura. * @param transaction - Transacción activa para la operación. - * @params options - Opciones adicionales para la consulta (Sequelize FindOptions) + * @param options - Opciones adicionales para la consulta (Sequelize FindOptions) * @returns Result */ async getByIdInCompany( @@ -367,21 +374,25 @@ export class CustomerInvoiceRepository /** * - * Elimina o marca como eliminada una factura. + * Elimina o marca como eliminada una proforma dentro de una empresa. * - * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. - * @param id - UUID de la factura a eliminar. + * @param companyId - ID de la empresa. + * @param id - UUID de la proforma a eliminar. * @param transaction - Transacción activa para la operación. - * @returns Result + * @returns Result */ - async deleteByIdInCompany( + async deleteProformaByIdInCompany( companyId: UniqueID, id: UniqueID, transaction: Transaction ): Promise> { try { const deleted = await CustomerInvoiceModel.destroy({ - where: { id: id.toString(), company_id: companyId.toString() }, + where: { + id: id.toString(), + company_id: companyId.toString(), + is_proforma: true, + }, transaction, }); @@ -424,8 +435,6 @@ export class CustomerInvoiceRepository } ); - console.log(affected); - if (affected === 0) { return Result.fail( new InfrastructureRepositoryError( diff --git a/modules/customer-invoices/src/common/dto/request/proformas/delete-proforma-by-id.request.dto.ts b/modules/customer-invoices/src/common/dto/request/proformas/delete-proforma-by-id.request.dto.ts index eba27fec..445f1fa6 100644 --- a/modules/customer-invoices/src/common/dto/request/proformas/delete-proforma-by-id.request.dto.ts +++ b/modules/customer-invoices/src/common/dto/request/proformas/delete-proforma-by-id.request.dto.ts @@ -6,8 +6,8 @@ import { z } from "zod/v4"; * */ -export const DeleteProformaByIdRequestSchema = z.object({ - id: z.string(), +export const DeleteProformaByIdParamsRequestSchema = z.object({ + proforma_id: z.string(), }); -export type DeleteProformaByIdRequestDTO = z.infer; +export type DeleteProformaByIdRequestDTO = z.infer; diff --git a/modules/customer-invoices/src/common/dto/request/proformas/index.ts b/modules/customer-invoices/src/common/dto/request/proformas/index.ts index 626ccdf1..d39b3dda 100644 --- a/modules/customer-invoices/src/common/dto/request/proformas/index.ts +++ b/modules/customer-invoices/src/common/dto/request/proformas/index.ts @@ -2,6 +2,7 @@ export * from "./change-status-proforma-by-id.request.dto"; export * from "./create-proforma.request.dto"; export * from "./delete-proforma-by-id.request.dto"; export * from "./get-proforma-by-id.request.dto"; +export * from "./issue-proforma-by-id.request.dto"; export * from "./list-proformas.request.dto"; export * from "./report-proforma-by-id.request.dto"; export * from "./update-proforma-by-id.request.dto"; diff --git a/modules/customer-invoices/src/common/dto/request/proformas/issue-proforma-by-id.request.dto.ts b/modules/customer-invoices/src/common/dto/request/proformas/issue-proforma-by-id.request.dto.ts new file mode 100644 index 00000000..fb754316 --- /dev/null +++ b/modules/customer-invoices/src/common/dto/request/proformas/issue-proforma-by-id.request.dto.ts @@ -0,0 +1,5 @@ +import { z } from "zod/v4"; + +export const IssueProformaByIdParamsRequestSchema = z.object({ + proforma_id: z.string(), +});