From 5d5959810618d17b7eb96607c623ad7bfdd744f8 Mon Sep 17 00:00:00 2001 From: david Date: Sat, 28 Mar 2026 22:10:05 +0100 Subject: [PATCH] . --- .vscode/extensions.json | 3 +- .vscode/settings.json | 33 ++-- apps/server/src/lib/modules/module-loader.ts | 9 +- .../src/lib/modules/service-registry.ts | 12 +- .../helpers.bak/has-no-undefined-fields.ts | 50 ------ .../src/api/application/helpers.bak/index.ts | 1 - ...map-dto-to-customer-invoice-items-props.ts | 85 --------- .../map-dto-to-customer-invoice-props.ts | 98 ----------- .../application/issued-invoices/di/index.ts | 2 + .../di/issued-invoice-creator.di.ts | 18 ++ ...ma-to-issued-invoice-props-converter.di.ts | 8 + .../issued-invoices/services/index.ts | 5 +- .../services/issued-invoice-creator.ts | 64 +++++++ ...ed-invoice-document-generator.interface.ts | 4 +- ...ssued-invoice-document-metadata-factory.ts | 10 +- .../services/issued-invoice-finder.ts | 13 +- ...sued-invoice-number-generator.interface.ts | 19 ++ .../services/issued-invoice-number-service.ts | 27 --- ...ssued-invoice-public-services.interface.ts | 19 ++ .../services/issued-invoice-write-service.ts | 32 ---- ...proforma-to-issued-invoice-materializer.ts | 56 ------ ...forma-to-issued-invoice-props-converter.ts | 149 ++++++++++++++++ .../snapshot-builders/index.ts | 1 + ...ed-invoice-report-item-snapshot-builder.ts | 4 +- .../list-issued-invoices.use-case.ts | 3 +- .../src/api/application/proformas/di/index.ts | 1 + .../proformas/di/proforma-issuer.di.ts | 15 ++ .../proformas/di/proforma-use-cases.di.ts | 34 +++- .../mappers/create-proforma-input.mapper.ts | 2 +- .../application/proformas/services/index.ts | 1 + .../proformas/services/proforma-creator.ts | 10 +- .../proformas/services/proforma-finder.ts | 13 +- .../proformas/services/proforma-issuer.ts | 77 +++++--- .../proforma-number-generator.interface.ts | 4 +- .../proforma-public-services.interface.ts | 34 ++++ .../services/proforma-write-service.ts | 134 -------------- .../proforma-item-full-snapshot.interface.ts | 4 +- .../proforma-items-full-snapshot-builder.ts | 6 +- .../proforma-items-report-snapshot-builder.ts | 8 +- .../proforma-report-snapshot-builder.ts | 49 ++++-- .../proforma-report-snapshot.interface.ts | 10 +- .../create-proforma.use-case.ts | 3 +- .../use-cases/issue-proforma.use-case.ts | 108 ++++++------ .../use-cases/list-proformas.use-case.ts | 3 +- .../update-proforma.use-case.ts | 3 +- .../customer-invoice-application.service.ts | 29 ++- .../common/value-objects/invoice-amount.vo.ts | 8 +- .../common/value-objects/invoice-status.vo.ts | 22 +-- .../common/value-objects/item-amount.vo.ts | 8 +- .../item-discount-percentage.vo.ts | 19 -- .../aggregates/issued-invoice.aggregate.ts | 92 +++++++--- .../issued-invoice-item.entity.ts | 59 ++++++- .../domain/issued-invoices/errors/index.ts | 1 + .../issued-invoice-item-not-valid-error.ts | 32 ++++ .../src/api/domain/issued-invoices/index.ts | 1 + .../aggregates/proforma.aggregate.ts | 11 +- .../proforma-items.collection.ts | 7 + .../proforma-items-totals-calculator.ts | 26 +-- .../services/proforma-taxes-calculator.ts | 4 +- .../issue-customer-invoice-domain-service.ts | 2 +- modules/customer-invoices/src/api/index.ts | 37 +--- .../domain/customer-invoice-item.mapper.ts | 16 +- .../issued-invoices/di/index.ts | 1 + .../di/issued-invoice-number-generator.di.ts | 5 + .../di/issued-invoice-public-services.ts | 38 ++-- .../issued-invoices-api-error-mapper.ts | 49 +----- .../express/issued-invoices.routes.ts | 10 +- .../persistence/sequelize/index.ts | 1 + .../sequelize-issued-invoice-domain.mapper.ts | 23 +-- ...elize-issued-invoice-item-domain.mapper.ts | 63 +++---- ...-issued-invoice-recipient-domain.mapper.ts | 4 +- ...lize-issued-invoice-taxes-domain.mapper.ts | 16 +- ...equelize-verifactu-record-domain.mapper.ts | 4 +- .../persistence/sequelize/services/index.ts | 1 + ...issued-invoice-number-generator.service.ts | 66 +++++++ .../proformas/di/proforma-public-services.ts | 18 +- .../proformas/di/proformas.di.ts | 26 ++- .../proformas/express/controllers/index.ts | 2 +- .../controllers/issue-proforma.controller.ts | 2 +- .../mappers/create-proforma-request-mapper.ts | 5 +- .../proformas/express/proformas.routes.ts | 27 ++- .../sequelize-proforma-domain.mapper.ts | 11 +- .../sequelize-proforma-item-domain.mapper.ts | 18 +- ...uelize-proforma-recipient-domain.mapper.ts | 7 - .../get-issued-invoice-by-id.response.dto.ts | 4 +- .../get-proforma-by-id.response.dto.ts | 4 +- .../use-proforma-grid-columns.tsx | 16 +- .../ui/components/proforma-status-badge.tsx | 4 +- .../customer-application.service.ts | 165 ------------------ .../application/services/customer-creator.ts | 15 +- .../application/services/customer-finder.ts | 25 ++- .../customer-public-services.interface.ts | 22 +++ .../application/services/customer-updater.ts | 5 +- .../src/api/application/services/index.ts | 1 + .../specs/customer-not-exists.spec.ts | 3 +- .../use-cases/create-customer.use-case.ts | 3 +- .../use-cases/list-customers.use-case.ts | 3 +- .../update/update-customer.use-case.ts | 3 +- modules/customers/src/api/index.ts | 4 +- .../di/customer-public-services.ts | 37 ++-- ...ate-proforma-from-factuges-input.mapper.ts | 2 +- .../create-proforma-from-factuges.use-case.ts | 18 +- modules/factuges/src/api/index.ts | 13 +- .../src/api/infraestructure/di/factuges.di.ts | 7 +- .../express/factuges.routes.ts | 28 +-- 105 files changed, 1134 insertions(+), 1263 deletions(-) delete mode 100644 modules/customer-invoices/src/api/application/helpers.bak/has-no-undefined-fields.ts delete mode 100644 modules/customer-invoices/src/api/application/helpers.bak/index.ts delete mode 100644 modules/customer-invoices/src/api/application/helpers.bak/map-dto-to-customer-invoice-items-props.ts delete mode 100644 modules/customer-invoices/src/api/application/helpers.bak/map-dto-to-customer-invoice-props.ts create mode 100644 modules/customer-invoices/src/api/application/issued-invoices/di/issued-invoice-creator.di.ts create mode 100644 modules/customer-invoices/src/api/application/issued-invoices/di/proforma-to-issued-invoice-props-converter.di.ts create mode 100644 modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-creator.ts create mode 100644 modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-number-generator.interface.ts delete mode 100644 modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-number-service.ts create mode 100644 modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-public-services.interface.ts delete mode 100644 modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-write-service.ts delete mode 100644 modules/customer-invoices/src/api/application/issued-invoices/services/proforma-to-issued-invoice-materializer.ts create mode 100644 modules/customer-invoices/src/api/application/issued-invoices/services/proforma-to-issued-invoice-props-converter.ts create mode 100644 modules/customer-invoices/src/api/application/proformas/di/proforma-issuer.di.ts create mode 100644 modules/customer-invoices/src/api/application/proformas/services/proforma-public-services.interface.ts delete mode 100644 modules/customer-invoices/src/api/application/proformas/services/proforma-write-service.ts delete mode 100644 modules/customer-invoices/src/api/domain/common/value-objects/item-discount-percentage.vo.ts create mode 100644 modules/customer-invoices/src/api/domain/issued-invoices/errors/issued-invoice-item-not-valid-error.ts create mode 100644 modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoice-number-generator.di.ts create mode 100644 modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/services/index.ts create mode 100644 modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/services/sequelize-issued-invoice-number-generator.service.ts delete mode 100644 modules/customers/src/api/application/customer-application.service.ts create mode 100644 modules/customers/src/api/application/services/customer-public-services.interface.ts diff --git a/.vscode/extensions.json b/.vscode/extensions.json index a8582dec..9dc70574 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -5,6 +5,7 @@ "cweijan.vscode-mysql-client2", "ms-vscode.vscode-json", "formulahendry.auto-rename-tag", - "cweijan.dbclient-jdbc" + "cweijan.dbclient-jdbc", + "pkief.material-icon-theme" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index f2a548e3..7d656bb9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,20 +1,19 @@ { - "files.associations": { - "tsconfig.json": "jsonc", - "typescript-config/*.json": "jsonc", - "*.css": "tailwindcss" - }, + // Font Family + "editor.fontFamily": "'Fira Code', 'Cascadia Code', 'Consolas'", + + // Enable Font Ligatures + "editor.fontLigatures": true, // Javascript and TypeScript settings - "javascript.suggest.enabled": true, - "javascript.suggest.autoImports": true, - "javascript.preferences.importModuleSpecifier": "shortest", + "js/ts.suggest.enabled": true, + "js/ts.suggest.autoImports": true, + "js/ts.preferences.importModuleSpecifier": "shortest", - "typescript.suggest.enabled": true, - "typescript.suggest.completeFunctionCalls": true, - "typescript.suggest.includeAutomaticOptionalChainCompletions": true, - "typescript.suggestionActions.enabled": true, - "typescript.autoClosingTags": true, + "js/ts.suggest.completeFunctionCalls": true, + "js/ts.suggest.includeAutomaticOptionalChainCompletions": true, + "js/ts.suggestionActions.enabled": true, + "js/ts.autoClosingTags.enabled": true, "editor.quickSuggestions": { "strings": "on" @@ -54,11 +53,5 @@ // other vscode settings "[sql]": { "editor.defaultFormatter": "cweijan.vscode-mysql-client2" - }, // <- your root font size here - - "invisibleAiChartDetector.watermark.includeSpaceFamily": true, - "invisibleAiChartDetector.watermark.includeUnicodeCf": true, - "invisibleAiChartDetector.doubleBlankThreshold": 2, - "invisibleAiChartDetector.replace.format": "unicode", - "invisibleAiChartDetector.clean.replaceSpaceLikesToAscii": true + } // <- your root font size here } diff --git a/apps/server/src/lib/modules/module-loader.ts b/apps/server/src/lib/modules/module-loader.ts index 7433da5b..ad879692 100644 --- a/apps/server/src/lib/modules/module-loader.ts +++ b/apps/server/src/lib/modules/module-loader.ts @@ -154,7 +154,12 @@ async function setupModule(name: string, params: ModuleParams, stack: string[]) function makeGetService(moduleName: string, pkg: IModuleServer) { return (serviceName: string): T => { const [serviceModule] = serviceName.split(":"); - trackDependencyUse(moduleName, serviceModule); + + // No registrar dependencias para modulos + // que usan sus propios servicios. + if (serviceModule !== "self") { + trackDependencyUse(moduleName, serviceModule); + } // IMPORTANTE: devolver el valor return getServiceScoped(moduleName, pkg.dependencies ?? [], serviceName); @@ -213,6 +218,8 @@ function validateModuleDependencies() { const declared = new Set(pkg.dependencies ?? []); const used = usedDependenciesByModule.get(moduleName) ?? new Set(); + console.log(declared, used); + // ❌ usadas pero no declaradas const undeclaredUsed = [...used].filter((d) => !declared.has(d)); diff --git a/apps/server/src/lib/modules/service-registry.ts b/apps/server/src/lib/modules/service-registry.ts index 2a0c3f8d..c9987990 100644 --- a/apps/server/src/lib/modules/service-registry.ts +++ b/apps/server/src/lib/modules/service-registry.ts @@ -12,15 +12,25 @@ export function registerService(name: string, api: any) { /** * Recupera un servicio registrado bajo un "scope". + * * getService("customers:repository") * Debe declarar: dependencies: ["customers"] + * + * El "scope" puede ser "self" para recuperar + * los servicios propios registrados. + * + * getService("self:repository") */ export function getServiceScoped( requesterModule: string, allowedDeps: readonly string[], name: string ): T { - const [serviceModule] = name.split(":"); + const [serviceModule, ...key] = name.split(":"); + + if (serviceModule === "self") { + return getService(`${requesterModule}:${key.join(":")}`); + } if (!allowedDeps.includes(serviceModule)) { throw new Error( diff --git a/modules/customer-invoices/src/api/application/helpers.bak/has-no-undefined-fields.ts b/modules/customer-invoices/src/api/application/helpers.bak/has-no-undefined-fields.ts deleted file mode 100644 index 89984e11..00000000 --- a/modules/customer-invoices/src/api/application/helpers.bak/has-no-undefined-fields.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * - * @param obj - El objeto a evaluar. - * @template T - El tipo del objeto. - * @description Verifica si un objeto no tiene campos con valor undefined. - * - * Esta función recorre los valores del objeto y devuelve true si todos los valores son diferentes de undefined. - * Si al menos un valor es undefined, devuelve false. - * - * @example - * const obj = { a: 1, b: 'test', c: null }; - * console.log(hasNoUndefinedFields(obj)); // true - * - * const objWithUndefined = { a: 1, b: undefined, c: null }; - * console.log(hasNoUndefinedFields(objWithUndefined)); // false - * - * @template T - El tipo del objeto. - * @param obj - El objeto a evaluar. - * @returns true si el objeto no tiene campos undefined, false en caso contrario. - */ - -export function hasNoUndefinedFields>( - obj: T -): obj is { [K in keyof T]-?: Exclude } { - return Object.values(obj).every((value) => value !== undefined); -} - -/** - * - * @description Verifica si un objeto tiene campos con valor undefined. - * Esta función es el complemento de `hasNoUndefinedFields`. - * - * @example - * const obj = { a: 1, b: 'test', c: null }; - * console.log(hasUndefinedFields(obj)); // false - * - * const objWithUndefined = { a: 1, b: undefined, c: null }; - * console.log(hasUndefinedFields(objWithUndefined)); // true - * - * @template T - El tipo del objeto. - * @param obj - El objeto a evaluar. - * @returns true si el objeto tiene al menos un campo undefined, false en caso contrario. - * - */ - -export function hasUndefinedFields>( - obj: T -): obj is { [K in keyof T]-?: Exclude } { - return !hasNoUndefinedFields(obj); -} diff --git a/modules/customer-invoices/src/api/application/helpers.bak/index.ts b/modules/customer-invoices/src/api/application/helpers.bak/index.ts deleted file mode 100644 index 8b137891..00000000 --- a/modules/customer-invoices/src/api/application/helpers.bak/index.ts +++ /dev/null @@ -1 +0,0 @@ - diff --git a/modules/customer-invoices/src/api/application/helpers.bak/map-dto-to-customer-invoice-items-props.ts b/modules/customer-invoices/src/api/application/helpers.bak/map-dto-to-customer-invoice-items-props.ts deleted file mode 100644 index b25f3344..00000000 --- a/modules/customer-invoices/src/api/application/helpers.bak/map-dto-to-customer-invoice-items-props.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { - ValidationErrorCollection, - type ValidationErrorDetail, - extractOrPushError, - maybeFromNullableResult, -} from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; - -import type { CreateCustomerInvoiceRequestDTO } from "../../../common"; -import { - IssuedInvoiceItem, - type IssuedInvoiceItemProps, - ItemAmount, - ItemDescription, - ItemDiscountPercentage, - ItemQuantity, -} from "../../domain"; - -import { hasNoUndefinedFields } from "./has-no-undefined-fields"; - -export function mapDTOToCustomerInvoiceItemsProps( - dtoItems: Pick["items"] -): Result { - const errors: ValidationErrorDetail[] = []; - const items: IssuedInvoiceItem[] = []; - - dtoItems.forEach((item, index) => { - const path = (field: string) => `items[${index}].${field}`; - - const description = extractOrPushError( - maybeFromNullableResult(item.description, (value) => ItemDescription.create(value)), - path("description"), - errors - ); - - const quantity = extractOrPushError( - maybeFromNullableResult(item.quantity, (value) => ItemQuantity.create({ value })), - path("quantity"), - errors - ); - - const unitAmount = extractOrPushError( - maybeFromNullableResult(item.unit_amount, (value) => ItemAmount.create({ value })), - path("unit_amount"), - errors - ); - - const discountPercentage = extractOrPushError( - maybeFromNullableResult(item.discount_percentage, (value) => - ItemDiscountPercentage.create({ value }) - ), - path("discount_percentage"), - errors - ); - - if (errors.length === 0) { - const itemProps: IssuedInvoiceItemProps = { - description: description, - quantity: quantity, - unitAmount: unitAmount, - itemDiscountPercentage: discountPercentage, - //currencyCode, - //languageCode, - //taxes: - }; - - if (hasNoUndefinedFields(itemProps)) { - // Validar y crear el item de factura - const itemOrError = IssuedInvoiceItem.create(itemProps); - - if (itemOrError.isSuccess) { - items.push(itemOrError.data); - } else { - errors.push({ path: `items[${index}]`, message: itemOrError.error.message }); - } - } - } - - if (errors.length > 0) { - return Result.fail(new ValidationErrorCollection("Invoice items dto mapping failed", errors)); - } - }); - - return Result.ok(items); -} diff --git a/modules/customer-invoices/src/api/application/helpers.bak/map-dto-to-customer-invoice-props.ts b/modules/customer-invoices/src/api/application/helpers.bak/map-dto-to-customer-invoice-props.ts deleted file mode 100644 index 065e264c..00000000 --- a/modules/customer-invoices/src/api/application/helpers.bak/map-dto-to-customer-invoice-props.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { - CurrencyCode, - UniqueID, - UtcDate, - ValidationErrorCollection, - type ValidationErrorDetail, - extractOrPushError, - maybeFromNullableResult, -} from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; - -import type { CreateCustomerInvoiceRequestDTO } from "../../../common"; -import { - type IProformaCreateProps, - InvoiceNumber, - InvoiceSerie, - InvoiceStatus, -} from "../../domain"; - -import { mapDTOToCustomerInvoiceItemsProps } from "./map-dto-to-customer-invoice-items-props"; - -/** - * Convierte el DTO a las props validadas (CustomerInvoiceProps). - * No construye directamente el agregado. - * - * @param dto - DTO con los datos de la factura de cliente - * @returns - - * - */ - -export function mapDTOToCustomerInvoiceProps(dto: CreateCustomerInvoiceRequestDTO) { - const errors: ValidationErrorDetail[] = []; - - const invoiceId = extractOrPushError(UniqueID.create(dto.id), "id", errors); - - const invoiceNumber = extractOrPushError( - maybeFromNullableResult(dto.invoice_number, (value) => InvoiceNumber.create(value)), - "invoice_number", - errors - ); - const invoiceSeries = extractOrPushError( - maybeFromNullableResult(dto.series, (value) => InvoiceSerie.create(value)), - "invoice_series", - errors - ); - const invoiceDate = extractOrPushError( - UtcDate.createFromISO(dto.invoice_date), - "invoice_date", - errors - ); - const operationDate = extractOrPushError( - maybeFromNullableResult(dto.operation_date, (value) => UtcDate.createFromISO(value)), - "operation_date", - errors - ); - - const currencyCode = extractOrPushError( - CurrencyCode.create(dto.currency_code), - "currency", - errors - ); - - // 🔄 Validar y construir los items de factura con helper especializado - const itemsResult = mapDTOToCustomerInvoiceItemsProps(dto.items); - if (itemsResult.isFailure) { - return Result.fail(itemsResult.error); - } - - if (errors.length > 0) { - return Result.fail(new ValidationErrorCollection("Invoice dto mapping failed", errors)); - } - - const invoiceProps: IProformaCreateProps = { - invoiceNumber: invoiceNumber!, - series: invoiceSeries!, - invoiceDate: invoiceDate!, - operationDate: operationDate!, - status: InvoiceStatus.fromDraft(), - currencyCode: currencyCode!, - }; - - return Result.ok({ id: invoiceId!, props: invoiceProps }); - - /*if (hasNoUndefinedFields(invoiceProps)) { - const invoiceOrError = CustomerInvoice.create(invoiceProps, invoiceId); - if (invoiceOrError.isFailure) { - return Result.fail(invoiceOrError.error); - } - return Result.ok(invoiceOrError.data); - } - - return Result.fail( - new ValidationErrorCollection([ - { path: "", message: "Error building from DTO: Some fields are undefined" }, - ]) - );*/ -} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/di/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/di/index.ts index 2fc0c886..74040a81 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/di/index.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/di/index.ts @@ -1,3 +1,5 @@ +export * from "./issued-invoice-creator.di"; export * from "./issued-invoice-finder.di"; export * from "./issued-invoice-snapshot-builders.di"; export * from "./issued-invoice-use-cases.di"; +export * from "./proforma-to-issued-invoice-props-converter.di"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/di/issued-invoice-creator.di.ts b/modules/customer-invoices/src/api/application/issued-invoices/di/issued-invoice-creator.di.ts new file mode 100644 index 00000000..5998c9fd --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/di/issued-invoice-creator.di.ts @@ -0,0 +1,18 @@ +import type { IIssuedInvoiceRepository } from "../repositories"; +import { + type IIssuedInvoiceCreator, + type IIssuedInvoiceNumberGenerator, + IssuedInvoiceCreator, +} from "../services"; + +export function buildIssuedInvoiceCreator(params: { + numberService: IIssuedInvoiceNumberGenerator; + repository: IIssuedInvoiceRepository; +}): IIssuedInvoiceCreator { + const { numberService, repository } = params; + + return new IssuedInvoiceCreator({ + repository, + numberService, + }); +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/di/proforma-to-issued-invoice-props-converter.di.ts b/modules/customer-invoices/src/api/application/issued-invoices/di/proforma-to-issued-invoice-props-converter.di.ts new file mode 100644 index 00000000..021e5fb2 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/di/proforma-to-issued-invoice-props-converter.di.ts @@ -0,0 +1,8 @@ +import { + type IProformaToIssuedInvoiceConverter, + ProformaToIssuedInvoiceConverter, +} from "../services"; + +export function buildProformaToIssuedInvoicePropsConverter(): IProformaToIssuedInvoiceConverter { + return new ProformaToIssuedInvoiceConverter(); +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/services/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/services/index.ts index 4559ef72..77015e51 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/services/index.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/services/index.ts @@ -1,5 +1,8 @@ +export * from "./issued-invoice-creator"; export * from "./issued-invoice-document-generator.interface"; export * from "./issued-invoice-document-metadata-factory"; export * from "./issued-invoice-document-properties-factory"; export * from "./issued-invoice-finder"; -export * from "./proforma-to-issued-invoice-materializer"; +export * from "./issued-invoice-number-generator.interface"; +export * from "./issued-invoice-public-services.interface"; +export * from "./proforma-to-issued-invoice-props-converter"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-creator.ts b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-creator.ts new file mode 100644 index 00000000..cc8e7e14 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-creator.ts @@ -0,0 +1,64 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import { type IIssuedInvoiceCreateProps, IssuedInvoice } from "../../../domain"; +import type { IIssuedInvoiceRepository } from "../repositories"; + +import type { IIssuedInvoiceNumberGenerator } from "./issued-invoice-number-generator.interface"; + +export interface IIssuedInvoiceCreatorParams { + companyId: UniqueID; + id: UniqueID; + props: Omit; + transaction: unknown; +} + +export type IIssuedInvoiceCreator = { + create(params: IIssuedInvoiceCreatorParams): Promise>; +}; + +type IssuedInvoiceCreatorDeps = { + numberService: IIssuedInvoiceNumberGenerator; + repository: IIssuedInvoiceRepository; +}; + +export class IssuedInvoiceCreator implements IIssuedInvoiceCreator { + private readonly numberService: IIssuedInvoiceNumberGenerator; + private readonly repository: IIssuedInvoiceRepository; + + constructor(deps: IssuedInvoiceCreatorDeps) { + this.numberService = deps.numberService; + this.repository = deps.repository; + } + + async create(params: IIssuedInvoiceCreatorParams): Promise> { + const { companyId, id, props, transaction } = params; + + // 1. Obtener siguiente número + const { series } = props; + const numberResult = await this.numberService.getNextForCompany(companyId, series, transaction); + + if (numberResult.isFailure) { + return Result.fail(numberResult.error); + } + + const invoiceNumber = numberResult.data; + + const invoiceResult = IssuedInvoice.create({ ...props, invoiceNumber, companyId }, id); + + if (invoiceResult.isFailure) { + return Result.fail(invoiceResult.error); + } + + const invoice = invoiceResult.data; + + // 3. Persistir + const saveResult = await this.repository.create(invoice, transaction); + + if (saveResult.isFailure) { + return Result.fail(saveResult.error); + } + + return Result.ok(invoice); + } +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-document-generator.interface.ts b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-document-generator.interface.ts index 8f08c463..c64fe1f0 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-document-generator.interface.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-document-generator.interface.ts @@ -1,6 +1,6 @@ import type { DocumentGenerationService } from "@erp/core/api"; -import type { IssuedInvoiceReportSnapshot } from "../application-models"; +import type { IIssuedInvoiceReportSnapshot } from "../snapshot-builders/report"; export interface IssuedInvoiceDocumentGeneratorService - extends DocumentGenerationService {} + extends DocumentGenerationService {} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-document-metadata-factory.ts b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-document-metadata-factory.ts index 9f29f27b..95c4da08 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-document-metadata-factory.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-document-metadata-factory.ts @@ -1,6 +1,6 @@ import type { IDocumentMetadata, IDocumentMetadataFactory } from "@erp/core/api"; -import type { IssuedInvoiceReportSnapshot } from "../application-models"; +import type { IIssuedInvoiceReportSnapshot } from "../snapshot-builders"; /** * Construye los metadatos del documento PDF de una factura emitida. @@ -10,9 +10,9 @@ import type { IssuedInvoiceReportSnapshot } from "../application-models"; * - Sin IO */ export class IssuedInvoiceDocumentMetadataFactory - implements IDocumentMetadataFactory + implements IDocumentMetadataFactory { - build(snapshot: IssuedInvoiceReportSnapshot): IDocumentMetadata { + build(snapshot: IIssuedInvoiceReportSnapshot): IDocumentMetadata { if (!snapshot.id) { throw new Error("IssuedInvoiceReportSnapshot.id is required"); } @@ -33,12 +33,12 @@ export class IssuedInvoiceDocumentMetadataFactory }; } - private buildFilename(snapshot: IssuedInvoiceReportSnapshot): string { + private buildFilename(snapshot: IIssuedInvoiceReportSnapshot): string { // Ejemplo: factura-F2024-000123-FULANITO.pdf return `factura-${snapshot.series}${snapshot.invoice_number}-${snapshot.recipient.name}.pdf`; } - private buildCacheKey(snapshot: IssuedInvoiceReportSnapshot): string { + private buildCacheKey(snapshot: IIssuedInvoiceReportSnapshot): string { // Versionado explícito para invalidaciones futuras return [ "issued-invoice", diff --git a/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-finder.ts b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-finder.ts index ecff558a..9910df65 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-finder.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-finder.ts @@ -1,7 +1,6 @@ import type { Criteria } from "@repo/rdx-criteria/server"; import type { UniqueID } from "@repo/rdx-ddd"; import type { Collection, Result } from "@repo/rdx-utils"; -import type { Transaction } from "sequelize"; import type { IssuedInvoice } from "../../../domain"; import type { IssuedInvoiceSummary } from "../models"; @@ -11,19 +10,19 @@ export interface IIssuedInvoiceFinder { findIssuedInvoiceById( companyId: UniqueID, invoiceId: UniqueID, - transaction?: Transaction + transaction?: unknown ): Promise>; issuedInvoiceExists( companyId: UniqueID, invoiceId: UniqueID, - transaction?: Transaction + transaction?: unknown ): Promise>; findIssuedInvoicesByCriteria( companyId: UniqueID, criteria: Criteria, - transaction?: Transaction + transaction?: unknown ): Promise, Error>>; } @@ -33,7 +32,7 @@ export class IssuedInvoiceFinder implements IIssuedInvoiceFinder { async findIssuedInvoiceById( companyId: UniqueID, invoiceId: UniqueID, - transaction?: Transaction + transaction?: unknown ): Promise> { return this.repository.getByIdInCompany(companyId, invoiceId, transaction); } @@ -41,7 +40,7 @@ export class IssuedInvoiceFinder implements IIssuedInvoiceFinder { async issuedInvoiceExists( companyId: UniqueID, invoiceId: UniqueID, - transaction?: Transaction + transaction?: unknown ): Promise> { return this.repository.existsByIdInCompany(companyId, invoiceId, transaction); } @@ -49,7 +48,7 @@ export class IssuedInvoiceFinder implements IIssuedInvoiceFinder { async findIssuedInvoicesByCriteria( companyId: UniqueID, criteria: Criteria, - transaction?: Transaction + transaction?: unknown ): Promise, Error>> { return this.repository.findByCriteriaInCompany(companyId, criteria, transaction); } diff --git a/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-number-generator.interface.ts b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-number-generator.interface.ts new file mode 100644 index 00000000..d2deb316 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-number-generator.interface.ts @@ -0,0 +1,19 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import type { Maybe, Result } from "@repo/rdx-utils"; + +import type { InvoiceNumber, InvoiceSerie } from "../../../domain"; + +export interface IIssuedInvoiceNumberGenerator { + /** + * 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 + */ + getNextForCompany( + companyId: UniqueID, + series: Maybe, + transaction: unknown + ): Promise>; +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-number-service.ts b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-number-service.ts deleted file mode 100644 index 0a6ecd77..00000000 --- a/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-number-service.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { UniqueID } from "@repo/rdx-ddd"; -import type { Maybe, Result } from "@repo/rdx-utils"; - -import type { ICustomerInvoiceNumberGenerator, InvoiceNumber, InvoiceSerie } from "../../../domain"; - -export interface IIssuedInvoiceNumberService { - /** - * Devuelve el siguiente número disponible para una factura emitida. - */ - nextIssuedInvoiceNumber( - companyId: UniqueID, - series: Maybe, - transaction: unknown - ): Promise>; -} - -export class IssuedInvoiceNumberService implements IIssuedInvoiceNumberService { - constructor(private readonly numberGenerator: ICustomerInvoiceNumberGenerator) {} - - async nextIssuedInvoiceNumber( - companyId: UniqueID, - series: Maybe, - transaction: unknown - ): Promise> { - return this.numberGenerator.nextForCompany(companyId, series, transaction); - } -} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-public-services.interface.ts b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-public-services.interface.ts new file mode 100644 index 00000000..df12af55 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-public-services.interface.ts @@ -0,0 +1,19 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import type { Result } from "@repo/rdx-utils"; + +import type { IIssuedInvoiceCreatorParams } from "."; + +import type { IssuedInvoice } from "../../../domain"; + +export interface IIssuedInvoiceServicesContext { + transaction: unknown; + companyId: UniqueID; +} + +export interface IIssuedInvoicePublicServices { + createIssuedInvoice: ( + id: UniqueID, + props: IIssuedInvoiceCreatorParams["props"], + context: IIssuedInvoiceServicesContext + ) => Promise>; +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-write-service.ts b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-write-service.ts deleted file mode 100644 index dabd8f02..00000000 --- a/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-write-service.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { UniqueID } from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; -import type { Transaction } from "sequelize"; - -import type { CustomerInvoice } from "../../../domain/aggregates"; -import type { ICustomerInvoiceRepository } from "../../../domain/repositories"; - -export type IIssuedInvoiceWriteService = {}; - -export class IssuedInvoiceWriteService implements IIssuedInvoiceWriteService { - constructor(private readonly repository: ICustomerInvoiceRepository) {} - - /** - * Emite (crea) una factura definitiva a partir de una factura ya preparada. - * - * Asume que: - * - el número ya ha sido asignado - * - el estado es correcto - */ - async createIssuedInvoice( - companyId: UniqueID, - invoice: CustomerInvoice, - transaction: Transaction - ): Promise> { - const result = await this.repository.create(invoice, transaction); - if (result.isFailure) { - return Result.fail(result.error); - } - - return result; - } -} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/services/proforma-to-issued-invoice-materializer.ts b/modules/customer-invoices/src/api/application/issued-invoices/services/proforma-to-issued-invoice-materializer.ts deleted file mode 100644 index c616595c..00000000 --- a/modules/customer-invoices/src/api/application/issued-invoices/services/proforma-to-issued-invoice-materializer.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { UniqueID } from "@repo/rdx-ddd"; -import { Collection, Result } from "@repo/rdx-utils"; - -import type { IIssuedInvoiceProps, Proforma } from "../../../domain"; - -export interface IProformaToIssuedInvoiceMaterializer { - materialize(proforma: Proforma, issuedInvoiceId: UniqueID): Result; -} - -export class ProformaToIssuedInvoiceMaterializer implements IProformaToIssuedInvoiceMaterializer { - public materialize( - proforma: Proforma, - issuedInvoiceId: UniqueID - ): Result { - const amounts = proforma.calculateAllAmounts(); - const taxGroups = proforma.getTaxes(); - - const issuedItems = proforma.items.map((item) => ({ - description: item.description, - quantity: item.quantity, - unitPrice: item.unitAmount, - taxableAmount: item.getTaxableAmount(), - taxesAmount: item.getTaxesAmount(), - totalAmount: item.getTotalAmount(), - })); - - const issuedTaxes = taxGroups.map((group) => ({ - ivaCode: group.iva.code, - ivaPercentage: group.iva.percentage, - ivaAmount: group.calculateAmounts().ivaAmount, - recCode: group.rec?.code, - recPercentage: group.rec?.percentage, - recAmount: group.calculateAmounts().recAmount, - retentionCode: group.retention?.code, - retentionPercentage: group.retention?.percentage, - retentionAmount: group.calculateAmounts().retentionAmount, - })); - - return Result.ok({ - companyId: proforma.companyId, - invoiceNumber: proforma.invoiceNumber, - invoiceDate: proforma.invoiceDate, - customerId: proforma.customerId, - languageCode: proforma.languageCode, - currencyCode: proforma.currencyCode, - paymentMethod: proforma.paymentMethod, - discountPercentage: proforma.globalDiscountPercentage, - - items: new Collection(issuedItems), - taxes: new Collection(issuedTaxes), - subtotalAmount: amounts.subtotalAmount, - taxableAmount: amounts.taxableAmount, - totalAmount: amounts.totalAmount, - }); - } -} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/services/proforma-to-issued-invoice-props-converter.ts b/modules/customer-invoices/src/api/application/issued-invoices/services/proforma-to-issued-invoice-props-converter.ts new file mode 100644 index 00000000..df190866 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/services/proforma-to-issued-invoice-props-converter.ts @@ -0,0 +1,149 @@ +import { Maybe, Result } from "@repo/rdx-utils"; + +import { + type IIssuedInvoiceCreateProps, + InvoiceStatus, + IssuedInvoiceItem, + IssuedInvoiceTax, + IssuedInvoiceTaxes, + type Proforma, +} from "../../../domain"; + +export interface IProformaToIssuedInvoiceConverter { + toCreateProps(proforma: Proforma): Result; +} + +export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoiceConverter { + public toCreateProps(proforma: Proforma): Result { + const proformaTotals = proforma.totals(); + const taxTotals = proforma.taxes(); + + const issuedItems: IssuedInvoiceItem[] = []; + + for (const item of proforma.items.getAll()) { + const itemTotals = item.totals(); + + const itemOrResult = IssuedInvoiceItem.create({ + description: item.description, + quantity: item.quantity, + unitAmount: item.unitAmount, + currencyCode: proforma.currencyCode, + languageCode: proforma.languageCode, + + itemDiscountPercentage: item.itemDiscountPercentage, + itemDiscountAmount: itemTotals.itemDiscountAmount, + + globalDiscountPercentage: item.globalDiscountPercentage, + globalDiscountAmount: itemTotals.globalDiscountAmount, + + totalDiscountAmount: itemTotals.totalDiscountAmount, + + ivaCode: item.ivaCode(), + ivaPercentage: item.ivaPercentage(), + ivaAmount: itemTotals.ivaAmount, + + recCode: item.recCode(), + recPercentage: item.recPercentage(), + recAmount: itemTotals.recAmount, + + retentionCode: item.retentionCode(), + retentionPercentage: item.retentionPercentage(), + retentionAmount: itemTotals.recAmount, + + subtotalAmount: itemTotals.subtotalAmount, + taxableAmount: itemTotals.taxableAmount, + taxesAmount: itemTotals.taxesAmount, + totalAmount: itemTotals.totalAmount, + }); + + if (itemOrResult.isFailure) { + return Result.fail(itemOrResult.error); + } + + issuedItems.push(itemOrResult.data); + } + + const issuedTaxes: IssuedInvoiceTax[] = []; + for (const tax of taxTotals.getAll()) { + if (tax.ivaCode.isNone()) { + return Result.fail(new Error("IVA code is required")); + } + + const taxOrResult = IssuedInvoiceTax.create({ + taxableAmount: tax.taxableAmount, + ivaCode: tax.ivaCode.unwrap(), + ivaPercentage: tax.ivaPercentage.unwrap(), + ivaAmount: tax.ivaAmount, + + recCode: tax.recCode, + recPercentage: tax.recPercentage, + recAmount: tax.recAmount, + + retentionCode: tax.retentionCode, + retentionAmount: tax.retentionAmount, + retentionPercentage: tax.retentionPercentage, + + taxesAmount: tax.taxesAmount, + }); + + if (taxOrResult.isFailure) { + return Result.fail(taxOrResult.error); + } + + issuedTaxes.push(taxOrResult.data); + } + + const issuedInvoiceProps: IIssuedInvoiceCreateProps = { + companyId: proforma.companyId, + status: InvoiceStatus.issued(), + + series: proforma.series, + proformaId: proforma.id, + + invoiceNumber: proforma.invoiceNumber, + + invoiceDate: proforma.invoiceDate, + operationDate: proforma.operationDate, + + description: proforma.description, + + languageCode: proforma.languageCode, + currencyCode: proforma.currencyCode, + notes: proforma.notes, + reference: proforma.reference, + + paymentMethod: proforma.paymentMethod, + + customerId: proforma.customerId, + recipient: proforma.recipient, + + items: issuedItems, + + taxes: IssuedInvoiceTaxes.create({ + currencyCode: proforma.currencyCode, + languageCode: proforma.languageCode, + taxes: issuedTaxes, + }), + + subtotalAmount: proformaTotals.subtotalAmount, + + itemsDiscountAmount: proformaTotals.itemsDiscountAmount, + globalDiscountPercentage: proforma.globalDiscountPercentage, + globalDiscountAmount: proformaTotals.globalDiscountAmount, + totalDiscountAmount: proformaTotals.totalDiscountAmount, + + taxableAmount: proformaTotals.taxableAmount, + + ivaAmount: proformaTotals.ivaAmount, + recAmount: proformaTotals.recAmount, + retentionAmount: proformaTotals.retentionAmount, + + taxesAmount: proformaTotals.taxesAmount, + totalAmount: proformaTotals.totalAmount, + + verifactu: Maybe.none(), + }; + + return Result.ok(issuedInvoiceProps); + } +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/index.ts index 3b83e1ff..a23ae1ee 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/index.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/index.ts @@ -1,2 +1,3 @@ export * from "./full"; +export * from "./report"; export * from "./summary"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/report/issued-invoice-report-item-snapshot-builder.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/report/issued-invoice-report-item-snapshot-builder.ts index 8940b1ac..33f482ab 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/report/issued-invoice-report-item-snapshot-builder.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/report/issued-invoice-report-item-snapshot-builder.ts @@ -27,10 +27,10 @@ export class IssuedInvoiceReportItemSnapshotBuilder quantity: QuantityDTOHelper.format(item.quantity, locale, { minimumFractionDigits: 0 }), unit_amount: MoneyDTOHelper.format(item.unit_amount, locale, moneyOptions), subtotal_amount: MoneyDTOHelper.format(item.subtotal_amount, locale, moneyOptions), - discount_percentage: PercentageDTOHelper.format(item.discount_percentage, locale, { + discount_percentage: PercentageDTOHelper.format(item.item_discount_percentage, locale, { minimumFractionDigits: 0, }), - discount_amount: MoneyDTOHelper.format(item.discount_amount, locale, moneyOptions), + discount_amount: MoneyDTOHelper.format(item.item_discount_amount, locale, moneyOptions), taxable_amount: MoneyDTOHelper.format(item.taxable_amount, locale, moneyOptions), taxes_amount: MoneyDTOHelper.format(item.taxes_amount, locale, moneyOptions), total_amount: MoneyDTOHelper.format(item.total_amount, locale, moneyOptions), diff --git a/modules/customer-invoices/src/api/application/issued-invoices/use-cases/list-issued-invoices.use-case.ts b/modules/customer-invoices/src/api/application/issued-invoices/use-cases/list-issued-invoices.use-case.ts index 695a1b9f..d6551f2b 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/use-cases/list-issued-invoices.use-case.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/use-cases/list-issued-invoices.use-case.ts @@ -2,7 +2,6 @@ import type { ITransactionManager } from "@erp/core/api"; import type { Criteria } from "@repo/rdx-criteria/server"; import type { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import type { Transaction } from "sequelize"; import type { IIssuedInvoiceFinder } from "../services"; import type { IIssuedInvoiceSummarySnapshotBuilder } from "../snapshot-builders"; @@ -22,7 +21,7 @@ export class ListIssuedInvoicesUseCase { public execute(params: ListIssuedInvoicesUseCaseInput) { const { criteria, companyId } = params; - return this.transactionManager.complete(async (transaction: Transaction) => { + return this.transactionManager.complete(async (transaction: unknown) => { try { const result = await this.finder.findIssuedInvoicesByCriteria( companyId, diff --git a/modules/customer-invoices/src/api/application/proformas/di/index.ts b/modules/customer-invoices/src/api/application/proformas/di/index.ts index 6c7b5c47..43c8a3f1 100644 --- a/modules/customer-invoices/src/api/application/proformas/di/index.ts +++ b/modules/customer-invoices/src/api/application/proformas/di/index.ts @@ -1,5 +1,6 @@ export * from "./proforma-creator.di"; export * from "./proforma-finder.di"; export * from "./proforma-input-mappers.di"; +export * from "./proforma-issuer.di"; export * from "./proforma-snapshot-builders.di"; export * from "./proforma-use-cases.di"; diff --git a/modules/customer-invoices/src/api/application/proformas/di/proforma-issuer.di.ts b/modules/customer-invoices/src/api/application/proformas/di/proforma-issuer.di.ts new file mode 100644 index 00000000..df17d7ad --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/di/proforma-issuer.di.ts @@ -0,0 +1,15 @@ +import type { IProformaToIssuedInvoiceConverter } from "../../issued-invoices"; +import type { IProformaRepository } from "../repositories"; +import { type IProformaIssuer, ProformaIssuer } from "../services"; + +export const buildProformaIssuer = (params: { + proformaConverter: IProformaToIssuedInvoiceConverter; + repository: IProformaRepository; +}): IProformaIssuer => { + const { proformaConverter, repository } = params; + + return new ProformaIssuer({ + proformaConverter, + repository, + }); +}; diff --git a/modules/customer-invoices/src/api/application/proformas/di/proforma-use-cases.di.ts b/modules/customer-invoices/src/api/application/proformas/di/proforma-use-cases.di.ts index 26678db8..35cdd861 100644 --- a/modules/customer-invoices/src/api/application/proformas/di/proforma-use-cases.di.ts +++ b/modules/customer-invoices/src/api/application/proformas/di/proforma-use-cases.di.ts @@ -1,9 +1,11 @@ import type { ITransactionManager } from "@erp/core/api"; +import type { IIssuedInvoicePublicServices } from "../../issued-invoices"; import type { ICreateProformaInputMapper } from "../mappers"; import type { IProformaCreator, IProformaFinder, + IProformaIssuer, ProformaDocumentGeneratorService, } from "../services"; import type { @@ -11,9 +13,13 @@ import type { IProformaSummarySnapshotBuilder, } from "../snapshot-builders"; import type { IProformaFullSnapshotBuilder } from "../snapshot-builders/full"; -import { GetProformaByIdUseCase, ListProformasUseCase, ReportProformaUseCase } from "../use-cases"; -import { CreateProformaUseCase } from "../use-cases/create-proforma"; -import { IssueProformaUseCase } from "../use-cases/issue-proforma.use-case"; +import { + CreateProformaUseCase, + GetProformaByIdUseCase, + IssueProformaUseCase, + ListProformasUseCase, + ReportProformaUseCase, +} from "../use-cases"; export function buildGetProformaByIdUseCase(deps: { finder: IProformaFinder; @@ -65,8 +71,26 @@ export function buildCreateProformaUseCase(deps: { }); } -export function buildIssueProformaUseCase(deps: { finder: IProformaFinder }) { - return new IssueProformaUseCase(deps.finder); +export function buildIssueProformaUseCase(deps: { + publicServices: { + issuedInvoiceServices: IIssuedInvoicePublicServices; + }; + finder: IProformaFinder; + issuer: IProformaIssuer; + transactionManager: ITransactionManager; +}) { + const { + finder, + issuer, + transactionManager, + publicServices: { issuedInvoiceServices }, + } = deps; + return new IssueProformaUseCase({ + issuedInvoiceServices, + finder, + issuer, + transactionManager, + }); } /*export function buildUpdateProformaUseCase(deps: { diff --git a/modules/customer-invoices/src/api/application/proformas/mappers/create-proforma-input.mapper.ts b/modules/customer-invoices/src/api/application/proformas/mappers/create-proforma-input.mapper.ts index d6d1cf22..deb81031 100644 --- a/modules/customer-invoices/src/api/application/proformas/mappers/create-proforma-input.mapper.ts +++ b/modules/customer-invoices/src/api/application/proformas/mappers/create-proforma-input.mapper.ts @@ -69,7 +69,7 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/ const { companyId } = params; try { - const defaultStatus = InvoiceStatus.fromDraft(); + const defaultStatus = InvoiceStatus.draft(); const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", errors); diff --git a/modules/customer-invoices/src/api/application/proformas/services/index.ts b/modules/customer-invoices/src/api/application/proformas/services/index.ts index 5bb325e5..843ab5fe 100644 --- a/modules/customer-invoices/src/api/application/proformas/services/index.ts +++ b/modules/customer-invoices/src/api/application/proformas/services/index.ts @@ -5,3 +5,4 @@ export * from "./proforma-document-properties-factory"; export * from "./proforma-finder"; export * from "./proforma-issuer"; export * from "./proforma-number-generator.interface"; +export * from "./proforma-public-services.interface"; diff --git a/modules/customer-invoices/src/api/application/proformas/services/proforma-creator.ts b/modules/customer-invoices/src/api/application/proformas/services/proforma-creator.ts index d73bdbe4..702a216e 100644 --- a/modules/customer-invoices/src/api/application/proformas/services/proforma-creator.ts +++ b/modules/customer-invoices/src/api/application/proformas/services/proforma-creator.ts @@ -1,6 +1,5 @@ import type { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import type { Transaction } from "sequelize"; import { type IProformaCreateProps, Proforma } from "../../../domain"; import type { IProformaRepository } from "../repositories"; @@ -11,7 +10,7 @@ export interface IProformaCreatorParams { companyId: UniqueID; id: UniqueID; props: Omit; - transaction: Transaction; + transaction: unknown; } export interface IProformaCreator { @@ -32,12 +31,7 @@ export class ProformaCreator implements IProformaCreator { this.repository = deps.repository; } - async create(params: { - companyId: UniqueID; - id: UniqueID; - props: IProformaCreateProps; - transaction: Transaction; - }): Promise> { + async create(params: IProformaCreatorParams): Promise> { const { companyId, id, props, transaction } = params; // 1. Obtener siguiente número diff --git a/modules/customer-invoices/src/api/application/proformas/services/proforma-finder.ts b/modules/customer-invoices/src/api/application/proformas/services/proforma-finder.ts index 0c5e3928..3bbe2aae 100644 --- a/modules/customer-invoices/src/api/application/proformas/services/proforma-finder.ts +++ b/modules/customer-invoices/src/api/application/proformas/services/proforma-finder.ts @@ -1,7 +1,6 @@ import type { Criteria } from "@repo/rdx-criteria/server"; import type { UniqueID } from "@repo/rdx-ddd"; import type { Collection, Result } from "@repo/rdx-utils"; -import type { Transaction } from "sequelize"; import type { Proforma } from "../../../domain"; import type { ProformaSummary } from "../models"; @@ -11,19 +10,19 @@ export interface IProformaFinder { findProformaById( companyId: UniqueID, invoiceId: UniqueID, - transaction?: Transaction + transaction?: unknown ): Promise>; proformaExists( companyId: UniqueID, invoiceId: UniqueID, - transaction?: Transaction + transaction?: unknown ): Promise>; findProformasByCriteria( companyId: UniqueID, criteria: Criteria, - transaction?: Transaction + transaction?: unknown ): Promise, Error>>; } @@ -33,7 +32,7 @@ export class ProformaFinder implements IProformaFinder { async findProformaById( companyId: UniqueID, proformaId: UniqueID, - transaction?: Transaction + transaction?: unknown ): Promise> { return this.repository.getByIdInCompany(companyId, proformaId, transaction); } @@ -41,7 +40,7 @@ export class ProformaFinder implements IProformaFinder { async proformaExists( companyId: UniqueID, proformaId: UniqueID, - transaction?: Transaction + transaction?: unknown ): Promise> { return this.repository.existsByIdInCompany(companyId, proformaId, transaction); } @@ -49,7 +48,7 @@ export class ProformaFinder implements IProformaFinder { async findProformasByCriteria( companyId: UniqueID, criteria: Criteria, - transaction?: Transaction + transaction?: unknown ): Promise, Error>> { return this.repository.findByCriteriaInCompany(companyId, criteria, transaction); } diff --git a/modules/customer-invoices/src/api/application/proformas/services/proforma-issuer.ts b/modules/customer-invoices/src/api/application/proformas/services/proforma-issuer.ts index 80ec781d..c5fa3f5f 100644 --- a/modules/customer-invoices/src/api/application/proformas/services/proforma-issuer.ts +++ b/modules/customer-invoices/src/api/application/proformas/services/proforma-issuer.ts @@ -1,44 +1,65 @@ import type { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import type { Transaction } from "sequelize"; -import type { IProformaToIssuedInvoiceMaterializer } from "../../issued-invoices"; +import type { IIssuedInvoiceCreateProps, Proforma } from "../../../domain"; +import type { IProformaToIssuedInvoiceConverter } from "../../issued-invoices"; +import type { IProformaRepository } from "../repositories"; + +export interface IProformaIssuerParams { + companyId: UniqueID; + proforma: Proforma; + issuedInvoiceId: UniqueID; + transaction: unknown; +} + +export interface IProformaIssuer { + issueProforma(params: IProformaIssuerParams): Promise>; +} + +type ProformaIssuerDeps = { + proformaConverter: IProformaToIssuedInvoiceConverter; + repository: IProformaRepository; +}; export class ProformaIssuer implements IProformaIssuer { - private readonly proformaRepository: IProformaRepository; - private readonly issuedInvoiceFactory: IIssuedInvoiceFactory; - private readonly issuedInvoiceRepository: IIssuedInvoiceRepository; - private readonly materializer: IProformaToIssuedInvoiceMaterializer; + private readonly proformaConverter: IProformaToIssuedInvoiceConverter; + private readonly repository: IProformaRepository; constructor(deps: ProformaIssuerDeps) { - this.proformaRepository = deps.proformaRepository; - this.issuedInvoiceFactory = deps.issuedInvoiceFactory; - this.issuedInvoiceRepository = deps.issuedInvoiceRepository; - this.materializer = deps.materializer; + this.proformaConverter = deps.proformaConverter; + this.repository = deps.repository; } - public async issue( - proforma: Proforma, - issuedInvoiceId: UniqueID, - transaction: Transaction - ): Promise> { + public async issueProforma( + params: IProformaIssuerParams + ): Promise> { + const { proforma, companyId, transaction } = params; + + // Cambiamos el estado de la proforma a 'issued' const issueResult = proforma.issue(); - if (issueResult.isFailure) return Result.fail(issueResult.error); - - const propsResult = this.materializer.materialize(proforma, issuedInvoiceId); - - if (propsResult.isFailure) return Result.fail(propsResult.error); - - const invoiceResult = this.issuedInvoiceFactory.create(propsResult.data, issuedInvoiceId); - - if (invoiceResult.isFailure) { - return Result.fail(invoiceResult.error); + if (issueResult.isFailure) { + return Result.fail(issueResult.error); } - await this.issuedInvoiceRepository.save(proforma.companyId, invoiceResult.data, transaction); + // Persistir + const updateStatusResult = await this.repository.updateStatusByIdInCompany( + companyId, + proforma.id, + proforma.status, + transaction + ); - await this.proformaRepository.save(proforma.companyId, proforma, transaction); + if (updateStatusResult.isFailure) { + return Result.fail(updateStatusResult.error); + } - return Result.ok(proforma); + // Generamos las propiedades de la factura a partir de la proforma + const propsResult = this.proformaConverter.toCreateProps(proforma); + + if (propsResult.isFailure) { + return Result.fail(propsResult.error); + } + + return Result.ok(propsResult.data); } } diff --git a/modules/customer-invoices/src/api/application/proformas/services/proforma-number-generator.interface.ts b/modules/customer-invoices/src/api/application/proformas/services/proforma-number-generator.interface.ts index 23558770..9b73cde2 100644 --- a/modules/customer-invoices/src/api/application/proformas/services/proforma-number-generator.interface.ts +++ b/modules/customer-invoices/src/api/application/proformas/services/proforma-number-generator.interface.ts @@ -7,10 +7,10 @@ import type { Maybe, Result } from "@repo/rdx-utils"; */ export interface IProformaNumberGenerator { /** - * Devuelve el siguiente número de factura disponible para una empresa dentro de una "serie" de factura. + * Devuelve el siguiente número de proforma disponible para una empresa dentro de una "serie" de proforma. * * @param companyId - Identificador de la empresa - * @param serie - Serie por la que buscar la última factura + * @param serie - Serie por la que buscar la última proforma * @param transaction - Transacción activa */ getNextForCompany( diff --git a/modules/customer-invoices/src/api/application/proformas/services/proforma-public-services.interface.ts b/modules/customer-invoices/src/api/application/proformas/services/proforma-public-services.interface.ts new file mode 100644 index 00000000..54ae989a --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/services/proforma-public-services.interface.ts @@ -0,0 +1,34 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import type { Result } from "@repo/rdx-utils"; + +import type { Proforma } from "../../../domain/proformas"; +import type { IProformaFullSnapshot } from "../snapshot-builders"; + +import type { IProformaCreatorParams } from "./proforma-creator"; + +export interface IProformaServicesContext { + transaction: unknown; + companyId: UniqueID; +} + +export interface IProformaPublicServices { + createProforma: ( + id: UniqueID, + props: IProformaCreatorParams["props"], + context: IProformaServicesContext + ) => Promise>; + + listProformas: (filters: unknown, context: unknown) => null; + + getProformaById: ( + id: UniqueID, + context: IProformaServicesContext + ) => Promise>; + + getProformaSnapshotById: ( + id: UniqueID, + context: IProformaServicesContext + ) => Promise>; + + generateProformaReport: (id: unknown, options: unknown, context: unknown) => null; +} diff --git a/modules/customer-invoices/src/api/application/proformas/services/proforma-write-service.ts b/modules/customer-invoices/src/api/application/proformas/services/proforma-write-service.ts deleted file mode 100644 index 5955ada9..00000000 --- a/modules/customer-invoices/src/api/application/proformas/services/proforma-write-service.ts +++ /dev/null @@ -1,134 +0,0 @@ -import type { UniqueID } from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; -import type { Transaction } from "sequelize"; - -import { - CustomerInvoiceIsProformaSpecification, - type InvoiceStatus, - ProformaCannotBeDeletedError, - StatusInvoiceIsDraftSpecification, -} from "../../../domain"; -import type { - CustomerInvoice, - CustomerInvoicePatchProps, - CustomerInvoiceProps, -} from "../../../domain/aggregates"; -import type { ICustomerInvoiceRepository } from "../../../domain/repositories"; -import type { IProformaFactory } from "../../services/proforma-factory"; - -export type IIssuedInvoiceWriteService = {}; - -export class IssuedInvoiceWriteService implements IIssuedInvoiceWriteService { - constructor( - private readonly repository: ICustomerInvoiceRepository, - private readonly proformaFactory: IProformaFactory - ) {} - - /** - * Crea y persiste una nueva proforma para una empresa. - */ - async createProforma( - companyId: UniqueID, - props: Omit, - transaction: Transaction - ): Promise> { - const invoiceResult = this.proformaFactory.createProforma(companyId, props); - if (invoiceResult.isFailure) { - return Result.fail(invoiceResult.error); - } - - return this.repository.create(invoiceResult.data, transaction); - } - - /** - * Actualiza una proforma existente. - */ - async updateProforma( - companyId: UniqueID, - proforma: CustomerInvoice, - transaction: Transaction - ): Promise> { - return this.repository.update(proforma, transaction); - } - - /** - * Aplica cambios parciales a una proforma existente. - * No persiste automáticamente. - */ - async patchProforma( - companyId: UniqueID, - proformaId: UniqueID, - changes: CustomerInvoicePatchProps, - transaction: Transaction - ): Promise> { - const proformaResult = await this.repository.getProformaByIdInCompany( - companyId, - proformaId, - transaction, - {} - ); - - if (proformaResult.isFailure) { - return Result.fail(proformaResult.error); - } - - const updated = proformaResult.data.update(changes); - if (updated.isFailure) { - return Result.fail(updated.error); - } - - return Result.ok(updated.data); - } - - /** - * Elimina (baja lógica) una proforma. - */ - async deleteProforma( - companyId: UniqueID, - proformaId: UniqueID, - transaction: Transaction - ): Promise> { - const proformaResult = await this.repository.getProformaByIdInCompany( - companyId, - proformaId, - transaction, - {} - ); - if (proformaResult.isFailure) { - return Result.fail(proformaResult.error); - } - - const proforma = proformaResult.data; - - const isProforma = new CustomerInvoiceIsProformaSpecification(); - if (!(await isProforma.isSatisfiedBy(proforma))) { - return Result.fail(new ProformaCannotBeDeletedError(proformaId.toString(), "not a proforma")); - } - - const isDraft = new StatusInvoiceIsDraftSpecification(); - if (!(await isDraft.isSatisfiedBy(proforma))) { - return Result.fail( - new ProformaCannotBeDeletedError(proformaId.toString(), "status is not 'draft'") - ); - } - - return this.repository.deleteProformaByIdInCompany(companyId, proformaId, transaction); - } - - /** - * Actualiza el estado de una proforma. - */ - async updateProformaStatus( - companyId: UniqueID, - proformaId: UniqueID, - newStatus: InvoiceStatus, - transaction?: Transaction - ): Promise> { - return this.repository.updateProformaStatusByIdInCompany( - companyId, - proformaId, - newStatus, - transaction - ); - } -} diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-item-full-snapshot.interface.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-item-full-snapshot.interface.ts index ea7cc9c0..37c94a5c 100644 --- a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-item-full-snapshot.interface.ts +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-item-full-snapshot.interface.ts @@ -9,8 +9,8 @@ export interface IProformaItemFullSnapshot { subtotal_amount: { value: string; scale: string; currency_code: string }; - discount_percentage: { value: string; scale: string }; - discount_amount: { value: string; scale: string; currency_code: string }; + item_discount_percentage: { value: string; scale: string }; + item_discount_amount: { value: string; scale: string; currency_code: string }; global_discount_percentage: { value: string; scale: string }; global_discount_amount: { value: string; scale: string; currency_code: string }; diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-items-full-snapshot-builder.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-items-full-snapshot-builder.ts index 42eb818d..4d9c4fda 100644 --- a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-items-full-snapshot-builder.ts +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-items-full-snapshot-builder.ts @@ -32,8 +32,10 @@ export class ProformaItemsFullSnapshotBuilder implements IProformaItemsFullSnaps ? allAmounts.subtotalAmount.toObjectString() : ItemAmount.EMPTY_MONEY_OBJECT, - discount_percentage: maybeToEmptyPercentageObjectString(proformaItem.itemDiscountPercentage), - discount_amount: isValued + item_discount_percentage: maybeToEmptyPercentageObjectString( + proformaItem.itemDiscountPercentage + ), + item_discount_amount: isValued ? allAmounts.itemDiscountAmount.toObjectString() : ItemAmount.EMPTY_MONEY_OBJECT, diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-items-report-snapshot-builder.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-items-report-snapshot-builder.ts index 78cfd35c..1b0b72d1 100644 --- a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-items-report-snapshot-builder.ts +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-items-report-snapshot-builder.ts @@ -1,14 +1,16 @@ import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/core"; import type { ISnapshotBuilder, ISnapshotBuilderParams } from "@erp/core/api"; -import type { ProformaFullSnapshot, ProformaReportItemSnapshot } from "../../application-models"; +import type { IProformaFullSnapshot } from "../full"; + +import type { ProformaReportItemSnapshot } from "./proforma-report-item-snapshot.interface"; export interface IProformaItemReportSnapshotBuilder - extends ISnapshotBuilder {} + extends ISnapshotBuilder {} export class ProformaItemReportSnapshotBuilder implements IProformaItemReportSnapshotBuilder { toOutput( - items: ProformaFullSnapshot["items"], + items: IProformaFullSnapshot["items"], params?: ISnapshotBuilderParams ): ProformaReportItemSnapshot[] { const locale = params?.locale as string; diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-report-snapshot-builder.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-report-snapshot-builder.ts index e5220270..9f058191 100644 --- a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-report-snapshot-builder.ts +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-report-snapshot-builder.ts @@ -1,30 +1,29 @@ import { DateHelper, MoneyDTOHelper, PercentageDTOHelper } from "@erp/core"; import type { ISnapshotBuilder, ISnapshotBuilderParams } from "@erp/core/api"; -import type { - ProformaFullSnapshot, - ProformaReportItemSnapshot, - ProformaReportSnapshot, - ProformaReportTaxSnapshot, -} from "../../application-models"; +import type { IProformaFullSnapshot } from "../full"; + +import type { ProformaReportItemSnapshot } from "./proforma-report-item-snapshot.interface"; +import type { ProformaReportSnapshot } from "./proforma-report-snapshot.interface"; +import type { ProformaReportTaxSnapshot } from "./proforma-report-tax-snapshot.interface"; export interface IProformaReportSnapshotBuilder - extends ISnapshotBuilder {} + extends ISnapshotBuilder {} export class ProformaReportSnapshotBuilder implements IProformaReportSnapshotBuilder { constructor( private readonly itemsBuilder: ISnapshotBuilder< - ProformaFullSnapshot["items"], + IProformaFullSnapshot["items"], ProformaReportItemSnapshot[] >, private readonly taxesBuilder: ISnapshotBuilder< - ProformaFullSnapshot["taxes"], + IProformaFullSnapshot["taxes"], ProformaReportTaxSnapshot[] > ) {} toOutput( - snapshot: ProformaFullSnapshot, + snapshot: IProformaFullSnapshot, params?: ISnapshotBuilderParams ): ProformaReportSnapshot { const locale = params?.locale as string; @@ -61,17 +60,37 @@ export class ProformaReportSnapshotBuilder implements IProformaReportSnapshotBui taxes: this.taxesBuilder.toOutput(snapshot.taxes, { locale }), subtotal_amount: MoneyDTOHelper.format(snapshot.subtotal_amount, locale, moneyOptions), - discount_percentage: PercentageDTOHelper.format(snapshot.discount_percentage, locale, { - hideZeros: true, - }), - discount_amount: MoneyDTOHelper.format(snapshot.discount_amount, locale, moneyOptions), + + items_discount_amount: MoneyDTOHelper.format( + snapshot.items_discount_amount, + locale, + moneyOptions + ), + global_discount_percentage: PercentageDTOHelper.format( + snapshot.global_discount_percentage, + locale, + { + hideZeros: true, + } + ), + global_discount_amount: MoneyDTOHelper.format( + snapshot.global_discount_amount, + locale, + moneyOptions + ), + total_discount_amount: MoneyDTOHelper.format( + snapshot.total_discount_amount, + locale, + moneyOptions + ), + taxable_amount: MoneyDTOHelper.format(snapshot.taxable_amount, locale, moneyOptions), taxes_amount: MoneyDTOHelper.format(snapshot.taxes_amount, locale, moneyOptions), total_amount: MoneyDTOHelper.format(snapshot.total_amount, locale, moneyOptions), }; } - private formatAddress(recipient: ProformaFullSnapshot["recipient"]): string { + private formatAddress(recipient: IProformaFullSnapshot["recipient"]): string { const lines: string[] = []; if (recipient.street) lines.push(recipient.street); diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-report-snapshot.interface.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-report-snapshot.interface.ts index c933444a..a8d4a59c 100644 --- a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-report-snapshot.interface.ts +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-report-snapshot.interface.ts @@ -27,8 +27,14 @@ export interface ProformaReportSnapshot { taxes: ProformaReportTaxSnapshot[]; subtotal_amount: string; - discount_percentage: string; - discount_amount: string; + + items_discount_amount: string; + + global_discount_percentage: string; + global_discount_amount: string; + + total_discount_amount: string; + taxable_amount: string; taxes_amount: string; total_amount: string; diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/create-proforma/create-proforma.use-case.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/create-proforma/create-proforma.use-case.ts index fed8e90e..f24478f8 100644 --- a/modules/customer-invoices/src/api/application/proformas/use-cases/create-proforma/create-proforma.use-case.ts +++ b/modules/customer-invoices/src/api/application/proformas/use-cases/create-proforma/create-proforma.use-case.ts @@ -1,7 +1,6 @@ import type { ITransactionManager } from "@erp/core/api"; import type { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import type { Transaction } from "sequelize"; import type { CreateProformaRequestDTO } from "../../../../../common"; import type { ICreateProformaInputMapper } from "../../mappers"; @@ -44,7 +43,7 @@ export class CreateProformaUseCase { const { props, id } = mappedPropsResult.data; - return this.transactionManager.complete(async (transaction: Transaction) => { + return this.transactionManager.complete(async (transaction: unknown) => { try { const createResult = await this.creator.create({ companyId, id, props, transaction }); diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/issue-proforma.use-case.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/issue-proforma.use-case.ts index c1b948ba..4c29f829 100644 --- a/modules/customer-invoices/src/api/application/proformas/use-cases/issue-proforma.use-case.ts +++ b/modules/customer-invoices/src/api/application/proformas/use-cases/issue-proforma.use-case.ts @@ -1,20 +1,23 @@ -import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api"; -import { UniqueID, UtcDate } from "@repo/rdx-ddd"; +import type { ITransactionManager } from "@erp/core/api"; +import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import { - IssueCustomerInvoiceDomainService, - ProformaCustomerInvoiceDomainService, -} from "../../../domain"; -import type { CustomerInvoiceApplicationService } from "../../services"; +import type { IIssuedInvoicePublicServices } from "../../issued-invoices"; +import type { IProformaFinder, IProformaIssuer } from "../services"; type IssueProformaUseCaseInput = { companyId: UniqueID; proforma_id: string; }; +type IssueProformaUseCaseDeps = { + issuedInvoiceServices: IIssuedInvoicePublicServices; + finder: IProformaFinder; + issuer: IProformaIssuer; + transactionManager: ITransactionManager; +}; /** - * Caso de uso: Conversión de una proforma a factura definitiva. + * Caso de uso: Conversión de una issuedinvoice a factura definitiva. * * - Recupera la proforma * - Valida su estado ("approved") @@ -23,29 +26,30 @@ type IssueProformaUseCaseInput = { * - Persiste ambas dentro de la misma transacción */ export class IssueProformaUseCase { - private readonly issueDomainService: IssueCustomerInvoiceDomainService; - private readonly proformaDomainService: ProformaCustomerInvoiceDomainService; + private readonly issuedInvoiceServices: IIssuedInvoicePublicServices; + private readonly finder: IProformaFinder; + private readonly issuer: IProformaIssuer; + private readonly transactionManager: ITransactionManager; - constructor( - private readonly service: CustomerInvoiceApplicationService, - private readonly transactionManager: ITransactionManager, - private readonly presenterRegistry: IPresenterRegistry - ) { - this.issueDomainService = new IssueCustomerInvoiceDomainService(); - this.proformaDomainService = new ProformaCustomerInvoiceDomainService(); + constructor(deps: IssueProformaUseCaseDeps) { + this.issuedInvoiceServices = deps.issuedInvoiceServices; + this.finder = deps.finder; + this.issuer = deps.issuer; + this.transactionManager = deps.transactionManager; } public execute(params: IssueProformaUseCaseInput) { const { proforma_id, companyId } = params; - const idOrError = UniqueID.create(proforma_id); - if (idOrError.isFailure) return Result.fail(idOrError.error); - const proformaId = idOrError.data; + const proformaIdOrError = UniqueID.create(proforma_id); + if (proformaIdOrError.isFailure) return Result.fail(proformaIdOrError.error); + + const proformaId = proformaIdOrError.data; return this.transactionManager.complete(async (transaction) => { try { - /** 1. Recuperamos la proforma */ - const proformaResult = await this.service.getProformaByIdInCompany( + // 1. Recuperamos la issuedinvoice + const proformaResult = await this.finder.findProformaById( companyId, proformaId, transaction @@ -54,49 +58,39 @@ export class IssueProformaUseCase { if (proformaResult.isFailure) return Result.fail(proformaResult.error); const proforma = proformaResult.data; - /** 2. Generar nueva factura */ - const nextNumberResult = await this.service.getNextIssuedInvoiceNumber( + // 2. Generamos la factura definitiva y la guardamos + const issuedInvoiceId = UniqueID.generateNewID(); + const createPropsOrError = await this.issuer.issueProforma({ companyId, - proforma.series, - transaction - ); - - if (nextNumberResult.isFailure) return Result.fail(nextNumberResult.error); - - /** 4. Crear factura definitiva (dominio) */ - const issuedInvoiceOrError = await this.issueDomainService.issueFromProforma(proforma, { - issueNumber: nextNumberResult.data, - issueDate: UtcDate.today(), + issuedInvoiceId, + proforma, + transaction, }); - if (issuedInvoiceOrError.isFailure) return Result.fail(issuedInvoiceOrError.error); + if (createPropsOrError.isFailure) { + return Result.fail(createPropsOrError.error); + } - /** 5. Guardar la nueva factura */ - const saveInvoiceResult = await this.service.createIssuedInvoiceInCompany( - companyId, - issuedInvoiceOrError.data, - transaction - ); - if (saveInvoiceResult.isFailure) return Result.fail(saveInvoiceResult.error); + const createProps = createPropsOrError.data; - /** 6. Actualizar la proforma */ - const closedProformaResult = await this.proformaDomainService.markAsIssued(proforma); - if (closedProformaResult.isFailure) return Result.fail(closedProformaResult.error); - const closedProforma = closedProformaResult.data; - - /** 7. Guardar la proforma */ - await this.service.updateProformaStatusByIdInCompany( - companyId, - proformaId, - closedProforma.status, - transaction + // Creamos y guardamos en persistencia la factura definitiva + const invoiceResult = await this.issuedInvoiceServices.createIssuedInvoice( + issuedInvoiceId, + createProps, + { + companyId, + transaction, + } ); - const invoice = saveInvoiceResult.data; + if (invoiceResult.isFailure) { + return Result.fail(invoiceResult.error); + } + const dto = { - proforma_id: proforma.id.toString(), - invoice_id: invoice.id.toString(), - customer_id: invoice.customerId.toString(), + issuedinvoice_id: issuedInvoiceId.toString(), + proforma_id: proformaId.toString(), + customer_id: proforma.customerId.toString(), }; return Result.ok(dto); } catch (error: unknown) { diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/list-proformas.use-case.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/list-proformas.use-case.ts index 11b47bc7..595f6370 100644 --- a/modules/customer-invoices/src/api/application/proformas/use-cases/list-proformas.use-case.ts +++ b/modules/customer-invoices/src/api/application/proformas/use-cases/list-proformas.use-case.ts @@ -2,7 +2,6 @@ import type { ITransactionManager } from "@erp/core/api"; import type { Criteria } from "@repo/rdx-criteria/server"; import type { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import type { Transaction } from "sequelize"; import type { IProformaFinder } from "../services"; import type { IProformaSummarySnapshotBuilder } from "../snapshot-builders"; @@ -22,7 +21,7 @@ export class ListProformasUseCase { public execute(params: ListProformasUseCaseInput) { const { criteria, companyId } = params; - return this.transactionManager.complete(async (transaction: Transaction) => { + return this.transactionManager.complete(async (transaction: unknown) => { try { const result = await this.finder.findProformasByCriteria(companyId, criteria, transaction); diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/update-proforma/update-proforma.use-case.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/update-proforma/update-proforma.use-case.ts index 7af377ce..e451ae3b 100644 --- a/modules/customer-invoices/src/api/application/proformas/use-cases/update-proforma/update-proforma.use-case.ts +++ b/modules/customer-invoices/src/api/application/proformas/use-cases/update-proforma/update-proforma.use-case.ts @@ -1,7 +1,6 @@ import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api"; import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import type { Transaction } from "sequelize"; import type { UpdateProformaByIdRequestDTO } from "../../../../../common"; import type { ProformaPatchProps } from "../../../../domain"; @@ -43,7 +42,7 @@ export class UpdateProformaUseCase { const patchProps: ProformaPatchProps = patchPropsResult.data; - return this.transactionManager.complete(async (transaction: Transaction) => { + return this.transactionManager.complete(async (transaction: unknown) => { try { const updatedInvoice = await this.service.patchProformaByIdInCompany( companyId, 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 1e64100b..45aeb0fa 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 @@ -1,7 +1,6 @@ import type { Criteria } from "@repo/rdx-criteria/server"; import type { UniqueID } from "@repo/rdx-ddd"; import { type Collection, Maybe, Result } from "@repo/rdx-utils"; -import type { Transaction } from "sequelize"; import { CustomerInvoiceIsProformaSpecification, @@ -35,7 +34,7 @@ export class CustomerInvoiceApplicationService { */ async getNextProformaNumber( companyId: UniqueID, - transaction: Transaction + transaction: unknown ): Promise> { return await this.numberGenerator.nextForCompany(companyId, Maybe.none(), transaction); } @@ -51,7 +50,7 @@ export class CustomerInvoiceApplicationService { async getNextIssuedInvoiceNumber( companyId: UniqueID, series: Maybe, - transaction: Transaction + transaction: unknown ): Promise> { return await this.numberGenerator.nextForCompany(companyId, series, transaction); } @@ -83,7 +82,7 @@ export class CustomerInvoiceApplicationService { async createIssuedInvoiceInCompany( companyId: UniqueID, invoice: CustomerInvoice, - transaction: Transaction + transaction: unknown ): Promise> { const result = await this.repository.create(invoice, transaction); if (result.isFailure) { @@ -104,7 +103,7 @@ export class CustomerInvoiceApplicationService { async createProformaInCompany( companyId: UniqueID, proforma: CustomerInvoice, - transaction: Transaction + transaction: unknown ): Promise> { const result = await this.repository.create(proforma, transaction); if (result.isFailure) { @@ -125,7 +124,7 @@ export class CustomerInvoiceApplicationService { async updateProformaInCompany( companyId: UniqueID, proforma: CustomerInvoice, - transaction: Transaction + transaction: unknown ): Promise> { const result = await this.repository.update(proforma, transaction); if (result.isFailure) { @@ -148,7 +147,7 @@ export class CustomerInvoiceApplicationService { async existsProformaByIdInCompany( companyId: UniqueID, proformaId: UniqueID, - transaction?: Transaction + transaction?: unknown ): Promise> { return this.repository.existsByIdInCompany(companyId, proformaId, transaction, { is_proforma: true, @@ -168,7 +167,7 @@ export class CustomerInvoiceApplicationService { async existsIssuedInvoiceByIdInCompany( companyId: UniqueID, invoiceId: UniqueID, - transaction?: Transaction + transaction?: unknown ): Promise> { return this.repository.existsByIdInCompany(companyId, invoiceId, transaction, { is_proforma: false, @@ -186,7 +185,7 @@ export class CustomerInvoiceApplicationService { async findProformasByCriteriaInCompany( companyId: UniqueID, criteria: Criteria, - transaction?: Transaction + transaction?: unknown ): Promise, Error>> { return this.repository.findProformasByCriteriaInCompany(companyId, criteria, transaction, {}); } @@ -202,7 +201,7 @@ export class CustomerInvoiceApplicationService { async findIssuedInvoiceByCriteriaInCompany( companyId: UniqueID, criteria: Criteria, - transaction?: Transaction + transaction?: unknown ): Promise, Error>> { return this.repository.findIssuedInvoicesByCriteriaInCompany( companyId, @@ -222,7 +221,7 @@ export class CustomerInvoiceApplicationService { async getIssuedInvoiceByIdInCompany( companyId: UniqueID, invoiceId: UniqueID, - transaction?: Transaction + transaction?: unknown ): Promise> { return await this.repository.getIssuedInvoiceByIdInCompany( companyId, @@ -242,7 +241,7 @@ export class CustomerInvoiceApplicationService { async getProformaByIdInCompany( companyId: UniqueID, proformaId: UniqueID, - transaction?: Transaction + transaction?: unknown ): Promise> { return await this.repository.getProformaByIdInCompany(companyId, proformaId, transaction, {}); } @@ -261,7 +260,7 @@ export class CustomerInvoiceApplicationService { companyId: UniqueID, proformaId: UniqueID, changes: CustomerInvoicePatchProps, - transaction?: Transaction + transaction?: unknown ): Promise> { const proformaResult = await this.getProformaByIdInCompany(companyId, proformaId, transaction); @@ -289,7 +288,7 @@ export class CustomerInvoiceApplicationService { async deleteProformaByIdInCompany( companyId: UniqueID, proformaId: UniqueID, - transaction?: Transaction + transaction?: unknown ): Promise> { // 1) Buscar la proforma const proformaResult = await this.getProformaByIdInCompany(companyId, proformaId, transaction); @@ -329,7 +328,7 @@ export class CustomerInvoiceApplicationService { companyId: UniqueID, proformaId: UniqueID, newStatus: InvoiceStatus, - transaction?: Transaction + transaction?: unknown ): Promise> { return this.repository.updateProformaStatusByIdInCompany( companyId, diff --git a/modules/customer-invoices/src/api/domain/common/value-objects/invoice-amount.vo.ts b/modules/customer-invoices/src/api/domain/common/value-objects/invoice-amount.vo.ts index e9e02b8a..fc8e8030 100644 --- a/modules/customer-invoices/src/api/domain/common/value-objects/invoice-amount.vo.ts +++ b/modules/customer-invoices/src/api/domain/common/value-objects/invoice-amount.vo.ts @@ -32,9 +32,11 @@ export class InvoiceAmount extends MoneyValue { } // Ensure fluent operations keep the subclass type - convertScale(newScale: number) { - const mv = super.convertScale(newScale); - const p = mv.toPrimitive(); + roundUsingScale(intermediateScale: number) { + const scaled = super.convertScale(intermediateScale); + const normalized = scaled.convertScale(InvoiceAmount.DEFAULT_SCALE); + const p = normalized.toPrimitive(); + return new InvoiceAmount({ value: p.value, currency_code: p.currency_code, diff --git a/modules/customer-invoices/src/api/domain/common/value-objects/invoice-status.vo.ts b/modules/customer-invoices/src/api/domain/common/value-objects/invoice-status.vo.ts index 0f460e93..40da6fba 100644 --- a/modules/customer-invoices/src/api/domain/common/value-objects/invoice-status.vo.ts +++ b/modules/customer-invoices/src/api/domain/common/value-objects/invoice-status.vo.ts @@ -19,7 +19,7 @@ export enum INVOICE_STATUS { const INVOICE_TRANSITIONS: Record = { draft: [INVOICE_STATUS.SENT], sent: [INVOICE_STATUS.APPROVED, INVOICE_STATUS.REJECTED], - approved: [INVOICE_STATUS.ISSUED, INVOICE_STATUS.DRAFT], + approved: [INVOICE_STATUS.ISSUED, INVOICE_STATUS.REJECTED, INVOICE_STATUS.DRAFT], rejected: [INVOICE_STATUS.DRAFT], issued: [], }; @@ -39,34 +39,34 @@ export class InvoiceStatus extends ValueObject { return Result.ok( value === "rejected" - ? InvoiceStatus.fromRejected() + ? InvoiceStatus.rejected() : value === "sent" - ? InvoiceStatus.fromSent() + ? InvoiceStatus.sent() : value === "issued" - ? InvoiceStatus.fromIssued() + ? InvoiceStatus.issued() : value === "approved" - ? InvoiceStatus.fromApproved() - : InvoiceStatus.fromDraft() + ? InvoiceStatus.approved() + : InvoiceStatus.draft() ); } - public static fromDraft(): InvoiceStatus { + public static draft(): InvoiceStatus { return new InvoiceStatus({ value: INVOICE_STATUS.DRAFT }); } - public static fromIssued(): InvoiceStatus { + public static issued(): InvoiceStatus { return new InvoiceStatus({ value: INVOICE_STATUS.ISSUED }); } - public static fromSent(): InvoiceStatus { + public static sent(): InvoiceStatus { return new InvoiceStatus({ value: INVOICE_STATUS.SENT }); } - public static fromApproved(): InvoiceStatus { + public static approved(): InvoiceStatus { return new InvoiceStatus({ value: INVOICE_STATUS.APPROVED }); } - public static fromRejected(): InvoiceStatus { + public static rejected(): InvoiceStatus { return new InvoiceStatus({ value: INVOICE_STATUS.REJECTED }); } diff --git a/modules/customer-invoices/src/api/domain/common/value-objects/item-amount.vo.ts b/modules/customer-invoices/src/api/domain/common/value-objects/item-amount.vo.ts index 8d13e53c..7bebb40d 100644 --- a/modules/customer-invoices/src/api/domain/common/value-objects/item-amount.vo.ts +++ b/modules/customer-invoices/src/api/domain/common/value-objects/item-amount.vo.ts @@ -32,9 +32,11 @@ export class ItemAmount extends MoneyValue { } // Ensure fluent operations keep the subclass type - convertScale(newScale: number) { - const mv = super.convertScale(newScale); - const p = mv.toPrimitive(); + roundUsingScale(intermediateScale: number) { + const scaled = super.convertScale(intermediateScale); + const normalized = scaled.convertScale(ItemAmount.DEFAULT_SCALE); + const p = normalized.toPrimitive(); + return new ItemAmount({ value: p.value, currency_code: p.currency_code, diff --git a/modules/customer-invoices/src/api/domain/common/value-objects/item-discount-percentage.vo.ts b/modules/customer-invoices/src/api/domain/common/value-objects/item-discount-percentage.vo.ts deleted file mode 100644 index fb184ae5..00000000 --- a/modules/customer-invoices/src/api/domain/common/value-objects/item-discount-percentage.vo.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Percentage, type PercentageProps } from "@repo/rdx-ddd"; -import type { Result } from "@repo/rdx-utils"; - -type ItemDiscountPercentageProps = Pick; - -export class ItemDiscountPercentage extends Percentage { - static DEFAULT_SCALE = 2; - - static create({ value }: ItemDiscountPercentageProps): Result { - return Percentage.create({ - value, - scale: ItemDiscountPercentage.DEFAULT_SCALE, - }); - } - - static zero() { - return ItemDiscountPercentage.create({ value: 0 }).data; - } -} diff --git a/modules/customer-invoices/src/api/domain/issued-invoices/aggregates/issued-invoice.aggregate.ts b/modules/customer-invoices/src/api/domain/issued-invoices/aggregates/issued-invoice.aggregate.ts index 6288df13..86b8da0b 100644 --- a/modules/customer-invoices/src/api/domain/issued-invoices/aggregates/issued-invoice.aggregate.ts +++ b/modules/customer-invoices/src/api/domain/issued-invoices/aggregates/issued-invoice.aggregate.ts @@ -19,13 +19,20 @@ import type { InvoiceSerie, InvoiceStatus, } from "../../common"; -import { IssuedInvoiceItems, type IssuedInvoiceTaxes, type VerifactuRecord } from "../entities"; +import { + type IIssuedInvoiceItemCreateProps, + IssuedInvoiceItem, + IssuedInvoiceItems, + type IssuedInvoiceTaxes, + type VerifactuRecord, +} from "../entities"; +import { IssuedInvoiceItemMismatch } from "../errors"; -export interface IIssuedInvoiceProps { +export interface IIssuedInvoiceCreateProps { companyId: UniqueID; status: InvoiceStatus; - proformaId: Maybe; // <- proforma padre en caso de issue + proformaId: UniqueID; // <- id de la proforma padre en caso de issue series: Maybe; invoiceNumber: InvoiceNumber; @@ -45,7 +52,7 @@ export interface IIssuedInvoiceProps { paymentMethod: Maybe; - items: IssuedInvoiceItems; + items: IIssuedInvoiceItemCreateProps[]; taxes: IssuedInvoiceTaxes; subtotalAmount: InvoiceAmount; @@ -67,21 +74,28 @@ export interface IIssuedInvoiceProps { verifactu: Maybe; } -export class IssuedInvoice extends AggregateRoot { - private _items!: IssuedInvoiceItems; +export interface IIssuedInvoice { + companyId: UniqueID; +} - protected constructor(props: IIssuedInvoiceProps, id?: UniqueID) { +export type InternalIssuedInvoiceProps = Omit; + +export class IssuedInvoice + extends AggregateRoot + implements IIssuedInvoice +{ + private readonly _items!: IssuedInvoiceItems; + + protected constructor( + props: InternalIssuedInvoiceProps, + items: IssuedInvoiceItems, + id?: UniqueID + ) { super(props, id); - this._items = - props.items || - IssuedInvoiceItems.create({ - languageCode: props.languageCode, - currencyCode: props.currencyCode, - globalDiscountPercentage: props.globalDiscountPercentage, - }); + this._items = items; } - static create(props: IIssuedInvoiceProps, id?: UniqueID): Result { + static create(props: IIssuedInvoiceCreateProps, id?: UniqueID): Result { if (!props.recipient) { return Result.fail( new DomainValidationError( @@ -92,7 +106,21 @@ export class IssuedInvoice extends AggregateRoot { ); } - const issuedInvoice = new IssuedInvoice(props, id); + const internalItems = IssuedInvoiceItems.create({ + items: [], + languageCode: props.languageCode, + currencyCode: props.currencyCode, + globalDiscountPercentage: props.globalDiscountPercentage, + }); + + const { items, ...internalProps } = props; + const issuedInvoice = new IssuedInvoice(internalProps, internalItems, id); + + const initializeResult = issuedInvoice.initializeItems(items); + + if (initializeResult.isFailure) { + return Result.fail(initializeResult.error); + } // Reglas de negocio / validaciones // ... @@ -104,6 +132,32 @@ export class IssuedInvoice extends AggregateRoot { return Result.ok(issuedInvoice); } + // Rehidratación desde persistencia + static rehydrate( + props: InternalIssuedInvoiceProps, + items: IssuedInvoiceItems, + id: UniqueID + ): IssuedInvoice { + return new IssuedInvoice(props, items, id); + } + + private initializeItems(itemsProps: IIssuedInvoiceItemCreateProps[]): Result { + for (const [index, itemProps] of itemsProps.entries()) { + const itemResult = IssuedInvoiceItem.create(itemProps); + + if (itemResult.isFailure) { + return Result.fail(itemResult.error); + } + + const added = this._items.add(itemResult.data); + + if (!added) { + return Result.fail(new IssuedInvoiceItemMismatch(index)); + } + } + return Result.ok(); + } + // Getters public get companyId(): UniqueID { @@ -114,7 +168,7 @@ export class IssuedInvoice extends AggregateRoot { return this.props.customerId; } - public get proformaId(): Maybe { + public get proformaId(): UniqueID { return this.props.proformaId; } @@ -230,8 +284,4 @@ export class IssuedInvoice extends AggregateRoot { public get hasPaymentMethod() { return this.paymentMethod.isSome(); } - - public getProps(): IIssuedInvoiceProps { - return this.props; - } } diff --git a/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-items/issued-invoice-item.entity.ts b/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-items/issued-invoice-item.entity.ts index 50963e76..69d509b9 100644 --- a/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-items/issued-invoice-item.entity.ts +++ b/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-items/issued-invoice-item.entity.ts @@ -14,7 +14,7 @@ import type { ItemAmount, ItemDescription, ItemQuantity } from "../../../common" * Todos los importes están previamente calculados y congelados. */ -export type IssuedInvoiceItemProps = { +export interface IIssuedInvoiceItemCreateProps { description: Maybe; quantity: Maybe; @@ -49,18 +49,55 @@ export type IssuedInvoiceItemProps = { languageCode: LanguageCode; currencyCode: CurrencyCode; -}; - -export interface IIssuedInvoiceItem extends IssuedInvoiceItemProps { - isValued(): boolean; // Indica si el item tiene cantidad o precio (o ambos) para ser considerado "valorizado" } +export interface IIssuedInvoiceItem { + isValued(): boolean; // Indica si el item tiene cantidad o precio (o ambos) para ser considerado "valorizado" + + description: Maybe; + + quantity: Maybe; + unitAmount: Maybe; + + subtotalAmount: ItemAmount; + + itemDiscountPercentage: Maybe; + itemDiscountAmount: ItemAmount; + + globalDiscountPercentage: DiscountPercentage; + globalDiscountAmount: ItemAmount; + + totalDiscountAmount: ItemAmount; + + taxableAmount: ItemAmount; + + ivaCode: Maybe; + ivaPercentage: Maybe; + ivaAmount: ItemAmount; + + recCode: Maybe; + recPercentage: Maybe; + recAmount: ItemAmount; + + retentionCode: Maybe; + retentionPercentage: Maybe; + retentionAmount: ItemAmount; + + taxesAmount: ItemAmount; + totalAmount: ItemAmount; + + languageCode: LanguageCode; + currencyCode: CurrencyCode; +} + +type InternalIssuedInvoiceItemProps = IIssuedInvoiceItemCreateProps; + export class IssuedInvoiceItem - extends DomainEntity + extends DomainEntity implements IIssuedInvoiceItem { public static create( - props: IssuedInvoiceItemProps, + props: IIssuedInvoiceItemCreateProps, id?: UniqueID ): Result { const item = new IssuedInvoiceItem(props, id); @@ -72,7 +109,11 @@ export class IssuedInvoiceItem return Result.ok(item); } - protected constructor(props: IssuedInvoiceItemProps, id?: UniqueID) { + static rehydrate(props: InternalIssuedInvoiceItemProps, id: UniqueID): IssuedInvoiceItem { + return new IssuedInvoiceItem(props, id); + } + + protected constructor(props: InternalIssuedInvoiceItemProps, id?: UniqueID) { super(props, id); } @@ -163,7 +204,7 @@ export class IssuedInvoiceItem return this.props.totalAmount; } - getProps(): IssuedInvoiceItemProps { + getProps(): IIssuedInvoiceItemCreateProps { return this.props; } diff --git a/modules/customer-invoices/src/api/domain/issued-invoices/errors/index.ts b/modules/customer-invoices/src/api/domain/issued-invoices/errors/index.ts index e69de29b..d93f3c65 100644 --- a/modules/customer-invoices/src/api/domain/issued-invoices/errors/index.ts +++ b/modules/customer-invoices/src/api/domain/issued-invoices/errors/index.ts @@ -0,0 +1 @@ +export * from "./issued-invoice-item-not-valid-error"; diff --git a/modules/customer-invoices/src/api/domain/issued-invoices/errors/issued-invoice-item-not-valid-error.ts b/modules/customer-invoices/src/api/domain/issued-invoices/errors/issued-invoice-item-not-valid-error.ts new file mode 100644 index 00000000..431fc193 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/issued-invoices/errors/issued-invoice-item-not-valid-error.ts @@ -0,0 +1,32 @@ +import { DomainError } from "@repo/rdx-ddd"; + +/** + * Error de dominio que indica que al añadir un nuevo item a la lista + * de detalles, este no tiene el mismo "currencyCode" y "languageCode" + * que la colección de items. + * + */ +export class IssuedInvoiceItemMismatch extends DomainError { + /** + * Crea una instancia del error con el identificador del item. + * + * @param position - Posición del item + * @param options - Opciones nativas de Error (puedes pasar `cause`). + */ + constructor(position: number, options?: ErrorOptions) { + super( + `Error. IssuedInvoice item with position '${position}' rejected due to currency/language mismatch.`, + options + ); + this.name = "IssuedInvoiceItemMismatch"; + } +} + +/** + * *Type guard* para `IssuedInvoiceItemNotValid`. + * + * @param e - Error desconocido + * @returns `true` si `e` es `IssuedInvoiceItemNotValid` + */ +export const isIssuedInvoiceItemMismatch = (e: unknown): e is IssuedInvoiceItemMismatch => + e instanceof IssuedInvoiceItemMismatch; diff --git a/modules/customer-invoices/src/api/domain/issued-invoices/index.ts b/modules/customer-invoices/src/api/domain/issued-invoices/index.ts index 59e9091f..38d1c552 100644 --- a/modules/customer-invoices/src/api/domain/issued-invoices/index.ts +++ b/modules/customer-invoices/src/api/domain/issued-invoices/index.ts @@ -1,3 +1,4 @@ export * from "./aggregates"; export * from "./entities"; +export * from "./errors"; export * from "./value-objects"; diff --git a/modules/customer-invoices/src/api/domain/proformas/aggregates/proforma.aggregate.ts b/modules/customer-invoices/src/api/domain/proformas/aggregates/proforma.aggregate.ts index 9c09dd99..b1786f27 100644 --- a/modules/customer-invoices/src/api/domain/proformas/aggregates/proforma.aggregate.ts +++ b/modules/customer-invoices/src/api/domain/proformas/aggregates/proforma.aggregate.ts @@ -17,7 +17,7 @@ import { type InvoiceNumber, type InvoiceRecipient, type InvoiceSerie, - type InvoiceStatus, + InvoiceStatus, type ItemAmount, } from "../../common/value-objects"; import { @@ -27,7 +27,7 @@ import { ProformaItems, } from "../entities"; import { ProformaItemMismatch } from "../errors"; -import { type IProformaTaxTotals, ProformaTaxesCalculator } from "../services"; +import type { IProformaTaxTotals } from "../services"; import { ProformaItemTaxes } from "../value-objects"; export interface IProformaCreateProps { @@ -249,7 +249,7 @@ export class Proforma extends AggregateRoot implements IP } public issue(): Result { - if (!this.props.status.canTransitionTo("ISSUED")) { + if (!this.props.status.canTransitionTo("issued")) { return Result.fail( new DomainValidationError( "INVALID_STATE", @@ -259,8 +259,7 @@ export class Proforma extends AggregateRoot implements IP ); } - // Falta - //this.props.status = this.props.status.canTransitionTo("ISSUED"); + this.props.status = InvoiceStatus.issued(); return Result.ok(); } @@ -292,7 +291,7 @@ export class Proforma extends AggregateRoot implements IP } public taxes(): Collection { - return new ProformaTaxesCalculator(this.items).calculate(); + return this.items.taxes(); } public addItem(props: IProformaItemCreateProps): Result { diff --git a/modules/customer-invoices/src/api/domain/proformas/entities/proforma-items/proforma-items.collection.ts b/modules/customer-invoices/src/api/domain/proformas/entities/proforma-items/proforma-items.collection.ts index f07b86ed..6561cee3 100644 --- a/modules/customer-invoices/src/api/domain/proformas/entities/proforma-items/proforma-items.collection.ts +++ b/modules/customer-invoices/src/api/domain/proformas/entities/proforma-items/proforma-items.collection.ts @@ -3,6 +3,7 @@ import type { CurrencyCode, LanguageCode } from "@repo/rdx-ddd"; import { Collection, Result } from "@repo/rdx-utils"; import { ProformaItemMismatch } from "../../errors"; +import { type IProformaTaxTotals, ProformaTaxesCalculator } from "../../services"; import { ProformaItemsTotalsCalculator } from "../../services/proforma-items-totals-calculator"; import type { IProformaItem, IProformaItemTotals, ProformaItem } from "./proforma-item.entity"; @@ -37,6 +38,7 @@ export interface IProformaItems { valued(): IProformaItem[]; // Devuelve solo las líneas valoradas. totals(): IProformaItemTotals; + taxes(): Collection; readonly globalDiscountPercentage: DiscountPercentage; // % descuento de la cabecera readonly languageCode: LanguageCode; // Para formateos específicos de idioma @@ -57,6 +59,7 @@ export class ProformaItems extends Collection implements IProforma if (this.items.length > 0) { this.ensureSameContext(this.items); } + 4; } static create(props: IProformaItemsProps): ProformaItems { @@ -113,6 +116,10 @@ export class ProformaItems extends Collection implements IProforma return new ProformaItemsTotalsCalculator(this).calculate(); } + public taxes(): Collection { + return new ProformaTaxesCalculator(this).calculate(); + } + private ensureSameContext(items: IProformaItem[]): void { for (const item of items) { const same = diff --git a/modules/customer-invoices/src/api/domain/proformas/services/proforma-items-totals-calculator.ts b/modules/customer-invoices/src/api/domain/proformas/services/proforma-items-totals-calculator.ts index 7d8fea22..0333d01a 100644 --- a/modules/customer-invoices/src/api/domain/proformas/services/proforma-items-totals-calculator.ts +++ b/modules/customer-invoices/src/api/domain/proformas/services/proforma-items-totals-calculator.ts @@ -1,5 +1,5 @@ import { ItemAmount } from "../../common"; -import type { IProformaItems } from "../entities"; +import type { ProformaItems } from "../entities"; export interface IProformaItemsTotals { subtotalAmount: ItemAmount; @@ -23,7 +23,7 @@ export interface IProformaItemsTotals { * La lógica fiscal está en ProformaItem; aquí solo se agregan resultados. */ export class ProformaItemsTotalsCalculator { - constructor(private readonly items: IProformaItems) {} + constructor(private readonly items: ProformaItems) {} public calculate(): IProformaItemsTotals { const zero = ItemAmount.zero(this.items.currencyCode.code); @@ -47,26 +47,28 @@ export class ProformaItemsTotalsCalculator { const amounts = item.totals(); // Subtotales - subtotalAmount = subtotalAmount.add(amounts.subtotalAmount); + subtotalAmount = subtotalAmount.add(amounts.subtotalAmount.roundUsingScale(2)); // Descuentos - itemDiscountAmount = itemDiscountAmount.add(amounts.itemDiscountAmount); - globalDiscountAmount = globalDiscountAmount.add(amounts.globalDiscountAmount); - totalDiscountAmount = totalDiscountAmount.add(amounts.totalDiscountAmount); + itemDiscountAmount = itemDiscountAmount.add(amounts.itemDiscountAmount.roundUsingScale(2)); + globalDiscountAmount = globalDiscountAmount.add( + amounts.globalDiscountAmount.roundUsingScale(2) + ); + totalDiscountAmount = totalDiscountAmount.add(amounts.totalDiscountAmount.roundUsingScale(2)); // Base imponible - taxableAmount = taxableAmount.add(amounts.taxableAmount); + taxableAmount = taxableAmount.add(amounts.taxableAmount.roundUsingScale(2)); // Impuestos individuales - ivaAmount = ivaAmount.add(amounts.ivaAmount); - recAmount = recAmount.add(amounts.recAmount); - retentionAmount = retentionAmount.add(amounts.retentionAmount); + ivaAmount = ivaAmount.add(amounts.ivaAmount.roundUsingScale(2)); + recAmount = recAmount.add(amounts.recAmount.roundUsingScale(2)); + retentionAmount = retentionAmount.add(amounts.retentionAmount.roundUsingScale(2)); // Total impuestos del ítem - taxesAmount = taxesAmount.add(amounts.taxesAmount); + taxesAmount = taxesAmount.add(amounts.taxesAmount.roundUsingScale(2)); // Total final del ítem - totalAmount = totalAmount.add(amounts.totalAmount); + totalAmount = totalAmount.add(amounts.totalAmount.roundUsingScale(2)); } return { diff --git a/modules/customer-invoices/src/api/domain/proformas/services/proforma-taxes-calculator.ts b/modules/customer-invoices/src/api/domain/proformas/services/proforma-taxes-calculator.ts index 98ab6039..1288292b 100644 --- a/modules/customer-invoices/src/api/domain/proformas/services/proforma-taxes-calculator.ts +++ b/modules/customer-invoices/src/api/domain/proformas/services/proforma-taxes-calculator.ts @@ -29,8 +29,8 @@ export class ProformaTaxesCalculator { constructor(private readonly items: IProformaItems) {} public calculate(): Collection { - const groups = proformaComputeTaxGroups(this.items); - const currencyCode = this.items.currencyCode; + const groups = proformaComputeTaxGroups(this.items); // <- devuelve en escala 4 + //const currencyCode = this.items.currencyCode; const rows = Array.from(groups.values()).map((g) => { const taxableAmount = this.toInvoiceAmount(g.taxableAmount); 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 248cde11..ce9a97b2 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 @@ -67,7 +67,7 @@ export class IssueCustomerInvoiceDomainService { ...proformaProps, isProforma: false, proformaId: Maybe.some(proforma.id), - status: InvoiceStatus.fromIssued(), + status: InvoiceStatus.issued(), invoiceNumber: issueNumber, invoiceDate: issueDate, description: proformaProps.description.isNone() ? Maybe.some(".") : proformaProps.description, diff --git a/modules/customer-invoices/src/api/index.ts b/modules/customer-invoices/src/api/index.ts index 38952a1c..6cd031a9 100644 --- a/modules/customer-invoices/src/api/index.ts +++ b/modules/customer-invoices/src/api/index.ts @@ -1,21 +1,15 @@ import type { IModuleServer } from "@erp/core/api"; import { - type IssuedInvoicePublicServices, - type IssuedInvoicesInternalDeps, - type ProformaPublicServices, - type ProformasInternalDeps, - buildIssuedInvoiceServices, + buildIssuedInvoicePublicServices, buildIssuedInvoicesDependencies, - buildProformaServices, + buildProformaPublicServices, buildProformasDependencies, issuedInvoicesRouter, models, proformasRouter, } from "./infrastructure"; -export type { IssuedInvoicePublicServices, ProformaPublicServices }; - export const customerInvoicesAPIModule: IModuleServer = { name: "customer-invoices", version: "1.0.0", @@ -36,14 +30,8 @@ export const customerInvoicesAPIModule: IModuleServer = { const proformasInternal = buildProformasDependencies(params); // 2) Servicios públicos (Application Services) - const issuedInvoicesServices: IssuedInvoicePublicServices = buildIssuedInvoiceServices( - params, - issuedInvoicesInternal - ); - const proformasServices: ProformaPublicServices = buildProformaServices( - params, - proformasInternal - ); + const issuedInvoicesServices = buildIssuedInvoicePublicServices(params, issuedInvoicesInternal); + const proformasServices = buildProformaPublicServices(params, proformasInternal); logger.info("🚀 CustomerInvoices module dependencies registered", { label: this.name }); @@ -73,22 +61,11 @@ export const customerInvoicesAPIModule: IModuleServer = { * - NO construye dominio */ async start(params) { - const { app, baseRoutePath, logger, getInternal } = params; - - // Recuperamos el dominio interno del módulo - const issuedInvoicesInternalDeps = getInternal( - "customer-invoices", - "issuedInvoices" - ); - - const proformasInternalDeps = getInternal( - "customer-invoices", - "proformas" - ); + const { app, baseRoutePath, logger, getInternal, getService } = params; // Registro de rutas HTTP - issuedInvoicesRouter(params, issuedInvoicesInternalDeps); - proformasRouter(params, proformasInternalDeps); + issuedInvoicesRouter(params); + proformasRouter(params); logger.info("🚀 CustomerInvoices module started", { label: this.name, diff --git a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/customer-invoice-item.mapper.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/customer-invoice-item.mapper.ts index 7b6358ab..2c425240 100644 --- a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/customer-invoice-item.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/customer-invoice-item.mapper.ts @@ -1,5 +1,6 @@ import type { JsonTaxCatalogProvider } from "@erp/core"; import { + DiscountPercentage, type ISequelizeDomainMapper, type MapperParamsType, SequelizeDomainMapper, @@ -18,18 +19,11 @@ import { Result } from "@repo/rdx-utils"; import { type IProformaCreateProps, IssuedInvoiceItem, - type IssuedInvoiceItemProps, ItemAmount, ItemDescription, - ItemDiscountPercentage, ItemQuantity, - ItemTaxGroup, type Proforma, } from "../../../../../../domain"; -import type { - CustomerInvoiceItemCreationAttributes, - CustomerInvoiceItemModel, -} from "../../../../sequelize"; export interface ICustomerInvoiceItemDomainMapper extends ISequelizeDomainMapper< @@ -99,7 +93,7 @@ export class CustomerInvoiceItemDomainMapper const discountPercentage = extractOrPushError( maybeFromNullableResult(source.discount_percentage_value, (v) => - ItemDiscountPercentage.create({ value: v }) + DiscountPercentage.create({ value: v }) ), `items[${index}].discount_percentage`, errors @@ -107,7 +101,7 @@ export class CustomerInvoiceItemDomainMapper const globalDiscountPercentage = extractOrPushError( maybeFromNullableResult(source.global_discount_percentage_value, (v) => - ItemDiscountPercentage.create({ value: v }) + DiscountPercentage.create({ value: v }) ), `items[${index}].discount_percentage`, errors @@ -242,7 +236,7 @@ export class CustomerInvoiceItemDomainMapper ), discount_percentage_scale: maybeToNullable(source.itemDiscountPercentage, (v) => v.toPrimitive().scale) ?? - ItemDiscountPercentage.DEFAULT_SCALE, + DiscountPercentage.DEFAULT_SCALE, discount_amount_value: allAmounts.itemDiscountAmount.value, discount_amount_scale: allAmounts.itemDiscountAmount.scale, @@ -255,7 +249,7 @@ export class CustomerInvoiceItemDomainMapper global_discount_percentage_scale: maybeToNullable(source.globalDiscountPercentage, (v) => v.toPrimitive().scale) ?? - ItemDiscountPercentage.DEFAULT_SCALE, + DiscountPercentage.DEFAULT_SCALE, global_discount_amount_value: allAmounts.globalDiscountAmount.value, global_discount_amount_scale: allAmounts.globalDiscountAmount.scale, diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/index.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/index.ts index 7a95685d..849cce19 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/index.ts @@ -1,2 +1,3 @@ +export * from "./issued-invoice-number-generator.di"; export * from "./issued-invoice-public-services"; export * from "./issued-invoices.di"; diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoice-number-generator.di.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoice-number-generator.di.ts new file mode 100644 index 00000000..4d3bddf2 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoice-number-generator.di.ts @@ -0,0 +1,5 @@ +import type { IIssuedInvoiceNumberGenerator } from "../../../application"; +import { SequelizeIssuedInvoiceNumberGenerator } from "../persistence"; + +export const buildIssuedInvoiceNumberGenerator = (): IIssuedInvoiceNumberGenerator => + new SequelizeIssuedInvoiceNumberGenerator(); diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoice-public-services.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoice-public-services.ts index 46fd31f0..027a728a 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoice-public-services.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoice-public-services.ts @@ -1,26 +1,27 @@ import type { SetupParams } from "@erp/core/api"; import { buildCatalogs, buildTransactionManager } from "@erp/core/api"; +import type { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; import { + type IIssuedInvoiceCreatorParams, + type IIssuedInvoicePublicServices, + type IIssuedInvoiceServicesContext, + buildIssuedInvoiceCreator, buildIssuedInvoiceFinder, buildIssuedInvoiceSnapshotBuilders, } from "../../../application/issued-invoices"; import { buildIssuedInvoiceDocumentService } from "./issued-invoice-documents.di"; +import { buildIssuedInvoiceNumberGenerator } from "./issued-invoice-number-generator.di"; import { buildIssuedInvoicePersistenceMappers } from "./issued-invoice-persistence-mappers.di"; import { buildIssuedInvoiceRepository } from "./issued-invoice-repositories.di"; import type { IssuedInvoicesInternalDeps } from "./issued-invoices.di"; -export type IssuedInvoicePublicServices = { - listIssuedInvoices: (filters: unknown, context: unknown) => null; - getIssuedInvoiceById: (id: unknown, context: unknown) => null; - generateIssuedInvoiceReport: (id: unknown, options: unknown, context: unknown) => null; -}; - -export function buildIssuedInvoiceServices( +export function buildIssuedInvoicePublicServices( params: SetupParams, deps: IssuedInvoicesInternalDeps -): IssuedInvoicePublicServices { +): IIssuedInvoicePublicServices { const { database } = params; // Infrastructure @@ -29,20 +30,29 @@ export function buildIssuedInvoiceServices( const persistenceMappers = buildIssuedInvoicePersistenceMappers(catalogs); const repository = buildIssuedInvoiceRepository({ database, mappers: persistenceMappers }); + const numberService = buildIssuedInvoiceNumberGenerator(); // Application helpers + const creator = buildIssuedInvoiceCreator({ numberService, repository }); const finder = buildIssuedInvoiceFinder(repository); const snapshotBuilders = buildIssuedInvoiceSnapshotBuilders(); const documentGeneratorPipeline = buildIssuedInvoiceDocumentService(params); return { - listIssuedInvoices: (filters, context) => null, - //internal.useCases.listIssuedInvoices().execute(filters, context), + createIssuedInvoice: async ( + id: UniqueID, + props: IIssuedInvoiceCreatorParams["props"], + context: IIssuedInvoiceServicesContext + ) => { + const { transaction, companyId } = context; - getIssuedInvoiceById: (id, context) => null, - //internal.useCases.getIssuedInvoiceById().execute(id, context), + const createResult = await creator.create({ companyId, id, props, transaction }); - generateIssuedInvoiceReport: (id, options, context) => null, - //internal.useCases.reportIssuedInvoice().execute(id, options, context), + if (createResult.isFailure) { + return Result.fail(createResult.error); + } + + return Result.ok(createResult.data); + }, }; } diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/express/issued-invoices-api-error-mapper.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/express/issued-invoices-api-error-mapper.ts index 243ac245..3b721e87 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/express/issued-invoices-api-error-mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/express/issued-invoices-api-error-mapper.ts @@ -10,14 +10,9 @@ import { import { type CustomerInvoiceIdAlreadyExistsError, - type EntityIsNotProformaError, - type InvalidProformaTransitionError, - type ProformaCannotBeConvertedToInvoiceError, + type IssuedInvoiceItemMismatch, isCustomerInvoiceIdAlreadyExistsError, - isEntityIsNotProformaError, - isInvalidProformaTransitionError, - isProformaCannotBeConvertedToInvoiceError, - isProformaCannotBeDeletedError, + isIssuedInvoiceItemMismatch, } from "../../../domain"; // Crea una regla específica (prioridad alta para sobreescribir mensajes) @@ -31,47 +26,17 @@ const invoiceDuplicateRule: ErrorToApiRule = { ), }; -const entityIsNotProformaError: ErrorToApiRule = { +const issuedinvoiceItemMismatchError: ErrorToApiRule = { priority: 120, - matches: (e) => isEntityIsNotProformaError(e), + matches: (e) => isIssuedInvoiceItemMismatch(e), build: (e) => new ValidationApiError( - (e as EntityIsNotProformaError).message || "Entity with the provided id is not proforma" - ), -}; - -const proformaTransitionRule: ErrorToApiRule = { - priority: 120, - matches: (e) => isInvalidProformaTransitionError(e), - build: (e) => - new ValidationApiError( - (e as InvalidProformaTransitionError).message || "Invalid transition for proforma." - ), -}; - -const proformaConversionRule: ErrorToApiRule = { - priority: 120, - matches: (e) => isProformaCannotBeConvertedToInvoiceError(e), - build: (e) => - new ValidationApiError( - (e as ProformaCannotBeConvertedToInvoiceError).message || - "Proforma cannot be converted to an Invoice." - ), -}; - -const proformaCannotBeDeletedRule: ErrorToApiRule = { - priority: 120, - matches: (e) => isProformaCannotBeDeletedError(e), - build: (e) => - new ValidationApiError( - (e as ProformaCannotBeConvertedToInvoiceError).message || "Proforma cannot be deleted." + (e as IssuedInvoiceItemMismatch).message || + "IssuedInvoice item rejected due to currency/language mismatch" ), }; // 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(proformaCannotBeDeletedRule) - .register(proformaTransitionRule); + .register(issuedinvoiceItemMismatchError); diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/express/issued-invoices.routes.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/express/issued-invoices.routes.ts index 34e3ff0a..c875ef71 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/express/issued-invoices.routes.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/express/issued-invoices.routes.ts @@ -1,5 +1,5 @@ import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth/api"; -import { type ModuleParams, type RequestWithAuth, validateRequest } from "@erp/core/api"; +import { type RequestWithAuth, type StartParams, validateRequest } from "@erp/core/api"; import { type NextFunction, type Request, type Response, Router } from "express"; import { @@ -16,11 +16,15 @@ import { ReportIssuedInvoiceController, } from "./controllers"; -export const issuedInvoicesRouter = (params: ModuleParams, deps: IssuedInvoicesInternalDeps) => { - const { app, config } = params; +export const issuedInvoicesRouter = (params: StartParams) => { + const { app, config, getService, getInternal } = params; + + const deps = getInternal("customer-invoices", "issuedInvoices"); const router: Router = Router({ mergeParams: true }); + // ---------------------------------------------- + if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "production") { // 🔐 Autenticación + Tenancy para TODO el router router.use( diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/index.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/index.ts index bad13e0e..0c2e706a 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/index.ts @@ -1,2 +1,3 @@ export * from "./mappers"; export * from "./repositories"; +export * from "./services"; diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-domain.mapper.ts index 8312e874..3a9819d3 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-domain.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-domain.mapper.ts @@ -14,7 +14,7 @@ import { import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils"; import { - type IIssuedInvoiceProps, + type InternalIssuedInvoiceProps, InvoiceAmount, InvoiceNumber, InvoicePaymentMethod, @@ -63,8 +63,9 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper< const customerId = extractOrPushError(UniqueID.create(raw.customer_id), "customer_id", errors); + // Para issued invoices, proforma_id debe estar relleno const proformaId = extractOrPushError( - maybeFromNullableResult(raw.proforma_id, (v) => UniqueID.create(v)), + UniqueID.create(String(raw.proforma_id)), "proforma_id", errors ); @@ -346,7 +347,7 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper< currencyCode: attributes.currencyCode!, }); - const invoiceProps: IIssuedInvoiceProps = { + const invoiceProps: InternalIssuedInvoiceProps = { companyId: attributes.companyId!, proformaId: attributes.proformaId!, @@ -383,22 +384,14 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper< paymentMethod: attributes.paymentMethod!, - items, taxes, verifactu, }; - const createResult = IssuedInvoice.create(invoiceProps, attributes.invoiceId); + const invoiceId = attributes.invoiceId!; + const invoice = IssuedInvoice.rehydrate(invoiceProps, items, invoiceId); - if (createResult.isFailure) { - return Result.fail( - new ValidationErrorCollection("Customer invoice entity creation failed", [ - { path: "invoice", message: createResult.error.message }, - ]) - ); - } - - return Result.ok(createResult.data); + return Result.ok(invoice); } catch (err: unknown) { return Result.fail(err as Error); } @@ -471,7 +464,7 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper< // Flags / estado / serie / número is_proforma: false, status: source.status.toPrimitive(), - proforma_id: maybeToNullable(source.proformaId, (v) => v.toPrimitive()), + proforma_id: source.proformaId.toPrimitive(), series: maybeToNullable(source.series, (v) => v.toPrimitive()), invoice_number: source.invoiceNumber.toPrimitive(), diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-item-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-item-domain.mapper.ts index ad2aed4f..17248d7c 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-item-domain.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-item-domain.mapper.ts @@ -1,5 +1,10 @@ import type { JsonTaxCatalogProvider } from "@erp/core"; -import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api"; +import { + DiscountPercentage, + type MapperParamsType, + SequelizeDomainMapper, + TaxPercentage, +} from "@erp/core/api"; import { UniqueID, ValidationErrorCollection, @@ -13,16 +18,13 @@ import { import { Result } from "@repo/rdx-utils"; import { - DiscountPercentage, - type IIssuedInvoiceProps, + type IIssuedInvoiceCreateProps, + type IIssuedInvoiceItemCreateProps, type IssuedInvoice, IssuedInvoiceItem, - type IssuedInvoiceItemProps, ItemAmount, ItemDescription, - ItemDiscountPercentage, ItemQuantity, - ItemTaxPercentage, } from "../../../../../../domain"; import type { CustomerInvoiceItemCreationAttributes, @@ -34,7 +36,7 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe CustomerInvoiceItemCreationAttributes, IssuedInvoiceItem > { - private taxCatalog!: JsonTaxCatalogProvider; + private readonly taxCatalog!: JsonTaxCatalogProvider; constructor(params: MapperParamsType) { super(); @@ -52,11 +54,11 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe private mapAttributesToDomain( raw: CustomerInvoiceItemModel, params?: MapperParamsType - ): Partial & { itemId?: UniqueID } { + ): Partial & { itemId?: UniqueID } { const { errors, index, attributes } = params as { index: number; errors: ValidationErrorDetail[]; - attributes: Partial; + attributes: Partial; }; const itemId = extractOrPushError( @@ -96,7 +98,7 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe const itemDiscountPercentage = extractOrPushError( maybeFromNullableResult(raw.item_discount_percentage_value, (v) => - ItemDiscountPercentage.create({ value: v }) + DiscountPercentage.create({ value: v }) ), `items[${index}].item_discount_percentage_value`, errors @@ -112,9 +114,7 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe ); const globalDiscountPercentage = extractOrPushError( - maybeFromNullableResult(raw.global_discount_percentage_value, (v) => - DiscountPercentage.create({ value: v }) - ), + DiscountPercentage.create({ value: raw.global_discount_percentage_value }), `items[${index}].global_discount_percentage_value`, errors ); @@ -149,9 +149,7 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe const ivaCode = maybeFromNullableOrEmptyString(raw.iva_code); const ivaPercentage = extractOrPushError( - maybeFromNullableResult(raw.iva_percentage_value, (value) => - ItemTaxPercentage.create({ value }) - ), + maybeFromNullableResult(raw.iva_percentage_value, (value) => TaxPercentage.create({ value })), `items[${index}].iva_percentage_value`, errors ); @@ -168,9 +166,7 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe const recCode = maybeFromNullableOrEmptyString(raw.rec_code); const recPercentage = extractOrPushError( - maybeFromNullableResult(raw.rec_percentage_value, (value) => - ItemTaxPercentage.create({ value }) - ), + maybeFromNullableResult(raw.rec_percentage_value, (value) => TaxPercentage.create({ value })), `items[${index}].rec_percentage_value`, errors ); @@ -188,7 +184,7 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe const retentionPercentage = extractOrPushError( maybeFromNullableResult(raw.retention_percentage_value, (value) => - ItemTaxPercentage.create({ value }) + TaxPercentage.create({ value }) ), `items[${index}].retention_percentage_value`, errors @@ -263,7 +259,7 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe const { errors, index } = params as { index: number; errors: ValidationErrorDetail[]; - attributes: Partial; + attributes: Partial; }; // 1) Valores escalares (atributos generales) @@ -277,7 +273,8 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe } // 2) Construcción del elemento de dominio - const createResult = IssuedInvoiceItem.create( + const itemId = attributes.itemId!; + const newItem = IssuedInvoiceItem.rehydrate( { description: attributes.description!, @@ -314,18 +311,10 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe languageCode: attributes.languageCode!, currencyCode: attributes.currencyCode!, }, - attributes.itemId + itemId ); - if (createResult.isFailure) { - return Result.fail( - new ValidationErrorCollection("Invoice item entity creation failed", [ - { path: `items[${index}]`, message: createResult.error.message }, - ]) - ); - } - - return createResult; + return Result.ok(newItem); } public mapToPersistence( @@ -364,18 +353,14 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe ), item_discount_percentage_scale: maybeToNullable(source.itemDiscountPercentage, (v) => v.toPrimitive().scale) ?? - ItemDiscountPercentage.DEFAULT_SCALE, + DiscountPercentage.DEFAULT_SCALE, item_discount_amount_value: source.itemDiscountAmount.toPrimitive().value, item_discount_amount_scale: source.itemDiscountAmount.toPrimitive().scale, - global_discount_percentage_value: maybeToNullable( - source.globalDiscountPercentage, - (v) => v.toPrimitive().value - ), + global_discount_percentage_value: source.globalDiscountPercentage.toPrimitive().value, global_discount_percentage_scale: - maybeToNullable(source.globalDiscountPercentage, (v) => v.toPrimitive().scale) ?? - ItemDiscountPercentage.DEFAULT_SCALE, + source.globalDiscountPercentage.toPrimitive().scale ?? DiscountPercentage.DEFAULT_SCALE, global_discount_amount_value: source.globalDiscountAmount.value, global_discount_amount_scale: source.globalDiscountAmount.scale, diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-recipient-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-recipient-domain.mapper.ts index 4b64b20a..3a0ab89c 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-recipient-domain.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-recipient-domain.mapper.ts @@ -16,7 +16,7 @@ import { import { Maybe, Result } from "@repo/rdx-utils"; import { - type IIssuedInvoiceProps, + type IIssuedInvoiceCreateProps, InvoiceRecipient, type IssuedInvoice, } from "../../../../../../domain"; @@ -33,7 +33,7 @@ export class SequelizeIssuedInvoiceRecipientDomainMapper { const { errors, attributes } = params as { errors: ValidationErrorDetail[]; - attributes: Partial; + attributes: Partial; }; const _name = source.customer_name!; diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-taxes-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-taxes-domain.mapper.ts index 7fc75e20..d35130d5 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-taxes-domain.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-taxes-domain.mapper.ts @@ -1,5 +1,10 @@ import type { JsonTaxCatalogProvider } from "@erp/core"; -import { type MapperParamsType, SequelizeDomainMapper, TaxPercentage } from "@erp/core/api"; +import { + DiscountPercentage, + type MapperParamsType, + SequelizeDomainMapper, + TaxPercentage, +} from "@erp/core/api"; import { Percentage, UniqueID, @@ -14,7 +19,7 @@ import { import { Result } from "@repo/rdx-utils"; import { - type IIssuedInvoiceProps, + type IIssuedInvoiceCreateProps, InvoiceAmount, type IssuedInvoice, IssuedInvoiceTax, @@ -64,7 +69,7 @@ export class SequelizeIssuedInvoiceTaxesDomainMapper extends SequelizeDomainMapp const { errors, index, attributes } = params as { index: number; errors: ValidationErrorDetail[]; - attributes: Partial; + attributes: Partial; }; const taxableAmount = extractOrPushError( @@ -78,9 +83,10 @@ export class SequelizeIssuedInvoiceTaxesDomainMapper extends SequelizeDomainMapp const ivaCode = raw.iva_code; + // Una issued invoice debe traer IVA const ivaPercentage = extractOrPushError( TaxPercentage.create({ - value: raw.iva_percentage_value, + value: Number(raw.iva_percentage_value), }), `taxes[${index}].iva_percentage_value`, errors @@ -210,7 +216,7 @@ export class SequelizeIssuedInvoiceTaxesDomainMapper extends SequelizeDomainMapp rec_percentage_value: maybeToNullable(source.recPercentage, (v) => v.toPrimitive().value), rec_percentage_scale: maybeToNullable(source.recPercentage, (v) => v.toPrimitive().scale) ?? - ItemDiscountPercentage.DEFAULT_SCALE, + DiscountPercentage.DEFAULT_SCALE, rec_amount_value: source.recAmount.toPrimitive().value, rec_amount_scale: source.recAmount.toPrimitive().scale ?? ItemAmount.DEFAULT_SCALE, diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-verifactu-record-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-verifactu-record-domain.mapper.ts index d1b43afa..ff110981 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-verifactu-record-domain.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-verifactu-record-domain.mapper.ts @@ -12,7 +12,7 @@ import { import { Maybe, Result } from "@repo/rdx-utils"; import { - type IIssuedInvoiceProps, + type IIssuedInvoiceCreateProps, type IssuedInvoice, VerifactuRecord, VerifactuRecordEstado, @@ -33,7 +33,7 @@ export class SequelizeIssuedInvoiceVerifactuDomainMapper extends SequelizeDomain ): Result, Error> { const { errors, attributes } = params as { errors: ValidationErrorDetail[]; - attributes: Partial; + attributes: Partial; }; if (!source) { diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/services/index.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/services/index.ts new file mode 100644 index 00000000..da9443af --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/services/index.ts @@ -0,0 +1 @@ +export * from "./sequelize-issued-invoice-number-generator.service"; diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/services/sequelize-issued-invoice-number-generator.service.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/services/sequelize-issued-invoice-number-generator.service.ts new file mode 100644 index 00000000..77bfb192 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/services/sequelize-issued-invoice-number-generator.service.ts @@ -0,0 +1,66 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import { type Maybe, Result } from "@repo/rdx-utils"; +import { type Transaction, type WhereOptions, literal } from "sequelize"; + +import type { IIssuedInvoiceNumberGenerator } from "../../../../../application/issued-invoices"; +import { InvoiceNumber, type InvoiceSerie } from "../../../../../domain"; +import { CustomerInvoiceModel } from "../../../../common/persistence"; + +/** + * Generador de números de factura + */ +export class SequelizeIssuedInvoiceNumberGenerator implements IIssuedInvoiceNumberGenerator { + public async getNextForCompany( + 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 = "0001"; // 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(4, "0"); + } + + const numberResult = InvoiceNumber.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/customer-invoices/src/api/infrastructure/proformas/di/proforma-public-services.ts b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-public-services.ts index 2236f449..e12fc321 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-public-services.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-public-services.ts @@ -23,13 +23,6 @@ type ProformaServicesContext = { }; export type ProformaPublicServices = { - createProforma: ( - id: UniqueID, - props: IProformaCreatorParams["props"], - context: ProformaServicesContext - ) => Promise>; - - listProformas: (filters: unknown, context: unknown) => null; getProformaById: ( id: UniqueID, context: ProformaServicesContext @@ -40,10 +33,14 @@ export type ProformaPublicServices = { context: ProformaServicesContext ) => Promise>; - generateProformaReport: (id: unknown, options: unknown, context: unknown) => null; + createProforma: ( + id: UniqueID, + props: IProformaCreatorParams["props"], + context: ProformaServicesContext + ) => Promise>; }; -export function buildProformaServices( +export function buildProformaPublicServices( params: SetupParams, deps: ProformasInternalDeps ): ProformaPublicServices { @@ -78,7 +75,6 @@ export function buildProformaServices( return Result.ok(createResult.data); }, - listProformas: (filters, context) => null, //internal.useCases.listProformas().execute(filters, context), getProformaById: async (id: UniqueID, context: ProformaServicesContext) => { @@ -105,7 +101,7 @@ export function buildProformaServices( return Result.ok(fullSnapshot); }, - generateProformaReport: (id, options, context) => null, + //generateProformaReport: (id, options, context) => null, //internal.useCases.reportProforma().execute(id, options, context), }; } diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts b/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts index b5f1528e..3397fd07 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts @@ -3,15 +3,20 @@ import { type ModuleParams, buildCatalogs, buildTransactionManager } from "@erp/ import { type CreateProformaUseCase, type GetProformaByIdUseCase, + type IIssuedInvoicePublicServices, + type IssueProformaUseCase, type ListProformasUseCase, type ReportProformaUseCase, buildCreateProformaUseCase, buildGetProformaByIdUseCase, + buildIssueProformaUseCase, buildListProformasUseCase, buildProformaCreator, buildProformaFinder, buildProformaInputMappers, + buildProformaIssuer, buildProformaSnapshotBuilders, + buildProformaToIssuedInvoicePropsConverter, buildReportProformaUseCase, } from "../../../application"; @@ -26,6 +31,9 @@ export type ProformasInternalDeps = { getProformaById: () => GetProformaByIdUseCase; reportProforma: () => ReportProformaUseCase; createProforma: () => CreateProformaUseCase; + issueProforma: (publicServices: { + issuedInvoiceServices: IIssuedInvoicePublicServices; + }) => IssueProformaUseCase; /* updateProforma: () => UpdateProformaUseCase; @@ -44,12 +52,18 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter const persistenceMappers = buildProformaPersistenceMappers(catalogs); const repository = buildProformaRepository({ database, mappers: persistenceMappers }); - const numberService = buildProformaNumberGenerator(); + const proformaNumberService = buildProformaNumberGenerator(); // Application helpers const inputMappers = buildProformaInputMappers(catalogs); const finder = buildProformaFinder(repository); - const creator = buildProformaCreator({ numberService, repository }); + const creator = buildProformaCreator({ numberService: proformaNumberService, repository }); + const proformaToIssuedInvoiceConverter = buildProformaToIssuedInvoicePropsConverter(); + + const issuer = buildProformaIssuer({ + proformaConverter: proformaToIssuedInvoiceConverter, + repository, + }); const snapshotBuilders = buildProformaSnapshotBuilders(); const documentGeneratorPipeline = buildProformaDocumentService(params); @@ -87,6 +101,14 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter fullSnapshotBuilder: snapshotBuilders.full, transactionManager, }), + + issueProforma: (publicServices: { issuedInvoiceServices: IIssuedInvoicePublicServices }) => + buildIssueProformaUseCase({ + publicServices, + finder, + issuer, + transactionManager, + }), }, }; } diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/index.ts index b094b4a9..398f8976 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/index.ts @@ -2,7 +2,7 @@ //export * from "./create-proforma.controller"; //export * from "./delete-proforma.controller"; export * from "./get-proforma.controller"; -//export * from "./issue-proforma.controller"; +export * from "./issue-proforma.controller"; export * from "./list-proformas.controller"; export * from "./report-proforma.controller"; //export * from "./update-proforma.controller"; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/issue-proforma.controller.ts b/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/issue-proforma.controller.ts index e1b2fb65..fffca478 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/issue-proforma.controller.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/express/controllers/issue-proforma.controller.ts @@ -5,7 +5,7 @@ import { requireCompanyContextGuard, } from "@erp/core/api"; -import type { IssueProformaUseCase } from "../../../../application/index.ts"; +import type { IssueProformaUseCase } from "../../../../application/proformas"; import { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts"; export class IssueProformaController extends ExpressController { diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/express/mappers/create-proforma-request-mapper.ts b/modules/customer-invoices/src/api/infrastructure/proformas/express/mappers/create-proforma-request-mapper.ts index 97bb7798..9db40437 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/express/mappers/create-proforma-request-mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/express/mappers/create-proforma-request-mapper.ts @@ -1,4 +1,5 @@ import type { JsonTaxCatalogProvider } from "@erp/core"; +import { DiscountPercentage } from "@erp/core/api"; import { CurrencyCode, DomainError, @@ -57,7 +58,7 @@ export class CreateProformaRequestMapper { try { this.errors = []; - const defaultStatus = InvoiceStatus.fromDraft(); + const defaultStatus = InvoiceStatus.draft(); const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", this.errors); @@ -209,7 +210,7 @@ export class CreateProformaRequestMapper { const discountPercentage = extractOrPushError( maybeFromNullableResult(item.item_discount_percentage, (value) => - ItemDiscountPercentage.create(value) + DiscountPercentage.create(value) ), "discount_percentage", this.errors diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas.routes.ts b/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas.routes.ts index bc91fbbc..6c71d26a 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas.routes.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas.routes.ts @@ -1,9 +1,10 @@ import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth/api"; -import { type ModuleParams, type RequestWithAuth, validateRequest } from "@erp/core/api"; +import { type RequestWithAuth, type StartParams, validateRequest } from "@erp/core/api"; import { type NextFunction, type Request, type Response, Router } from "express"; import { GetProformaController, + IssueProformaController, ListProformasController, type ProformasInternalDeps, ReportProformaController, @@ -12,18 +13,30 @@ import { import { CreateProformaRequestSchema, GetProformaByIdRequestSchema, + IssueProformaByIdParamsRequestSchema, ListProformasRequestSchema, ReportProformaByIdParamsRequestSchema, ReportProformaByIdQueryRequestSchema, } from "../../../../common"; +import type { IIssuedInvoicePublicServices } from "../../../application"; import { CreateProformaController } from "./controllers/create-proforma.controller"; -export const proformasRouter = (params: ModuleParams, deps: ProformasInternalDeps) => { - const { app, config } = params; +export const proformasRouter = (params: StartParams) => { + const { app, config, getService, getInternal } = params; + + const deps = getInternal("customer-invoices", "proformas"); + + const issuedInvoicesServices = getService("self:issuedInvoices"); + + const publicServices = { + issuedInvoiceServices: issuedInvoicesServices, + }; const router: Router = Router({ mergeParams: true }); + // ---------------------------------------------- + if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "production") { // 🔐 Autenticación + Tenancy para TODO el router router.use( @@ -131,18 +144,18 @@ export const proformasRouter = (params: ModuleParams, deps: ProformasInternalDep } );*/ - /*router.put( + router.put( "/:proforma_id/issue", //checkTabContext, validateRequest(IssueProformaByIdParamsRequestSchema, "params"), (req: Request, res: Response, next: NextFunction) => { - const useCase = deps.useCases.issue_proforma(); - const controller = new IssuedProformaController(useCase); + const useCase = deps.useCases.issueProforma(publicServices); + const controller = new IssueProformaController(useCase); return controller.execute(req, res, next); } - );*/ + ); app.use(`${config.server.apiBasePath}/proformas`, router); }; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-domain.mapper.ts index eb44f3b6..9054cb2e 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-domain.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-domain.mapper.ts @@ -58,12 +58,6 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper< const customerId = extractOrPushError(UniqueID.create(raw.customer_id), "customer_id", errors); - const proformaId = extractOrPushError( - maybeFromNullableResult(raw.proforma_id, (v) => UniqueID.create(v)), - "proforma_id", - errors - ); - const status = extractOrPushError(InvoiceStatus.create(raw.status), "status", errors); const series = extractOrPushError( @@ -160,7 +154,6 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper< invoiceId, companyId, customerId, - proformaId, status, series, invoiceNumber, @@ -244,8 +237,8 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper< paymentMethod: attributes.paymentMethod!, }; - const invoiceId = attributes.invoiceId!; - const proforma = Proforma.rehydrate(invoiceProps, items, invoiceId); + const proformaId = attributes.invoiceId!; + const proforma = Proforma.rehydrate(invoiceProps, items, proformaId); return Result.ok(proforma); } catch (err: unknown) { diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-item-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-item-domain.mapper.ts index 59d5b33b..a0afe5e6 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-item-domain.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-item-domain.mapper.ts @@ -75,7 +75,7 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper< const quantity = extractOrPushError( maybeFromNullableResult(raw.quantity_value, (v) => ItemQuantity.create({ value: v })), - `items[${index}].quantity`, + `items[${index}].quantity_value`, errors ); @@ -83,7 +83,7 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper< maybeFromNullableResult(raw.unit_amount_value, (value) => ItemAmount.create({ value, currency_code: parent.currencyCode?.code }) ), - `items[${index}].unit_amount`, + `items[${index}].unit_amount_value`, errors ); @@ -163,26 +163,20 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper< const itemId = attributes.itemId!; const newItem = ProformaItem.rehydrate( { - languageCode: attributes.languageCode!, - currencyCode: attributes.currencyCode!, description: attributes.description!, quantity: attributes.quantity!, unitAmount: attributes.unitAmount!, + itemDiscountPercentage: attributes.itemDiscountPercentage!, globalDiscountPercentage: attributes.globalDiscountPercentage!, taxes: taxesResult.data, + + languageCode: attributes.languageCode!, + currencyCode: attributes.currencyCode!, }, itemId ); - /*if (createResult.isFailure) { - return Result.fail( - new ValidationErrorCollection("Invoice item entity creation failed", [ - { path: `items[${index}]`, message: createResult.error.message }, - ]) - ); - }*/ - return Result.ok(newItem); } diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-recipient-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-recipient-domain.mapper.ts index 15a51e0d..e839e524 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-recipient-domain.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-recipient-domain.mapper.ts @@ -31,13 +31,6 @@ export class SequelizeProformaRecipientDomainMapper { parent: Partial; }; - /* if (!source.current_customer) { - errors.push({ - path: "current_customer", - message: "Current customer not included in query (SequelizeProformaRecipientDomainMapper)", - }); - } - */ const _name = source.current_customer.name; const _tin = source.current_customer.tin; const _street = source.current_customer.street; diff --git a/modules/customer-invoices/src/common/dto/response/issued-invoices/get-issued-invoice-by-id.response.dto.ts b/modules/customer-invoices/src/common/dto/response/issued-invoices/get-issued-invoice-by-id.response.dto.ts index 49d5a76e..e56f0328 100644 --- a/modules/customer-invoices/src/common/dto/response/issued-invoices/get-issued-invoice-by-id.response.dto.ts +++ b/modules/customer-invoices/src/common/dto/response/issued-invoices/get-issued-invoice-by-id.response.dto.ts @@ -89,8 +89,8 @@ export const GetIssuedInvoiceByIdResponseSchema = z.object({ subtotal_amount: MoneySchema, - discount_percentage: PercentageSchema, - discount_amount: MoneySchema, + item_discount_percentage: PercentageSchema, + item_discount_amount: MoneySchema, global_discount_percentage: PercentageSchema, global_discount_amount: MoneySchema, diff --git a/modules/customer-invoices/src/common/dto/response/proformas/get-proforma-by-id.response.dto.ts b/modules/customer-invoices/src/common/dto/response/proformas/get-proforma-by-id.response.dto.ts index 44affb66..eb28a7e0 100644 --- a/modules/customer-invoices/src/common/dto/response/proformas/get-proforma-by-id.response.dto.ts +++ b/modules/customer-invoices/src/common/dto/response/proformas/get-proforma-by-id.response.dto.ts @@ -83,8 +83,8 @@ export const GetProformaByIdResponseSchema = z.object({ subtotal_amount: MoneySchema, - discount_percentage: PercentageSchema, - discount_amount: MoneySchema, + item_discount_percentage: PercentageSchema, + item_discount_amount: MoneySchema, global_discount_percentage: PercentageSchema, global_discount_amount: MoneySchema, diff --git a/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/use-proforma-grid-columns.tsx b/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/use-proforma-grid-columns.tsx index 1287a0ca..bf72c61e 100644 --- a/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/use-proforma-grid-columns.tsx +++ b/modules/customer-invoices/src/web/proformas/list/ui/blocks/proformas-grid/use-proforma-grid-columns.tsx @@ -19,24 +19,24 @@ import * as React from "react"; import { useTranslation } from "../../../../../i18n"; import { PROFORMA_STATUS_TRANSITIONS, + type ProformaListRow, type ProformaStatus, - type ProformaSummaryData, -} from "../../../../types"; +} from "../../../../shared"; import { ProformaStatusBadge } from "../../components"; type GridActionHandlers = { - onEditClick?: (proforma: ProformaSummaryData) => void; - onIssueClick?: (proforma: ProformaSummaryData) => void; - onChangeStatusClick?: (proforma: ProformaSummaryData, nextStatus: string) => void; - onDeleteClick?: (proforma: ProformaSummaryData) => void; + onEditClick?: (proforma: ProformaListRow) => void; + onIssueClick?: (proforma: ProformaListRow) => void; + onChangeStatusClick?: (proforma: ProformaListRow, nextStatus: string) => void; + onDeleteClick?: (proforma: ProformaListRow) => void; }; export function useProformasGridColumns( actionHandlers: GridActionHandlers = {} -): ColumnDef[] { +): ColumnDef[] { const { t } = useTranslation(); - return React.useMemo[]>( + return React.useMemo[]>( () => [ /*{ id: "select", diff --git a/modules/customer-invoices/src/web/proformas/list/ui/components/proforma-status-badge.tsx b/modules/customer-invoices/src/web/proformas/list/ui/components/proforma-status-badge.tsx index 590e732d..3c2124fe 100644 --- a/modules/customer-invoices/src/web/proformas/list/ui/components/proforma-status-badge.tsx +++ b/modules/customer-invoices/src/web/proformas/list/ui/components/proforma-status-badge.tsx @@ -3,11 +3,11 @@ import { cn } from "@repo/shadcn-ui/lib/utils"; import { useTranslation } from "../../../../i18n"; import { - type ProformaStatus, getProformaStatusButtonVariant, getProformaStatusColor, getProformaStatusIcon, -} from "../../../types"; +} from "../../../change-status/helpers"; +import type { ProformaStatus } from "../../../shared"; export type ProformaStatusBadgeProps = { status: string | ProformaStatus; // permitir cualquier valor diff --git a/modules/customers/src/api/application/customer-application.service.ts b/modules/customers/src/api/application/customer-application.service.ts deleted file mode 100644 index bc1290b9..00000000 --- a/modules/customers/src/api/application/customer-application.service.ts +++ /dev/null @@ -1,165 +0,0 @@ -// application/customer-application-service.ts -import type { Criteria } from "@repo/rdx-criteria/server"; -import type { UniqueID } from "@repo/rdx-ddd"; -import { type Collection, Result } from "@repo/rdx-utils"; -import type { Transaction } from "sequelize"; - -import { - Customer, - type CustomerPatchProps, - type ICustomerCreateProps, - type ICustomerRepository, -} from "../domain"; -import type { CustomerListDTO } from "../infrastructure"; - -export class CustomerApplicationService { - constructor(private readonly repository: ICustomerRepository) {} - - /** - * Construye un nuevo agregado CustomerInvoice a partir de props validadas. - * - * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. - * @param props - Las propiedades ya validadas para crear el cliente. - * @param customerId - Identificador UUID del cliente (opcional). - * @returns Result - El agregado construido o un error si falla la creación. - */ - buildCustomerInCompany( - companyId: UniqueID, - props: Omit, - customerId?: UniqueID - ): Result { - return Customer.create({ ...props, companyId }, customerId); - } - - /** - * Guarda un nuevo cliente y devuelve el cliente guardado. - * - * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. - * @param customer - El cliente a guardar. - * @param transaction - Transacción activa para la operación. - * @returns Result - El cliente guardado o un error si falla la operación. - */ - async createCustomerInCompany( - companyId: UniqueID, - customer: Customer, - transaction?: Transaction - ): Promise> { - const result = await this.repository.create(customer, transaction); - if (result.isFailure) return Result.fail(result.error); - - return this.getCustomerByIdInCompany(companyId, customer.id, transaction); - } - - /** - * Actualiza un cliente existente y devuelve el cliente actualizado. - * - * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. - * @param customer - El cliente a guardar. - * @param transaction - Transacción activa para la operación. - * @returns Result - El cliente guardado o un error si falla la operación. - */ - async updateCustomerInCompany( - companyId: UniqueID, - customer: Customer, - transaction?: Transaction - ): Promise> { - const result = await this.repository.update(customer, transaction); - if (result.isFailure) return Result.fail(result.error); - - return this.getCustomerByIdInCompany(companyId, customer.id, transaction); - } - - /** - * Actualiza parcialmente un cliente existente con nuevos datos. - * Solo en memoria. No lo guarda en el repositorio. - * - * @param companyId - Identificador de la empresa a la que pertenece el cliente. - * @param customerId - Identificador del cliente a actualizar. - * @param changes - Subconjunto de props válidas para aplicar. - * @param transaction - Transacción activa para la operación. - * @returns Result - Cliente actualizado o error. - */ - async patchCustomerByIdInCompany( - companyId: UniqueID, - customerId: UniqueID, - changes: CustomerPatchProps, - transaction?: Transaction - ): Promise> { - const customerResult = await this.getCustomerByIdInCompany(companyId, customerId, transaction); - if (customerResult.isFailure) { - return Result.fail(customerResult.error); - } - - const updated = customerResult.data.update(changes); - if (updated.isFailure) { - return Result.fail(updated.error); - } - - return Result.ok(updated.data); - } - - /** - * Elimina (o marca como eliminado) un cliente según su ID. - * - * @param companyId - Identificador de la empresa a la que pertenece el cliente. - * @param customerId - Identificador UUID del cliente. - * @param transaction - Transacción activa para la operación. - * @returns Result - Resultado de la operación. - */ - async deleteCustomerByIdInCompany( - companyId: UniqueID, - customerId: UniqueID, - transaction?: Transaction - ): Promise> { - return this.repository.deleteByIdInCompany(companyId, customerId, transaction); - } - - /** - * - * Comprueba si existe o no en persistencia un cliente con el ID proporcionado - * - * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. - * @param customerId - Identificador UUID del cliente - * @param transaction - Transacción activa para la operación. - * @returns Result - Existe el cliente o no. - */ - async existsByIdInCompany( - companyId: UniqueID, - customerId: UniqueID, - transaction?: Transaction - ): Promise> { - return this.repository.existsByIdInCompany(companyId, customerId, transaction); - } - - /** - * Recupera un cliente por su identificador único. - * - * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. - * @param customerId - Identificador UUID del cliente. - * @param transaction - Transacción activa para la operación. - * @returns Result - Cliente encontrado o error. - */ - async getCustomerByIdInCompany( - companyId: UniqueID, - customerId: UniqueID, - transaction?: Transaction - ): Promise> { - return this.repository.getByIdInCompany(companyId, customerId, transaction); - } - - /** - * Obtiene una colección de clientes que cumplen con los filtros definidos en un objeto Criteria. - * - * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. - * @param criteria - Objeto con condiciones de filtro, paginación y orden. - * @param transaction - Transacción activa para la operación. - * @returns Result, Error> - Colección de clientes o error. - */ - async findCustomerByCriteriaInCompany( - companyId: UniqueID, - criteria: Criteria, - transaction?: Transaction - ): Promise, Error>> { - return this.repository.findByCriteriaInCompany(companyId, criteria, transaction); - } -} diff --git a/modules/customers/src/api/application/services/customer-creator.ts b/modules/customers/src/api/application/services/customer-creator.ts index 371007bf..67b40708 100644 --- a/modules/customers/src/api/application/services/customer-creator.ts +++ b/modules/customers/src/api/application/services/customer-creator.ts @@ -1,7 +1,6 @@ import { DuplicateEntityError } from "@erp/core/api"; import type { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import type { Transaction } from "sequelize"; import { Customer, type ICustomerCreateProps } from "../../domain"; import type { ICustomerRepository } from "../repositories"; @@ -12,7 +11,7 @@ export interface ICustomerCreator { companyId: UniqueID; id: UniqueID; props: ICustomerCreateProps; - transaction: Transaction; + unknown: unknown; }): Promise>; } @@ -31,16 +30,12 @@ export class CustomerCreator implements ICustomerCreator { companyId: UniqueID; id: UniqueID; props: ICustomerCreateProps; - transaction: Transaction; + unknown: unknown; }): Promise> { - const { companyId, id, props, transaction } = params; + const { companyId, id, props, unknown } = params; // 1. Verificar unicidad - const spec = new CustomerNotExistsInCompanySpecification( - this.repository, - companyId, - transaction - ); + const spec = new CustomerNotExistsInCompanySpecification(this.repository, companyId, unknown); const isNew = await spec.isSatisfiedBy(id); @@ -64,7 +59,7 @@ export class CustomerCreator implements ICustomerCreator { const newCustomer = createResult.data; // 3. Persistir agregado - const saveResult = await this.repository.create(newCustomer, transaction); + const saveResult = await this.repository.create(newCustomer, unknown); if (saveResult.isFailure) { return Result.fail(saveResult.error); diff --git a/modules/customers/src/api/application/services/customer-finder.ts b/modules/customers/src/api/application/services/customer-finder.ts index 42e48056..f8d9d334 100644 --- a/modules/customers/src/api/application/services/customer-finder.ts +++ b/modules/customers/src/api/application/services/customer-finder.ts @@ -1,7 +1,6 @@ import type { Criteria } from "@repo/rdx-criteria/server"; import type { TINNumber, UniqueID } from "@repo/rdx-ddd"; import type { Collection, Result } from "@repo/rdx-utils"; -import type { Transaction } from "sequelize"; import type { Customer } from "../../domain"; import type { CustomerSummary } from "../models"; @@ -11,25 +10,25 @@ export interface ICustomerFinder { findCustomerById( companyId: UniqueID, customerId: UniqueID, - transaction?: Transaction + unknown?: unknown ): Promise>; findCustomerByTIN( companyId: UniqueID, tin: TINNumber, - transaction?: Transaction + unknown?: unknown ): Promise>; customerExists( companyId: UniqueID, invoiceId: UniqueID, - transaction?: Transaction + unknown?: unknown ): Promise>; findCustomersByCriteria( companyId: UniqueID, criteria: Criteria, - transaction?: Transaction + unknown?: unknown ): Promise, Error>>; } @@ -39,32 +38,32 @@ export class CustomerFinder implements ICustomerFinder { async findCustomerById( companyId: UniqueID, customerId: UniqueID, - transaction?: Transaction + unknown?: unknown ): Promise> { - return this.repository.getByIdInCompany(companyId, customerId, transaction); + return this.repository.getByIdInCompany(companyId, customerId, unknown); } findCustomerByTIN( companyId: UniqueID, tin: TINNumber, - transaction?: Transaction + unknown?: unknown ): Promise> { - return this.repository.getByTINInCompany(companyId, tin, transaction); + return this.repository.getByTINInCompany(companyId, tin, unknown); } async customerExists( companyId: UniqueID, customerId: UniqueID, - transaction?: Transaction + unknown?: unknown ): Promise> { - return this.repository.existsByIdInCompany(companyId, customerId, transaction); + return this.repository.existsByIdInCompany(companyId, customerId, unknown); } async findCustomersByCriteria( companyId: UniqueID, criteria: Criteria, - transaction?: Transaction + unknown?: unknown ): Promise, Error>> { - return this.repository.findByCriteriaInCompany(companyId, criteria, transaction); + return this.repository.findByCriteriaInCompany(companyId, criteria, unknown); } } diff --git a/modules/customers/src/api/application/services/customer-public-services.interface.ts b/modules/customers/src/api/application/services/customer-public-services.interface.ts new file mode 100644 index 00000000..7942d13a --- /dev/null +++ b/modules/customers/src/api/application/services/customer-public-services.interface.ts @@ -0,0 +1,22 @@ +import type { TINNumber, UniqueID } from "@repo/rdx-ddd"; +import type { Result } from "@repo/rdx-utils"; + +import type { Customer, ICustomerCreateProps } from "../../domain"; + +export interface ICustomerServicesContext { + transaction: unknown; + companyId: UniqueID; +} + +export interface ICustomerPublicServices { + findCustomerByTIN: ( + tin: TINNumber, + context: ICustomerServicesContext + ) => Promise>; + + createCustomer: ( + id: UniqueID, + props: ICustomerCreateProps, + context: ICustomerServicesContext + ) => Promise>; +} diff --git a/modules/customers/src/api/application/services/customer-updater.ts b/modules/customers/src/api/application/services/customer-updater.ts index 522e1da9..42f34a31 100644 --- a/modules/customers/src/api/application/services/customer-updater.ts +++ b/modules/customers/src/api/application/services/customer-updater.ts @@ -1,6 +1,5 @@ import type { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import type { Transaction } from "sequelize"; import type { Customer, CustomerPatchProps } from "../../domain"; import type { ICustomerRepository } from "../repositories"; @@ -10,7 +9,7 @@ export interface ICustomerUpdater { companyId: UniqueID; id: UniqueID; props: CustomerPatchProps; - transaction: Transaction; + transaction: unknown; }): Promise>; } @@ -29,7 +28,7 @@ export class CustomerUpdater implements ICustomerUpdater { companyId: UniqueID; id: UniqueID; props: CustomerPatchProps; - transaction: Transaction; + transaction: unknown; }): Promise> { const { companyId, id, props, transaction } = params; diff --git a/modules/customers/src/api/application/services/index.ts b/modules/customers/src/api/application/services/index.ts index f70f97e0..54e13177 100644 --- a/modules/customers/src/api/application/services/index.ts +++ b/modules/customers/src/api/application/services/index.ts @@ -1,3 +1,4 @@ export * from "./customer-creator"; export * from "./customer-finder"; +export * from "./customer-public-services.interface"; export * from "./customer-updater"; diff --git a/modules/customers/src/api/application/specs/customer-not-exists.spec.ts b/modules/customers/src/api/application/specs/customer-not-exists.spec.ts index c8a4ae85..05b1d677 100644 --- a/modules/customers/src/api/application/specs/customer-not-exists.spec.ts +++ b/modules/customers/src/api/application/specs/customer-not-exists.spec.ts @@ -1,5 +1,4 @@ import { CompositeSpecification, type UniqueID } from "@repo/rdx-ddd"; -import type { Transaction } from "sequelize"; import type { ICustomerRepository } from "../../application"; import { logger } from "../../helpers"; @@ -8,7 +7,7 @@ export class CustomerNotExistsInCompanySpecification extends CompositeSpecificat constructor( private readonly repository: ICustomerRepository, private readonly companyId: UniqueID, - private readonly transaction?: Transaction + private readonly transaction?: unknown ) { super(); } diff --git a/modules/customers/src/api/application/use-cases/create-customer.use-case.ts b/modules/customers/src/api/application/use-cases/create-customer.use-case.ts index e232fba9..18d2f29b 100644 --- a/modules/customers/src/api/application/use-cases/create-customer.use-case.ts +++ b/modules/customers/src/api/application/use-cases/create-customer.use-case.ts @@ -1,7 +1,6 @@ import type { ITransactionManager } from "@erp/core/api"; import type { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import type { Transaction } from "sequelize"; import type { CreateCustomerRequestDTO } from "../../../common"; import type { ICreateCustomerInputMapper } from "../mappers"; @@ -43,7 +42,7 @@ export class CreateCustomerUseCase { const { props, id } = mappedPropsResult.data; - return this.transactionManager.complete(async (transaction: Transaction) => { + return this.transactionManager.complete(async (transaction: unknown) => { try { const createResult = await this.creator.create({ companyId, id, props, transaction }); diff --git a/modules/customers/src/api/application/use-cases/list-customers.use-case.ts b/modules/customers/src/api/application/use-cases/list-customers.use-case.ts index aba9f99a..08cbf3cb 100644 --- a/modules/customers/src/api/application/use-cases/list-customers.use-case.ts +++ b/modules/customers/src/api/application/use-cases/list-customers.use-case.ts @@ -2,7 +2,6 @@ import type { ITransactionManager } from "@erp/core/api"; import type { Criteria } from "@repo/rdx-criteria/server"; import type { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import type { Transaction } from "sequelize"; import type { ICustomerFinder } from "../services"; import type { ICustomerSummarySnapshotBuilder } from "../snapshot-builders/summary"; @@ -22,7 +21,7 @@ export class ListCustomersUseCase { public execute(params: ListCustomersUseCaseInput) { const { criteria, companyId } = params; - return this.transactionManager.complete(async (transaction: Transaction) => { + return this.transactionManager.complete(async (transaction: unknown) => { try { const result = await this.finder.findCustomersByCriteria(companyId, criteria, transaction); diff --git a/modules/customers/src/api/application/use-cases/update/update-customer.use-case.ts b/modules/customers/src/api/application/use-cases/update/update-customer.use-case.ts index 6cb5bf8b..5b5ed017 100644 --- a/modules/customers/src/api/application/use-cases/update/update-customer.use-case.ts +++ b/modules/customers/src/api/application/use-cases/update/update-customer.use-case.ts @@ -1,7 +1,6 @@ import type { ITransactionManager } from "@erp/core/api"; import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import type { Transaction } from "sequelize"; import type { UpdateCustomerByIdRequestDTO } from "../../../../common/dto"; import type { CustomerPatchProps } from "../../../domain"; @@ -52,7 +51,7 @@ export class UpdateCustomerUseCase { const patchProps: CustomerPatchProps = patchPropsResult.data; - return this.transactionManager.complete(async (transaction: Transaction) => { + return this.transactionManager.complete(async (transaction: unknown) => { try { const updateResult = await this.updater.update({ companyId, diff --git a/modules/customers/src/api/index.ts b/modules/customers/src/api/index.ts index 387a05a4..19f6780b 100644 --- a/modules/customers/src/api/index.ts +++ b/modules/customers/src/api/index.ts @@ -3,7 +3,7 @@ import type { IModuleServer } from "@erp/core/api"; import { type CustomerPublicServices, customersRouter, models } from "./infrastructure"; import { type CustomersInternalDeps, - buildCustomerServices, + buildCustomerPublicServices, buildCustomersDependencies, } from "./infrastructure/di"; @@ -30,7 +30,7 @@ export const customersAPIModule: IModuleServer = { const internal = buildCustomersDependencies(params); // 2) Servicios públicos (Application Services) - const customersServices: CustomerPublicServices = buildCustomerServices(params, internal); + const customersServices: CustomerPublicServices = buildCustomerPublicServices(params, internal); logger.info("🚀 Customers module dependencies registered", { label: this.name, diff --git a/modules/customers/src/api/infrastructure/di/customer-public-services.ts b/modules/customers/src/api/infrastructure/di/customer-public-services.ts index a1bb929f..117fdf68 100644 --- a/modules/customers/src/api/infrastructure/di/customer-public-services.ts +++ b/modules/customers/src/api/infrastructure/di/customer-public-services.ts @@ -1,38 +1,23 @@ import { type SetupParams, buildCatalogs } from "@erp/core/api"; import type { TINNumber, UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import type { Transaction } from "sequelize"; -import { buildCustomerCreator, buildCustomerFinder } from "../../application"; -import type { Customer, ICustomerCreateProps } from "../../domain"; +import { + type ICustomerPublicServices, + type ICustomerServicesContext, + buildCustomerCreator, + buildCustomerFinder, +} from "../../application"; +import type { ICustomerCreateProps } from "../../domain"; import { buildCustomerPersistenceMappers } from "./customer-persistence-mappers.di"; import { buildCustomerRepository } from "./customer-repositories.di"; import type { CustomersInternalDeps } from "./customers.di"; -type CustomerServicesContext = { - transaction: Transaction; - companyId: UniqueID; -}; - -export type CustomerPublicServices = { - //listCustomers: (filters: unknown, context: unknown) => null; - findCustomerByTIN: ( - tin: TINNumber, - context: CustomerServicesContext - ) => Promise>; - createCustomer: ( - id: UniqueID, - props: ICustomerCreateProps, - context: CustomerServicesContext - ) => Promise>; - //generateCustomerReport: (id: unknown, options: unknown, context: unknown) => null; -}; - -export function buildCustomerServices( +export function buildCustomerPublicServices( params: SetupParams, deps: CustomersInternalDeps -): CustomerPublicServices { +): ICustomerPublicServices { const { database } = params; const catalogs = buildCatalogs(); @@ -44,7 +29,7 @@ export function buildCustomerServices( const creator = buildCustomerCreator({ repository }); return { - findCustomerByTIN: async (tin: TINNumber, context: CustomerServicesContext) => { + findCustomerByTIN: async (tin: TINNumber, context: ICustomerServicesContext) => { const { companyId, transaction } = context; const customerResult = await finder.findCustomerByTIN(companyId, tin, transaction); @@ -59,7 +44,7 @@ export function buildCustomerServices( createCustomer: async ( id: UniqueID, props: ICustomerCreateProps, - context: CustomerServicesContext + context: ICustomerServicesContext ) => { const { companyId, transaction } = context; diff --git a/modules/factuges/src/api/application/mappers/create-proforma-from-factuges-input.mapper.ts b/modules/factuges/src/api/application/mappers/create-proforma-from-factuges-input.mapper.ts index f8947b16..5ffd1b3a 100644 --- a/modules/factuges/src/api/application/mappers/create-proforma-from-factuges-input.mapper.ts +++ b/modules/factuges/src/api/application/mappers/create-proforma-from-factuges-input.mapper.ts @@ -490,7 +490,7 @@ export class CreateProformaFromFactugesInputMapper //legalRecord: Maybe.none(), - //defaultTaxes: customerTaxes, + //defaultTaxes: customerTaxes!, languageCode: languageCode!, currencyCode: currencyCode!, diff --git a/modules/factuges/src/api/application/use-cases/create-proforma-from-factuges.use-case.ts b/modules/factuges/src/api/application/use-cases/create-proforma-from-factuges.use-case.ts index a589bb50..21a3f4df 100644 --- a/modules/factuges/src/api/application/use-cases/create-proforma-from-factuges.use-case.ts +++ b/modules/factuges/src/api/application/use-cases/create-proforma-from-factuges.use-case.ts @@ -1,5 +1,5 @@ import type { JsonTaxCatalogProvider } from "@erp/core"; -import { type ITransactionManager, Tax, isEntityNotFoundError } from "@erp/core/api"; +import { type ITransactionManager, isEntityNotFoundError } from "@erp/core/api"; import type { ProformaPublicServices } from "@erp/customer-invoices/api"; import { type InvoiceAmount, @@ -180,8 +180,6 @@ export class CreateProformaFromFactugesUseCase { const errors: ValidationErrorDetail[] = []; const proformaTotals = proforma.totals(); - console.log(proformaTotals); - if (proformaDraft.items.length !== proforma.items.size()) { errors.push({ path: "items", @@ -267,7 +265,7 @@ export class CreateProformaFromFactugesUseCase { }): string { const { baseMessage, expected, actual } = params; - return `${baseMessage} Esperado: ${expected.formattedValue}. Actual: ${actual.formattedValue}.`; + return `${baseMessage} FactuGES: ${expected.formattedValue}. Calculado: ${actual.formattedValue}.`; } /** @@ -319,7 +317,7 @@ export class CreateProformaFromFactugesUseCase { const { proformaDraft, payment, customerId, context } = deps; const { companyId } = context; - const defaultStatus = InvoiceStatus.fromApproved(); + const defaultStatus = InvoiceStatus.approved(); const recipient = Maybe.none(); const paymentMethod = Maybe.some( InvoicePaymentMethod.create({ paymentDescription: payment.description }, payment.id).data @@ -420,16 +418,8 @@ export class CreateProformaFromFactugesUseCase { const status = CustomerStatus.createActive(); - const ivaResult = Tax.createFromCode("iva_21", this.taxCatalog); - if (ivaResult.isFailure) { - return Result.fail(ivaResult.error); - } + const defaultTaxes = CustomerTaxes.fromKey("iva_21;#;#", this.taxCatalog); - const defaultTaxes = CustomerTaxes.create({ - iva: Maybe.some(ivaResult.data), - rec: Maybe.none(), - retention: Maybe.none(), - }); if (defaultTaxes.isFailure) { return Result.fail(defaultTaxes.error); } diff --git a/modules/factuges/src/api/index.ts b/modules/factuges/src/api/index.ts index 88b28c5a..1152df4c 100644 --- a/modules/factuges/src/api/index.ts +++ b/modules/factuges/src/api/index.ts @@ -1,9 +1,7 @@ import type { IModuleServer } from "@erp/core/api"; -import type { ProformaPublicServices } from "@erp/customer-invoices/api"; -import type { CustomerPublicServices } from "@erp/customers/api"; import { factugesRouter } from "./infraestructure"; -import { type FactugesInternalDeps, buildFactugesDependencies } from "./infraestructure/di"; +import { buildFactugesDependencies } from "./infraestructure/di"; export const factugesAPIModule: IModuleServer = { name: "factuges", @@ -50,15 +48,8 @@ export const factugesAPIModule: IModuleServer = { async start(params) { const { app, baseRoutePath, logger, getInternal, getService, listServices } = params; - // Recuperamos el dominio interno del módulo - const factugesInternapDeps = getInternal("factuges"); - - // Recuperamos servicios externos que necesitemos - const customerServices = getService("customers:general"); - const proformaServices = getService("customer-invoices:proformas"); - // Registro de rutas HTTP - factugesRouter(params, factugesInternapDeps, { customerServices, proformaServices }); + factugesRouter(params); logger.info("🚀 Factuges module started", { label: this.name }); }, diff --git a/modules/factuges/src/api/infraestructure/di/factuges.di.ts b/modules/factuges/src/api/infraestructure/di/factuges.di.ts index dc1e1c01..b6438fcd 100644 --- a/modules/factuges/src/api/infraestructure/di/factuges.di.ts +++ b/modules/factuges/src/api/infraestructure/di/factuges.di.ts @@ -33,14 +33,13 @@ export function buildFactugesDependencies(params: SetupParams): FactugesInternal createProforma: (publicServices: { customerServices: CustomerPublicServices; proformaServices: ProformaPublicServices; - }) => { - return buildCreateProformaFromFactugesUseCase({ + }) => + buildCreateProformaFromFactugesUseCase({ dtoMapper: inputMappers.createInputMapper, publicServices, catalogs, transactionManager, - }); - }, + }), }, }; } diff --git a/modules/factuges/src/api/infraestructure/express/factuges.routes.ts b/modules/factuges/src/api/infraestructure/express/factuges.routes.ts index 57556714..2db61809 100644 --- a/modules/factuges/src/api/infraestructure/express/factuges.routes.ts +++ b/modules/factuges/src/api/infraestructure/express/factuges.routes.ts @@ -1,23 +1,27 @@ import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth/api"; -import { type ModuleParams, type RequestWithAuth, validateRequest } from "@erp/core/api"; -import type { ProformaPublicServices } from "@erp/customer-invoices/api"; -import type { CustomerPublicServices } from "@erp/customers/api"; +import { type RequestWithAuth, type StartParams, validateRequest } from "@erp/core/api"; import { CreateProformaFromFactugesRequestSchema } from "@erp/factuges/common"; import { type NextFunction, type Request, type Response, Router } from "express"; +import type { IProformaPublicServices } from "node_modules/@erp/customer-invoices/src/api/application"; import type { FactugesInternalDeps } from "../di/factuges.di"; import { CreateProformaFromFactugesController } from "./controllers"; -export const factugesRouter = ( - params: ModuleParams, - deps: FactugesInternalDeps, - publicServices: { - customerServices: CustomerPublicServices; - proformaServices: ProformaPublicServices; - } -) => { - const { app, config } = params; +export const factugesRouter = (params: StartParams) => { + const { app, config, getInternal, getService } = params; + + // Recuperamos el dominio interno del módulo + const deps = getInternal("factuges"); + + // Recuperamos servicios externos que necesitemos + const customerServices = getService("customers:general"); + const proformaServices = getService("customer-invoices:proformas"); + + const publicServices = { + customerServices, + proformaServices, + }; const router: Router = Router({ mergeParams: true });