diff --git a/modules/core/src/api/domain/repositories/repository.interface.ts b/modules/core/src/api/domain/repositories/repository.interface.ts index 5ac3d3e3..501bb752 100644 --- a/modules/core/src/api/domain/repositories/repository.interface.ts +++ b/modules/core/src/api/domain/repositories/repository.interface.ts @@ -1,4 +1,4 @@ -import { Collection, Result } from "@repo/rdx-utils"; +import type { Collection, Result } from "@repo/rdx-utils"; /** * Tipo para los parámetros que reciben los métodos de los mappers diff --git a/modules/core/src/api/infrastructure/sequelize/sequelize-repository.ts b/modules/core/src/api/infrastructure/sequelize/sequelize-repository.ts index a004b629..c4f2f4ef 100644 --- a/modules/core/src/api/infrastructure/sequelize/sequelize-repository.ts +++ b/modules/core/src/api/infrastructure/sequelize/sequelize-repository.ts @@ -3,15 +3,11 @@ import type { IAggregateRootRepository, UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import type { FindOptions, ModelDefined, Sequelize, Transaction } from "sequelize"; -import type { IMapperRegistry } from "../mappers"; - export abstract class SequelizeRepository implements IAggregateRootRepository { - protected readonly _database!: Sequelize; - protected readonly _registry!: IMapperRegistry; + protected readonly database!: Sequelize; - constructor(params: { mapperRegistry: IMapperRegistry; database: Sequelize }) { - this._registry = params.mapperRegistry; - this._database = params.database; + constructor(params: { database: Sequelize }) { + this.database = params.database; } protected convertCriteria(criteria: Criteria): FindOptions { 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 index 4deb41a5..7312ca82 100644 --- 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 @@ -8,10 +8,10 @@ import { Result } from "@repo/rdx-utils"; import type { CreateCustomerInvoiceRequestDTO } from "../../../common"; import { - CustomerInvoiceItem, - CustomerInvoiceItemDescription, - type CustomerInvoiceItemProps, + IssuedInvoiceItem, + type IssuedInvoiceItemProps, ItemAmount, + ItemDescription, ItemDiscount, ItemQuantity, } from "../../domain"; @@ -20,17 +20,15 @@ import { hasNoUndefinedFields } from "./has-no-undefined-fields"; export function mapDTOToCustomerInvoiceItemsProps( dtoItems: Pick["items"] -): Result { +): Result { const errors: ValidationErrorDetail[] = []; - const items: CustomerInvoiceItem[] = []; + const items: IssuedInvoiceItem[] = []; dtoItems.forEach((item, index) => { const path = (field: string) => `items[${index}].${field}`; const description = extractOrPushError( - maybeFromNullableVO(item.description, (value) => - CustomerInvoiceItemDescription.create(value) - ), + maybeFromNullableVO(item.description, (value) => ItemDescription.create(value)), path("description"), errors ); @@ -54,7 +52,7 @@ export function mapDTOToCustomerInvoiceItemsProps( ); if (errors.length === 0) { - const itemProps: CustomerInvoiceItemProps = { + const itemProps: IssuedInvoiceItemProps = { description: description, quantity: quantity, unitAmount: unitAmount, @@ -66,7 +64,7 @@ export function mapDTOToCustomerInvoiceItemsProps( if (hasNoUndefinedFields(itemProps)) { // Validar y crear el item de factura - const itemOrError = CustomerInvoiceItem.create(itemProps); + const itemOrError = IssuedInvoiceItem.create(itemProps); if (itemOrError.isSuccess) { items.push(itemOrError.data); 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 index 3d4e29b3..5a785199 100644 --- 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 @@ -10,12 +10,7 @@ import { import { Result } from "@repo/rdx-utils"; import type { CreateCustomerInvoiceRequestDTO } from "../../../common"; -import { - CustomerInvoiceNumber, - type CustomerInvoiceProps, - CustomerInvoiceSerie, - CustomerInvoiceStatus, -} from "../../domain"; +import { type IProformaProps, InvoiceNumber, InvoiceSerie, InvoiceStatus } from "../../domain"; import { mapDTOToCustomerInvoiceItemsProps } from "./map-dto-to-customer-invoice-items-props"; @@ -35,12 +30,12 @@ export function mapDTOToCustomerInvoiceProps(dto: CreateCustomerInvoiceRequestDT const invoiceId = extractOrPushError(UniqueID.create(dto.id), "id", errors); const invoiceNumber = extractOrPushError( - maybeFromNullableVO(dto.invoice_number, (value) => CustomerInvoiceNumber.create(value)), + maybeFromNullableVO(dto.invoice_number, (value) => InvoiceNumber.create(value)), "invoice_number", errors ); const invoiceSeries = extractOrPushError( - maybeFromNullableVO(dto.series, (value) => CustomerInvoiceSerie.create(value)), + maybeFromNullableVO(dto.series, (value) => InvoiceSerie.create(value)), "invoice_series", errors ); @@ -71,12 +66,12 @@ export function mapDTOToCustomerInvoiceProps(dto: CreateCustomerInvoiceRequestDT return Result.fail(new ValidationErrorCollection("Invoice dto mapping failed", errors)); } - const invoiceProps: CustomerInvoiceProps = { + const invoiceProps: IProformaProps = { invoiceNumber: invoiceNumber!, series: invoiceSeries!, invoiceDate: invoiceDate!, operationDate: operationDate!, - status: CustomerInvoiceStatus.createDraft(), + status: InvoiceStatus.createDraft(), currencyCode: currencyCode!, }; diff --git a/modules/customer-invoices/src/api/application/index.ts b/modules/customer-invoices/src/api/application/index.ts index ffdca24a..de2b09ca 100644 --- a/modules/customer-invoices/src/api/application/index.ts +++ b/modules/customer-invoices/src/api/application/index.ts @@ -1,5 +1,2 @@ -//export * from "./services"; -//export * from "./snapshot-builders"; - export * from "./issued-invoices"; -export * from "./use-cases"; +export * from "./proformas"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/application-models/index.ts index 02b05bb8..e69de29b 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/application-models/index.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/application-models/index.ts @@ -1,3 +0,0 @@ -export * from "./issued-invoice-document.model"; -export * from "./report-cache-key"; -export * from "./snapshots"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/issued-invoice-document.model.ts b/modules/customer-invoices/src/api/application/issued-invoices/application-models/issued-invoice-document.model.ts deleted file mode 100644 index fa02ca33..00000000 --- a/modules/customer-invoices/src/api/application/issued-invoices/application-models/issued-invoice-document.model.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Documento legal generado para una factura emitida. - * - */ - -export interface IIssuedInvoiceDocument { - payload: Buffer; - mimeType: string; - filename: string; -} - -export class IssuedInvoiceDocument implements IIssuedInvoiceDocument { - public readonly payload: Buffer; - public readonly mimeType: string; - public readonly filename: string; - - constructor(params: { - payload: Buffer; - filename: string; - }) { - if (!params.payload || params.payload.length === 0) { - throw new Error("IssuedInvoiceDocument payload cannot be empty"); - } - - if (!params.filename.toLowerCase().endsWith(".pdf")) { - throw new Error("IssuedInvoiceDocument filename must end with .pdf"); - } - - this.payload = params.payload; - this.mimeType = "application/pdf"; - this.filename = params.filename; - } -} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/report-cache-key.ts b/modules/customer-invoices/src/api/application/issued-invoices/application-models/report-cache-key.ts deleted file mode 100644 index a8f96f1d..00000000 --- a/modules/customer-invoices/src/api/application/issued-invoices/application-models/report-cache-key.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { UniqueID } from "@repo/rdx-ddd"; - -/** - * Clave determinista que identifica de forma única - * un documento legal generado. - * - * Encapsula la regla de idempotencia del caso de uso. - */ -export class ReportCacheKey { - private readonly value: string; - - private constructor(value: string) { - this.value = value; - } - - static forIssuedInvoice(params: { - companyId: UniqueID; - invoiceId: UniqueID; - language: string; - canonicalModelHash: string; - templateChecksum: string; - rendererVersion: string; - signingProviderVersion: string; - }): ReportCacheKey { - const { - companyId, - invoiceId, - language, - canonicalModelHash, - templateChecksum, - rendererVersion, - signingProviderVersion, - } = params; - - // Fail-fast: campos obligatorios - for (const [key, value] of Object.entries(params)) { - if (!value || String(value).trim() === "") { - throw new Error(`ReportCacheKey missing field: ${key}`); - } - } - - return new ReportCacheKey( - [ - "issued-invoice", - companyId, - invoiceId, - language, - canonicalModelHash, - templateChecksum, - rendererVersion, - signingProviderVersion, - ].join("__") - ); - } - - toString(): string { - return this.value; - } -} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/full/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/full/index.ts deleted file mode 100644 index 0b54621c..00000000 --- a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/full/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./issued-invoice-full-snapshot"; -export * from "./issued-invoice-item-full-snapshot"; -export * from "./issued-invoice-recipient-full-snapshot"; -export * from "./issued-invoice-verifactu-full-snapshot"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/list/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/list/index.ts deleted file mode 100644 index 977ca20e..00000000 --- a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/list/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./issued-invoice-list-item-snapshot"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/report/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/report/index.ts deleted file mode 100644 index 536472e8..00000000 --- a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/report/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./issued-invoice-report-item-snapshot"; -export * from "./issued-invoice-report-snapshot"; -export * from "./issued-invoice-report-tax-snapshot"; 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 19c0cf44..2fc0c886 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,3 @@ -export * from "./finder.di"; -export * from "./snapshot-builders.di"; -export * from "./use-cases.di"; +export * from "./issued-invoice-finder.di"; +export * from "./issued-invoice-snapshot-builders.di"; +export * from "./issued-invoice-use-cases.di"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/di/finder.di.ts b/modules/customer-invoices/src/api/application/issued-invoices/di/issued-invoice-finder.di.ts similarity index 100% rename from modules/customer-invoices/src/api/application/issued-invoices/di/finder.di.ts rename to modules/customer-invoices/src/api/application/issued-invoices/di/issued-invoice-finder.di.ts diff --git a/modules/customer-invoices/src/api/application/issued-invoices/di/snapshot-builders.di.ts b/modules/customer-invoices/src/api/application/issued-invoices/di/issued-invoice-snapshot-builders.di.ts similarity index 100% rename from modules/customer-invoices/src/api/application/issued-invoices/di/snapshot-builders.di.ts rename to modules/customer-invoices/src/api/application/issued-invoices/di/issued-invoice-snapshot-builders.di.ts diff --git a/modules/customer-invoices/src/api/application/issued-invoices/di/use-cases.di.ts b/modules/customer-invoices/src/api/application/issued-invoices/di/issued-invoice-use-cases.di.ts similarity index 100% rename from modules/customer-invoices/src/api/application/issued-invoices/di/use-cases.di.ts rename to modules/customer-invoices/src/api/application/issued-invoices/di/issued-invoice-use-cases.di.ts diff --git a/modules/customer-invoices/src/api/application/issued-invoices/dtos/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/dtos/index.ts index bca58664..7e005094 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/dtos/index.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/dtos/index.ts @@ -1 +1 @@ -export * from "./sign-document-command"; +export * from "./issued-invoice-list.dto"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/dtos/issued-invoice-list.dto.ts b/modules/customer-invoices/src/api/application/issued-invoices/dtos/issued-invoice-list.dto.ts new file mode 100644 index 00000000..ef7dbf2f --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/dtos/issued-invoice-list.dto.ts @@ -0,0 +1,43 @@ +import type { CurrencyCode, LanguageCode, Percentage, UniqueID, UtcDate } from "@repo/rdx-ddd"; +import type { Maybe } from "@repo/rdx-utils"; + +import type { + InvoiceAmount, + InvoiceNumber, + InvoiceRecipient, + InvoiceSerie, + InvoiceStatus, + VerifactuRecord, +} from "../../../domain"; + +export type IssuedInvoiceListDTO = { + id: UniqueID; + companyId: UniqueID; + + isProforma: boolean; + invoiceNumber: InvoiceNumber; + status: InvoiceStatus; + series: Maybe; + + invoiceDate: UtcDate; + operationDate: Maybe; + + reference: Maybe; + description: Maybe; + + customerId: UniqueID; + recipient: InvoiceRecipient; + + languageCode: LanguageCode; + currencyCode: CurrencyCode; + + discountPercentage: Percentage; + + subtotalAmount: InvoiceAmount; + discountAmount: InvoiceAmount; + taxableAmount: InvoiceAmount; + taxesAmount: InvoiceAmount; + totalAmount: InvoiceAmount; + + verifactu: Maybe; +}; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/dtos/sign-document-command.ts b/modules/customer-invoices/src/api/application/issued-invoices/dtos/sign-document-command.ts deleted file mode 100644 index c5029ef0..00000000 --- a/modules/customer-invoices/src/api/application/issued-invoices/dtos/sign-document-command.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * DTO de Application para solicitar la firma de un documento. - * - * Se utiliza exclusivamente para intercambiar datos con la capa - * de infraestructura (DocumentSigningService). - * - * No contiene lógica ni validaciones de negocio. - */ -export interface SignDocumentCommand { - /** PDF sin firmar */ - readonly file: Buffer; - - /** Identificador estable de la empresa */ - readonly companyId: string; -} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/index.ts index 1a960d9a..680f4d36 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/index.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/index.ts @@ -1,5 +1,8 @@ export * from "./application-models"; export * from "./di"; +export * from "./dtos"; +export * from "./mappers"; +export * from "./repositories"; export * from "./services"; export * from "./snapshot-builders"; export * from "./use-cases"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/mappers/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/mappers/index.ts new file mode 100644 index 00000000..9c8fdfbb --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/mappers/index.ts @@ -0,0 +1,2 @@ +export * from "./issued-invoice-domain-mapper.interface"; +export * from "./issued-invoice-list-mapper.interface"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/mappers/issued-invoice-domain-mapper.interface.ts b/modules/customer-invoices/src/api/application/issued-invoices/mappers/issued-invoice-domain-mapper.interface.ts new file mode 100644 index 00000000..ebeca5bd --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/mappers/issued-invoice-domain-mapper.interface.ts @@ -0,0 +1,9 @@ +import type { MapperParamsType } from "@erp/core/api"; +import type { Result } from "@repo/rdx-utils"; + +import type { IssuedInvoice } from "../../../domain"; + +export interface IIssuedInvoiceDomainMapper { + mapToPersistence(invoice: IssuedInvoice, params?: MapperParamsType): Result; + mapToDomain(raw: unknown, params?: MapperParamsType): Result; +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/mappers/issued-invoice-list-mapper.interface.ts b/modules/customer-invoices/src/api/application/issued-invoices/mappers/issued-invoice-list-mapper.interface.ts new file mode 100644 index 00000000..c0e4334c --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/mappers/issued-invoice-list-mapper.interface.ts @@ -0,0 +1,8 @@ +import type { MapperParamsType } from "@erp/core/api"; +import type { Result } from "@repo/rdx-utils"; + +import type { IssuedInvoiceListDTO } from "../dtos"; + +export interface IIssuedInvoiceListMapper { + mapToDTO(raw: unknown, params?: MapperParamsType): Result; +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/repositories/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/repositories/index.ts new file mode 100644 index 00000000..1cb7405c --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/repositories/index.ts @@ -0,0 +1 @@ +export * from "./issued-invoice-repository.interface"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/repositories/issued-invoice-repository.interface.ts b/modules/customer-invoices/src/api/application/issued-invoices/repositories/issued-invoice-repository.interface.ts new file mode 100644 index 00000000..5d1d314a --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/repositories/issued-invoice-repository.interface.ts @@ -0,0 +1,22 @@ +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 { IssuedInvoice } from "../../../domain"; +import type { IssuedInvoiceListDTO } from "../dtos"; + +export interface IIssuedInvoiceRepository { + create(invoice: IssuedInvoice, transaction?: unknown): Promise>; + + getByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction: unknown + ): Promise>; + + findByCriteriaInCompany( + companyId: UniqueID, + criteria: Criteria, + transaction: unknown + ): Promise, Error>>; +} 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 ea1cbc4e..4559ef72 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,5 @@ 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-document-renderer.interface"; export * from "./issued-invoice-finder"; +export * from "./proforma-to-issued-invoice-materializer"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-document-renderer.interface.ts b/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-document-renderer.interface.ts deleted file mode 100644 index 7c55d363..00000000 --- a/modules/customer-invoices/src/api/application/issued-invoices/services/issued-invoice-document-renderer.interface.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Result } from "@repo/rdx-utils"; - -import type { IIssuedInvoiceDocument, IssuedInvoiceReportSnapshot } from "../application-models"; - -export interface IIssuedInvoiceDocumentRenderer { - render(input: { - snapshot: IssuedInvoiceReportSnapshot; - documentId: string; - }): Promise>; -} 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 37b515f6..44e0bc98 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 @@ -4,14 +4,14 @@ import type { UniqueID } from "@repo/rdx-ddd"; import type { Collection, Result } from "@repo/rdx-utils"; import type { Transaction } from "sequelize"; -import type { CustomerInvoice, ICustomerInvoiceRepository } from "../../../domain"; +import type { ICustomerInvoiceRepository, Proforma } from "../../../domain"; export interface IIssuedInvoiceFinder { findIssuedInvoiceById( companyId: UniqueID, invoiceId: UniqueID, transaction?: Transaction - ): Promise>; + ): Promise>; issuedInvoiceExists( companyId: UniqueID, @@ -33,18 +33,10 @@ export class IssuedInvoiceFinder implements IIssuedInvoiceFinder { companyId: UniqueID, invoiceId: UniqueID, transaction?: Transaction - ): Promise> { + ): Promise> { return this.repository.getIssuedInvoiceByIdInCompany(companyId, invoiceId, transaction, {}); } - async findProformaById( - companyId: UniqueID, - proformaId: UniqueID, - transaction?: Transaction - ): Promise> { - return this.repository.getProformaByIdInCompany(companyId, proformaId, transaction, {}); - } - async issuedInvoiceExists( companyId: UniqueID, invoiceId: UniqueID, @@ -55,16 +47,6 @@ export class IssuedInvoiceFinder implements IIssuedInvoiceFinder { }); } - async proformaExists( - companyId: UniqueID, - proformaId: UniqueID, - transaction?: Transaction - ): Promise> { - return this.repository.existsByIdInCompany(companyId, proformaId, transaction, { - is_proforma: true, - }); - } - async findIssuedInvoicesByCriteria( companyId: UniqueID, criteria: Criteria, @@ -77,12 +59,4 @@ export class IssuedInvoiceFinder implements IIssuedInvoiceFinder { {} ); } - - async findProformasByCriteria( - companyId: UniqueID, - criteria: Criteria, - transaction?: Transaction - ): Promise, Error>> { - return this.repository.findProformasByCriteriaInCompany(companyId, criteria, transaction, {}); - } } 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 index e1723728..0a6ecd77 100644 --- 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 @@ -1,11 +1,7 @@ import type { UniqueID } from "@repo/rdx-ddd"; import type { Maybe, Result } from "@repo/rdx-utils"; -import type { - CustomerInvoiceNumber, - CustomerInvoiceSerie, - ICustomerInvoiceNumberGenerator, -} from "../../../domain"; +import type { ICustomerInvoiceNumberGenerator, InvoiceNumber, InvoiceSerie } from "../../../domain"; export interface IIssuedInvoiceNumberService { /** @@ -13,9 +9,9 @@ export interface IIssuedInvoiceNumberService { */ nextIssuedInvoiceNumber( companyId: UniqueID, - series: Maybe, + series: Maybe, transaction: unknown - ): Promise>; + ): Promise>; } export class IssuedInvoiceNumberService implements IIssuedInvoiceNumberService { @@ -23,9 +19,9 @@ export class IssuedInvoiceNumberService implements IIssuedInvoiceNumberService { async nextIssuedInvoiceNumber( companyId: UniqueID, - series: Maybe, + series: Maybe, transaction: unknown - ): Promise> { + ): Promise> { return this.numberGenerator.nextForCompany(companyId, series, transaction); } } 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 new file mode 100644 index 00000000..6f428db3 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/services/proforma-to-issued-invoice-materializer.ts @@ -0,0 +1,56 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import { Collection, Result } from "@repo/rdx-utils"; + +import type { IssuedInvoiceProps, 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.discountPercentage, + + 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/snapshot-builders/full/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/index.ts index c0dd789a..009bc206 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/index.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/index.ts @@ -1,4 +1,8 @@ +export * from "./issued-invoice-full-snapshot.interface"; export * from "./issued-invoice-full-snapshot-builder"; +export * from "./issued-invoice-item-full-snapshot.interface"; export * from "./issued-invoice-items-full-snapshot-builder"; +export * from "./issued-invoice-recipient-full-snapshot.interfce"; export * from "./issued-invoice-recipient-full-snapshot-builder"; +export * from "./issued-invoice-verifactu-full-snapshot.interface"; export * from "./issued-invoice-verifactu-full-snapshot-builder"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-full-snapshot-builder.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-full-snapshot-builder.ts index 48f6393b..07fb7784 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-full-snapshot-builder.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-full-snapshot-builder.ts @@ -1,15 +1,15 @@ import type { ISnapshotBuilder } from "@erp/core/api"; import { toEmptyString } from "@repo/rdx-ddd"; -import { type CustomerInvoice, InvoiceAmount } from "../../../../domain"; -import type { IssuedInvoiceFullSnapshot } from "../../application-models"; +import { InvoiceAmount, type Proforma } from "../../../../domain"; +import type { IssuedInvoiceFullSnapshot } from "./issued-invoice-full-snapshot.interface"; import type { IIssuedInvoiceItemsFullSnapshotBuilder } from "./issued-invoice-items-full-snapshot-builder"; import type { IIssuedInvoiceRecipientFullSnapshotBuilder } from "./issued-invoice-recipient-full-snapshot-builder"; import type { IIssuedInvoiceVerifactuFullSnapshotBuilder } from "./issued-invoice-verifactu-full-snapshot-builder"; export interface IIssuedInvoiceFullSnapshotBuilder - extends ISnapshotBuilder {} + extends ISnapshotBuilder {} export class IssuedInvoiceFullSnapshotBuilder implements IIssuedInvoiceFullSnapshotBuilder { constructor( @@ -18,7 +18,7 @@ export class IssuedInvoiceFullSnapshotBuilder implements IIssuedInvoiceFullSnaps private readonly verifactuBuilder: IIssuedInvoiceVerifactuFullSnapshotBuilder ) {} - toOutput(invoice: CustomerInvoice): IssuedInvoiceFullSnapshot { + toOutput(invoice: Proforma): IssuedInvoiceFullSnapshot { const items = this.itemsBuilder.toOutput(invoice.items); const recipient = this.recipientBuilder.toOutput(invoice); const verifactu = this.verifactuBuilder.toOutput(invoice); @@ -129,7 +129,6 @@ export class IssuedInvoiceFullSnapshotBuilder implements IIssuedInvoiceFullSnaps metadata: { entity: "issued-invoices", - link: "", }, }; } diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/full/issued-invoice-full-snapshot.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-full-snapshot.interface.ts similarity index 94% rename from modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/full/issued-invoice-full-snapshot.ts rename to modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-full-snapshot.interface.ts index 856474dd..e7689aef 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/full/issued-invoice-full-snapshot.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-full-snapshot.interface.ts @@ -1,6 +1,6 @@ -import type { IssuedInvoiceItemFullSnapshot } from "./issued-invoice-item-full-snapshot"; -import type { IssuedInvoiceRecipientFullSnapshot } from "./issued-invoice-recipient-full-snapshot"; -import type { IssuedInvoiceVerifactuFullSnapshot } from "./issued-invoice-verifactu-full-snapshot"; +import type { IssuedInvoiceItemFullSnapshot } from "./issued-invoice-item-full-snapshot.interface"; +import type { IssuedInvoiceRecipientFullSnapshot } from "./issued-invoice-recipient-full-snapshot.interfce"; +import type { IssuedInvoiceVerifactuFullSnapshot } from "./issued-invoice-verifactu-full-snapshot.interface"; export interface IssuedInvoiceFullSnapshot { id: string; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/full/issued-invoice-item-full-snapshot.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-item-full-snapshot.interface.ts similarity index 100% rename from modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/full/issued-invoice-item-full-snapshot.ts rename to modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-item-full-snapshot.interface.ts diff --git a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-items-full-snapshot-builder.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-items-full-snapshot-builder.ts index e8985fac..02ff3f5c 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-items-full-snapshot-builder.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-items-full-snapshot-builder.ts @@ -1,7 +1,7 @@ import type { ISnapshotBuilder } from "@erp/core/api"; import { toEmptyString } from "@repo/rdx-ddd"; -import type { CustomerInvoiceItem, CustomerInvoiceItems } from "../../../../domain"; +import type { CustomerInvoiceItems, IssuedInvoiceItem } from "../../../../domain"; import type { IssuedInvoiceItemFullSnapshot } from "../../application-models"; export interface IIssuedInvoiceItemsFullSnapshotBuilder @@ -10,7 +10,7 @@ export interface IIssuedInvoiceItemsFullSnapshotBuilder export class IssuedInvoiceItemsFullSnapshotBuilder implements IIssuedInvoiceItemsFullSnapshotBuilder { - private mapItem(invoiceItem: CustomerInvoiceItem, index: number): IssuedInvoiceItemFullSnapshot { + private mapItem(invoiceItem: IssuedInvoiceItem, index: number): IssuedInvoiceItemFullSnapshot { const allAmounts = invoiceItem.calculateAllAmounts(); return { diff --git a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-recipient-full-snapshot-builder.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-recipient-full-snapshot-builder.ts index d40faebf..468a1407 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-recipient-full-snapshot-builder.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-recipient-full-snapshot-builder.ts @@ -1,16 +1,16 @@ import type { ISnapshotBuilder } from "@erp/core/api"; import { DomainValidationError, toEmptyString } from "@repo/rdx-ddd"; -import type { CustomerInvoice, InvoiceRecipient } from "../../../../domain"; +import type { InvoiceRecipient, Proforma } from "../../../../domain"; import type { IssuedInvoiceRecipientFullSnapshot } from "../../application-models"; export interface IIssuedInvoiceRecipientFullSnapshotBuilder - extends ISnapshotBuilder {} + extends ISnapshotBuilder {} export class IssuedInvoiceRecipientFullSnapshotBuilder implements IIssuedInvoiceRecipientFullSnapshotBuilder { - toOutput(invoice: CustomerInvoice): IssuedInvoiceRecipientFullSnapshot { + toOutput(invoice: Proforma): IssuedInvoiceRecipientFullSnapshot { if (!invoice.recipient) { throw DomainValidationError.requiredValue("recipient", { cause: invoice, diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/full/issued-invoice-recipient-full-snapshot.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-recipient-full-snapshot.interfce.ts similarity index 100% rename from modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/full/issued-invoice-recipient-full-snapshot.ts rename to modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-recipient-full-snapshot.interfce.ts diff --git a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-verifactu-full-snapshot-builder.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-verifactu-full-snapshot-builder.ts index 9885c067..ba8d499c 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-verifactu-full-snapshot-builder.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-verifactu-full-snapshot-builder.ts @@ -1,16 +1,16 @@ import type { ISnapshotBuilder } from "@erp/core/api"; import { DomainValidationError } from "@repo/rdx-ddd"; -import type { CustomerInvoice } from "../../../../domain"; +import type { Proforma } from "../../../../domain"; import type { IssuedInvoiceVerifactuFullSnapshot } from "../../application-models"; export interface IIssuedInvoiceVerifactuFullSnapshotBuilder - extends ISnapshotBuilder {} + extends ISnapshotBuilder {} export class IssuedInvoiceVerifactuFullSnapshotBuilder implements IIssuedInvoiceVerifactuFullSnapshotBuilder { - toOutput(invoice: CustomerInvoice): IssuedInvoiceVerifactuFullSnapshot { + toOutput(invoice: Proforma): IssuedInvoiceVerifactuFullSnapshot { if (!invoice.verifactu) { throw DomainValidationError.requiredValue("verifactu", { cause: invoice, diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/full/issued-invoice-verifactu-full-snapshot.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-verifactu-full-snapshot.interface.ts similarity index 100% rename from modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/full/issued-invoice-verifactu-full-snapshot.ts rename to modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-verifactu-full-snapshot.interface.ts diff --git a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/list/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/list/index.ts index 8828ec69..c3dabd99 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/list/index.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/list/index.ts @@ -1 +1,2 @@ +export * from "./issued-invoice-list-item-snapshot.interface"; export * from "./issued-invoice-list-item-snapshot-builder"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/list/issued-invoice-list-item-snapshot-builder.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/list/issued-invoice-list-item-snapshot-builder.ts index 0cd9e184..b3ba47c4 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/list/issued-invoice-list-item-snapshot-builder.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/list/issued-invoice-list-item-snapshot-builder.ts @@ -2,7 +2,7 @@ import type { ISnapshotBuilder } from "@erp/core/api"; import { toEmptyString } from "@repo/rdx-ddd"; import type { CustomerInvoiceListDTO } from "../../../../infrastructure"; -import type { IssuedInvoiceListItemSnapshot } from "../../application-models/snapshots/list"; +import type { IssuedInvoiceListItemSnapshot } from "../../application-models"; export interface IIssuedInvoiceListItemSnapshotBuilder extends ISnapshotBuilder {} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/list/issued-invoice-list-item-snapshot.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/list/issued-invoice-list-item-snapshot.interface.ts similarity index 100% rename from modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/list/issued-invoice-list-item-snapshot.ts rename to modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/list/issued-invoice-list-item-snapshot.interface.ts diff --git a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/report/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/report/index.ts index fe88cbc8..9666d1e1 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/report/index.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/report/index.ts @@ -1,3 +1,6 @@ export * from "./issued-invoice-items-report-snapshot-builder"; +export * from "./issued-invoice-report-item-snapshot.interface"; +export * from "./issued-invoice-report-snapshot.interface"; export * from "./issued-invoice-report-snapshot-builder"; +export * from "./issued-invoice-report-tax-snapshot.interface"; export * from "./issued-invoice-tax-report-snapshot-builder"; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/report/issued-invoice-report-item-snapshot.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/report/issued-invoice-report-item-snapshot.interface.ts similarity index 100% rename from modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/report/issued-invoice-report-item-snapshot.ts rename to modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/report/issued-invoice-report-item-snapshot.interface.ts diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/report/issued-invoice-report-snapshot.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/report/issued-invoice-report-snapshot.interface.ts similarity index 91% rename from modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/report/issued-invoice-report-snapshot.ts rename to modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/report/issued-invoice-report-snapshot.interface.ts index 6a29596c..f8ce6443 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/report/issued-invoice-report-snapshot.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/report/issued-invoice-report-snapshot.interface.ts @@ -1,5 +1,5 @@ -import type { IssuedInvoiceReportItemSnapshot } from "./issued-invoice-report-item-snapshot"; -import type { IssuedInvoiceReportTaxSnapshot } from "./issued-invoice-report-tax-snapshot"; +import type { IssuedInvoiceReportItemSnapshot } from "./issued-invoice-report-item-snapshot.interface"; +import type { IssuedInvoiceReportTaxSnapshot } from "./issued-invoice-report-tax-snapshot.interface"; export interface IssuedInvoiceReportSnapshot { id: string; diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/report/issued-invoice-report-tax-snapshot.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/report/issued-invoice-report-tax-snapshot.interface.ts similarity index 100% rename from modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/report/issued-invoice-report-tax-snapshot.ts rename to modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/report/issued-invoice-report-tax-snapshot.interface.ts diff --git a/modules/customer-invoices/src/api/application/issued-invoices/use-cases/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/use-cases/index.ts index e8d3ab9d..d368a837 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/use-cases/index.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/use-cases/index.ts @@ -1,3 +1,3 @@ export * from "./get-issued-invoice-by-id.use-case"; export * from "./list-issued-invoices.use-case"; -export * from "./report-issued-invoices"; +export * from "./report-issued-invoice.use-case"; 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 da9f1376..ebe0794f 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 @@ -15,7 +15,7 @@ type ListIssuedInvoicesUseCaseInput = { export class ListIssuedInvoicesUseCase { constructor( private readonly finder: IIssuedInvoiceFinder, - private readonly itemSnapshotBuilder: IIssuedInvoiceListItemSnapshotBuilder, + private readonly listItemSnapshotBuilder: IIssuedInvoiceListItemSnapshotBuilder, private readonly transactionManager: ITransactionManager ) {} @@ -37,9 +37,8 @@ export class ListIssuedInvoicesUseCase { const invoices = result.data; const totalInvoices = invoices.total(); - const items = invoices.map((item) => this.itemSnapshotBuilder.toOutput(item)); + const items = invoices.map((item) => this.listItemSnapshotBuilder.toOutput(item)); - // ????? const snapshot = { page: criteria.pageNumber, per_page: criteria.pageSize, diff --git a/modules/customer-invoices/src/api/application/issued-invoices/use-cases/report-issued-invoices/report-issued-invoice.use-case.ts b/modules/customer-invoices/src/api/application/issued-invoices/use-cases/report-issued-invoice.use-case.ts similarity index 89% rename from modules/customer-invoices/src/api/application/issued-invoices/use-cases/report-issued-invoices/report-issued-invoice.use-case.ts rename to modules/customer-invoices/src/api/application/issued-invoices/use-cases/report-issued-invoice.use-case.ts index 13533295..8a782d74 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/use-cases/report-issued-invoices/report-issued-invoice.use-case.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/use-cases/report-issued-invoice.use-case.ts @@ -2,9 +2,9 @@ import type { ITransactionManager, RendererFormat } from "@erp/core/api"; import { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import type { IIssuedInvoiceFinder, IssuedInvoiceDocumentGeneratorService } from "../../services"; -import type { IIssuedInvoiceFullSnapshotBuilder } from "../../snapshot-builders"; -import type { IIssuedInvoiceReportSnapshotBuilder } from "../../snapshot-builders/report"; +import type { IIssuedInvoiceFinder, IssuedInvoiceDocumentGeneratorService } from "../services"; +import type { IIssuedInvoiceFullSnapshotBuilder } from "../snapshot-builders"; +import type { IIssuedInvoiceReportSnapshotBuilder } from "../snapshot-builders/report"; type ReportIssuedInvoiceUseCaseInput = { companyId: UniqueID; @@ -23,7 +23,7 @@ export class ReportIssuedInvoiceUseCase { ) {} public async execute(params: ReportIssuedInvoiceUseCaseInput) { - const { invoice_id, companyId, companySlug, format } = params; + const { invoice_id, companyId } = params; const idOrError = UniqueID.create(invoice_id); diff --git a/modules/customer-invoices/src/api/application/issued-invoices/use-cases/report-issued-invoices/index.ts b/modules/customer-invoices/src/api/application/issued-invoices/use-cases/report-issued-invoices/index.ts deleted file mode 100644 index 9d695a7a..00000000 --- a/modules/customer-invoices/src/api/application/issued-invoices/use-cases/report-issued-invoices/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -//export * from "./issued-invoice-document"; -export * from "./report-issued-invoice.use-case"; diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/index.ts b/modules/customer-invoices/src/api/application/proformas/application-models/index.ts similarity index 100% rename from modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/index.ts rename to modules/customer-invoices/src/api/application/proformas/application-models/index.ts diff --git a/modules/customer-invoices/src/api/application/proformas/di/index.ts b/modules/customer-invoices/src/api/application/proformas/di/index.ts new file mode 100644 index 00000000..7a607d8b --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/di/index.ts @@ -0,0 +1,4 @@ +export * from "./proforma-creator.di"; +export * from "./proforma-finder.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-creator.di.ts b/modules/customer-invoices/src/api/application/proformas/di/proforma-creator.di.ts new file mode 100644 index 00000000..d26ddf85 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/di/proforma-creator.di.ts @@ -0,0 +1,14 @@ +import type { ICustomerInvoiceRepository } from "../../../domain/repositories"; +import { ProformaFactory } from "../factories"; +import { type IProformaCreator, type IProformaNumberGenerator, ProformaCreator } from "../services"; + +export function buildProformaCreator( + numberService: IProformaNumberGenerator, + repository: ICustomerInvoiceRepository +): IProformaCreator { + return new ProformaCreator({ + numberService, + factory: new ProformaFactory(), + repository, + }); +} diff --git a/modules/customer-invoices/src/api/application/proformas/di/proforma-finder.di.ts b/modules/customer-invoices/src/api/application/proformas/di/proforma-finder.di.ts new file mode 100644 index 00000000..1333e18b --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/di/proforma-finder.di.ts @@ -0,0 +1,6 @@ +import type { ICustomerInvoiceRepository } from "../../../domain"; +import { type IProformaFinder, ProformaFinder } from "../services"; + +export function buildProformaFinder(repository: ICustomerInvoiceRepository): IProformaFinder { + return new ProformaFinder(repository); +} diff --git a/modules/customer-invoices/src/api/application/proformas/di/proforma-snapshot-builders.di.ts b/modules/customer-invoices/src/api/application/proformas/di/proforma-snapshot-builders.di.ts new file mode 100644 index 00000000..c6b32c3d --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/di/proforma-snapshot-builders.di.ts @@ -0,0 +1,34 @@ +// application/issued-invoices/di/snapshot-builders.di.ts + +import { + ProformaFullSnapshotBuilder, + ProformaItemReportSnapshotBuilder, + ProformaItemsFullSnapshotBuilder, + ProformaListItemSnapshotBuilder, + ProformaRecipientFullSnapshotBuilder, + ProformaReportSnapshotBuilder, + ProformaTaxReportSnapshotBuilder, +} from "../snapshot-builders"; + +export function buildProformaSnapshotBuilders() { + const itemsBuilder = new ProformaItemsFullSnapshotBuilder(); + + const recipientBuilder = new ProformaRecipientFullSnapshotBuilder(); + + const fullSnapshotBuilder = new ProformaFullSnapshotBuilder(itemsBuilder, recipientBuilder); + + const listSnapshotBuilder = new ProformaListItemSnapshotBuilder(); + + const itemsReportBuilder = new ProformaItemReportSnapshotBuilder(); + const taxesReportBuilder = new ProformaTaxReportSnapshotBuilder(); + const reportSnapshotBuilder = new ProformaReportSnapshotBuilder( + itemsReportBuilder, + taxesReportBuilder + ); + + return { + full: fullSnapshotBuilder, + list: listSnapshotBuilder, + report: reportSnapshotBuilder, + }; +} 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 new file mode 100644 index 00000000..166ab095 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/di/proforma-use-cases.di.ts @@ -0,0 +1,76 @@ +import type { ITransactionManager } from "@erp/core/api"; + +import type { IProformaFinder, ProformaDocumentGeneratorService } from "../services"; +import type { + IProformaListItemSnapshotBuilder, + IProformaReportSnapshotBuilder, +} from "../snapshot-builders"; +import type { IProformaFullSnapshotBuilder } from "../snapshot-builders/full"; +import { GetProformaByIdUseCase, ListProformasUseCase, ReportProformaUseCase } from "../use-cases"; + +export function buildGetProformaByIdUseCase(deps: { + finder: IProformaFinder; + fullSnapshotBuilder: IProformaFullSnapshotBuilder; + transactionManager: ITransactionManager; +}) { + return new GetProformaByIdUseCase(deps.finder, deps.fullSnapshotBuilder, deps.transactionManager); +} + +export function buildListProformasUseCase(deps: { + finder: IProformaFinder; + itemSnapshotBuilder: IProformaListItemSnapshotBuilder; + transactionManager: ITransactionManager; +}) { + return new ListProformasUseCase(deps.finder, deps.itemSnapshotBuilder, deps.transactionManager); +} + +export function buildReportProformaUseCase(deps: { + finder: IProformaFinder; + fullSnapshotBuilder: IProformaFullSnapshotBuilder; + reportSnapshotBuilder: IProformaReportSnapshotBuilder; + documentService: ProformaDocumentGeneratorService; + transactionManager: ITransactionManager; +}) { + return new ReportProformaUseCase( + deps.finder, + deps.fullSnapshotBuilder, + deps.reportSnapshotBuilder, + deps.documentService, + deps.transactionManager + ); +} + +/*export function buildCreateProformaUseCase(deps: { + creator: IProformaCreator; + fullSnapshotBuilder: IProformaFullSnapshotBuilder; + transactionManager: ITransactionManager; +}) { + return new CreateProformaUseCase({ + mapper: new CreateProformaPropsMapper(), + creator: deps.creator, + fullSnapshotBuilder: deps.fullSnapshotBuilder, + transactionManager: deps.transactionManager, + }); +}*/ + +/*export function buildUpdateProformaUseCase(deps: { + finder: IProformaFinder; + fullSnapshotBuilder: IProformaFullSnapshotBuilder; +}) { + return new UpdateProformaUseCase(deps.finder, deps.fullSnapshotBuilder); +} + +export function buildDeleteProformaUseCase(deps: { finder: IProformaFinder }) { + return new DeleteProformaUseCase(deps.finder); +} + +export function buildIssueProformaUseCase(deps: { finder: IProformaFinder }) { + return new IssueProformaUseCase(deps.finder); +} + +export function buildChangeStatusProformaUseCase(deps: { + finder: IProformaFinder; + transactionManager: ITransactionManager; +}) { + return new ChangeStatusProformaUseCase(deps.finder, deps.transactionManager); +}*/ diff --git a/modules/customer-invoices/src/api/application/proformas/dtos/index.ts b/modules/customer-invoices/src/api/application/proformas/dtos/index.ts new file mode 100644 index 00000000..4d243ca9 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/dtos/index.ts @@ -0,0 +1 @@ +export * from "./proforma-list.dto"; diff --git a/modules/customer-invoices/src/api/application/proformas/dtos/proforma-list.dto.ts b/modules/customer-invoices/src/api/application/proformas/dtos/proforma-list.dto.ts new file mode 100644 index 00000000..41eb8ef8 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/dtos/proforma-list.dto.ts @@ -0,0 +1,40 @@ +import type { CurrencyCode, LanguageCode, Percentage, UniqueID, UtcDate } from "@repo/rdx-ddd"; +import type { Maybe } from "@repo/rdx-utils"; + +import type { + InvoiceAmount, + InvoiceNumber, + InvoiceRecipient, + InvoiceSerie, + InvoiceStatus, +} from "../../../domain"; + +export type ProformaListDTO = { + id: UniqueID; + companyId: UniqueID; + + isProforma: boolean; + invoiceNumber: InvoiceNumber; + status: InvoiceStatus; + series: Maybe; + + invoiceDate: UtcDate; + operationDate: Maybe; + + reference: Maybe; + description: Maybe; + + customerId: UniqueID; + recipient: InvoiceRecipient; + + languageCode: LanguageCode; + currencyCode: CurrencyCode; + + discountPercentage: Percentage; + + subtotalAmount: InvoiceAmount; + discountAmount: InvoiceAmount; + taxableAmount: InvoiceAmount; + taxesAmount: InvoiceAmount; + totalAmount: InvoiceAmount; +}; diff --git a/modules/customer-invoices/src/api/application/proformas/factories/index.ts b/modules/customer-invoices/src/api/application/proformas/factories/index.ts new file mode 100644 index 00000000..3ec988be --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/factories/index.ts @@ -0,0 +1,2 @@ +export * from "./proforma-factory"; +export * from "./proforma-factory.interface"; diff --git a/modules/customer-invoices/src/api/application/proformas/factories/proforma-factory.interface.ts b/modules/customer-invoices/src/api/application/proformas/factories/proforma-factory.interface.ts new file mode 100644 index 00000000..49887c79 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/factories/proforma-factory.interface.ts @@ -0,0 +1,17 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import type { Result } from "@repo/rdx-utils"; + +import type { IProformaProps, Proforma } from "../../../domain"; + +export interface IProformaFactory { + /** + * Crea una proforma válida para una empresa a partir de props ya validadas. + * + * No persiste el agregado. + */ + createProforma( + companyId: UniqueID, + props: Omit, + proformaId?: UniqueID + ): Result; +} diff --git a/modules/customer-invoices/src/api/application/proformas/factories/proforma-factory.ts b/modules/customer-invoices/src/api/application/proformas/factories/proforma-factory.ts new file mode 100644 index 00000000..b404a811 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/factories/proforma-factory.ts @@ -0,0 +1,16 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import type { Result } from "@repo/rdx-utils"; + +import { type IProformaProps, Proforma } from "../../../domain"; + +import type { IProformaFactory } from "./proforma-factory.interface"; + +export class ProformaFactory implements IProformaFactory { + createProforma( + companyId: UniqueID, + props: Omit, + proformaId?: UniqueID + ): Result { + return Proforma.create({ ...props, companyId }, proformaId); + } +} diff --git a/modules/customer-invoices/src/api/application/proformas/index.ts b/modules/customer-invoices/src/api/application/proformas/index.ts new file mode 100644 index 00000000..680f4d36 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/index.ts @@ -0,0 +1,8 @@ +export * from "./application-models"; +export * from "./di"; +export * from "./dtos"; +export * from "./mappers"; +export * from "./repositories"; +export * from "./services"; +export * from "./snapshot-builders"; +export * from "./use-cases"; diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/create-proforma/map-dto-to-create-proforma-props.ts b/modules/customer-invoices/src/api/application/proformas/mappers/create-proforma-props.mapper.ts similarity index 88% rename from modules/customer-invoices/src/api/application/use-cases/proformas/create-proforma/map-dto-to-create-proforma-props.ts rename to modules/customer-invoices/src/api/application/proformas/mappers/create-proforma-props.mapper.ts index 44c4bbee..2ee2d7fe 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/create-proforma/map-dto-to-create-proforma-props.ts +++ b/modules/customer-invoices/src/api/application/proformas/mappers/create-proforma-props.mapper.ts @@ -15,24 +15,25 @@ import { } from "@repo/rdx-ddd"; import { Maybe, Result } from "@repo/rdx-utils"; -import type { CreateProformaItemRequestDTO, CreateProformaRequestDTO } from "../../../../../common"; +import type { CreateProformaItemRequestDTO, CreateProformaRequestDTO } from "../../../../common"; import { - CustomerInvoiceItem, - CustomerInvoiceItemDescription, - type CustomerInvoiceItemProps, CustomerInvoiceItems, - CustomerInvoiceNumber, - type CustomerInvoiceProps, - CustomerInvoiceSerie, - CustomerInvoiceStatus, + type IProformaProps, + InvoiceNumber, InvoicePaymentMethod, type InvoiceRecipient, + InvoiceSerie, + InvoiceStatus, + IssuedInvoiceItem, + type IssuedInvoiceItemProps, ItemAmount, + ItemDescription, ItemDiscount, ItemQuantity, -} from "../../../../domain"; +} from "../../../domain"; /** + * CreateProformaPropsMapper * Convierte el DTO a las props validadas (CustomerProps). * No construye directamente el agregado. * @@ -42,7 +43,7 @@ import { * */ -export class CreateCustomerInvoicePropsMapper { +export class CreateProformaPropsMapper { private readonly taxCatalog: JsonTaxCatalogProvider; private errors: ValidationErrorDetail[] = []; private languageCode?: LanguageCode; @@ -57,7 +58,7 @@ export class CreateCustomerInvoicePropsMapper { try { this.errors = []; - const defaultStatus = CustomerInvoiceStatus.createDraft(); + const defaultStatus = InvoiceStatus.createDraft(); const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", this.errors); @@ -72,13 +73,13 @@ export class CreateCustomerInvoicePropsMapper { const recipient = Maybe.none(); const proformaNumber = extractOrPushError( - CustomerInvoiceNumber.create(dto.invoice_number), + InvoiceNumber.create(dto.invoice_number), "invoice_number", this.errors ); const series = extractOrPushError( - maybeFromNullableVO(dto.series, (value) => CustomerInvoiceSerie.create(value)), + maybeFromNullableVO(dto.series, (value) => InvoiceSerie.create(value)), "series", this.errors ); @@ -150,7 +151,7 @@ export class CreateCustomerInvoicePropsMapper { ); } - const proformaProps: CustomerInvoiceProps = { + const proformaProps: IProformaProps = { companyId, isProforma, proformaId: Maybe.none(), @@ -194,9 +195,7 @@ export class CreateCustomerInvoicePropsMapper { items.forEach((item, index) => { const description = extractOrPushError( - maybeFromNullableVO(item.description, (value) => - CustomerInvoiceItemDescription.create(value) - ), + maybeFromNullableVO(item.description, (value) => ItemDescription.create(value)), "description", this.errors ); @@ -221,7 +220,7 @@ export class CreateCustomerInvoicePropsMapper { const taxes = this.mapTaxes(item, index); - const itemProps: CustomerInvoiceItemProps = { + const itemProps: IssuedInvoiceItemProps = { currencyCode: this.currencyCode!, languageCode: this.languageCode!, description: description!, @@ -231,7 +230,7 @@ export class CreateCustomerInvoicePropsMapper { taxes: taxes, }; - const itemResult = CustomerInvoiceItem.create(itemProps); + const itemResult = IssuedInvoiceItem.create(itemProps); if (itemResult.isSuccess) { invoiceItems.add(itemResult.data); } else { diff --git a/modules/customer-invoices/src/api/application/proformas/mappers/index.ts b/modules/customer-invoices/src/api/application/proformas/mappers/index.ts new file mode 100644 index 00000000..8e6fc996 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/mappers/index.ts @@ -0,0 +1,4 @@ +export * from "./create-proforma-props.mapper"; +export * from "./proforma-domain-mapper.interface"; +export * from "./proforma-list-mapper.interface"; +export * from "./update-proforma-props.mapper"; diff --git a/modules/customer-invoices/src/api/application/proformas/mappers/proforma-domain-mapper.interface.ts b/modules/customer-invoices/src/api/application/proformas/mappers/proforma-domain-mapper.interface.ts new file mode 100644 index 00000000..a849ae74 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/mappers/proforma-domain-mapper.interface.ts @@ -0,0 +1,9 @@ +import type { MapperParamsType } from "@erp/core/api"; +import type { Result } from "@repo/rdx-utils"; + +import type { Proforma } from "../../../domain"; + +export interface IProformaDomainMapper { + mapToPersistence(proforma: Proforma, params?: MapperParamsType): Result; + mapToDomain(raw: unknown, params?: MapperParamsType): Result; +} diff --git a/modules/customer-invoices/src/api/application/proformas/mappers/proforma-list-mapper.interface.ts b/modules/customer-invoices/src/api/application/proformas/mappers/proforma-list-mapper.interface.ts new file mode 100644 index 00000000..d2dc4668 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/mappers/proforma-list-mapper.interface.ts @@ -0,0 +1,8 @@ +import type { MapperParamsType } from "@erp/core/api"; +import type { Result } from "@repo/rdx-utils"; + +import type { ProformaListDTO } from "../dtos"; + +export interface IProformaListMapper { + mapToDTO(raw: unknown, params?: MapperParamsType): Result; +} diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/update-proforma/map-dto-to-update-customer-invoice-props.ts b/modules/customer-invoices/src/api/application/proformas/mappers/update-proforma-props.mapper.ts similarity index 96% rename from modules/customer-invoices/src/api/application/use-cases/proformas/update-proforma/map-dto-to-update-customer-invoice-props.ts rename to modules/customer-invoices/src/api/application/proformas/mappers/update-proforma-props.mapper.ts index 68a9ebf4..7b3f3168 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/update-proforma/map-dto-to-update-customer-invoice-props.ts +++ b/modules/customer-invoices/src/api/application/proformas/mappers/update-proforma-props.mapper.ts @@ -16,7 +16,7 @@ import type { UpdateProformaByIdRequestDTO } from "../../../../../common/dto"; import { type CustomerInvoicePatchProps, CustomerInvoiceSerie } from "../../../../domain"; /** - * mapDTOToUpdateCustomerInvoicePatchProps + * UpdateProformaPropsMapper * Convierte el DTO a las props validadas (CustomerInvoiceProps). * No construye directamente el agregado. * Tri-estado: @@ -29,7 +29,7 @@ import { type CustomerInvoicePatchProps, CustomerInvoiceSerie } from "../../../. * */ -export function mapDTOToUpdateCustomerInvoicePatchProps(dto: UpdateProformaByIdRequestDTO) { +export function UpdateProformaPropsMapper(dto: UpdateProformaByIdRequestDTO) { try { const errors: ValidationErrorDetail[] = []; const props: CustomerInvoicePatchProps = {}; diff --git a/modules/customer-invoices/src/api/application/proformas/repositories/index.ts b/modules/customer-invoices/src/api/application/proformas/repositories/index.ts new file mode 100644 index 00000000..477f94cd --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/repositories/index.ts @@ -0,0 +1 @@ +export * from "./proforma-repository.interface"; diff --git a/modules/customer-invoices/src/api/application/proformas/repositories/proforma-repository.interface.ts b/modules/customer-invoices/src/api/application/proformas/repositories/proforma-repository.interface.ts new file mode 100644 index 00000000..53e3ec99 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/repositories/proforma-repository.interface.ts @@ -0,0 +1,43 @@ +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 { InvoiceStatus, Proforma } from "../../../domain"; +import type { ProformaListDTO } from "../dtos"; + +export interface IProformaRepository { + create(proforma: Proforma, transaction?: unknown): Promise>; + + update(proforma: Proforma, transaction?: unknown): Promise>; + + existsByIdInCompany( + companyId: UniqueID, + id: UniqueID, + tx: unknown + ): Promise>; + + getByIdInCompany( + companyId: UniqueID, + id: UniqueID, + tx: unknown + ): Promise>; + + findByCriteriaInCompany( + companyId: UniqueID, + criteria: Criteria, + tx: unknown + ): Promise, Error>>; + + deleteByIdInCompany( + companyId: UniqueID, + id: UniqueID, + tx: unknown + ): Promise>; + + updateStatusByIdInCompany( + companyId: UniqueID, + id: UniqueID, + newStatus: InvoiceStatus, + tx: unknown + ): Promise>; +} diff --git a/modules/customer-invoices/src/api/application/proformas/services/index.ts b/modules/customer-invoices/src/api/application/proformas/services/index.ts new file mode 100644 index 00000000..5bb325e5 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/services/index.ts @@ -0,0 +1,7 @@ +export * from "./proforma-creator"; +export * from "./proforma-document-generator.interface"; +export * from "./proforma-document-metadata-factory"; +export * from "./proforma-document-properties-factory"; +export * from "./proforma-finder"; +export * from "./proforma-issuer"; +export * from "./proforma-number-generator.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 new file mode 100644 index 00000000..f782bd69 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/services/proforma-creator.ts @@ -0,0 +1,78 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; +import type { Transaction } from "sequelize"; + +import type { ICustomerInvoiceRepository } from "../../../domain"; +import type { CustomerInvoice, CustomerInvoiceProps } from "../../../domain/aggregates"; +import type { IProformaFactory } from "../factories"; + +import type { IProformaNumberGenerator } from "./proforma-number-generator.interface"; + +export interface IProformaCreator { + create( + companyId: UniqueID, + id: UniqueID, + props: CustomerInvoiceProps, + transaction: Transaction + ): Promise>; +} + +type ProformaCreatorDeps = { + numberService: IProformaNumberGenerator; + factory: IProformaFactory; + repository: ICustomerInvoiceRepository; +}; + +export class ProformaCreator implements IProformaCreator { + private readonly numberService: IProformaNumberGenerator; + private readonly factory: IProformaFactory; + private readonly repository: ICustomerInvoiceRepository; + + constructor(deps: ProformaCreatorDeps) { + this.numberService = deps.numberService; + this.factory = deps.factory; + this.repository = deps.repository; + } + + async create( + companyId: UniqueID, + id: UniqueID, + props: CustomerInvoiceProps, + transaction: Transaction + ): Promise> { + // 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; + + // 2. Crear agregado + const buildResult = this.factory.createProforma( + companyId, + { + ...props, + invoiceNumber, + }, + id + ); + + if (buildResult.isFailure) { + return Result.fail(buildResult.error); + } + + const proforma = buildResult.data; + + // 3. Persistir + const saveResult = await this.repository.create(proforma, transaction); + + if (saveResult.isFailure) { + return Result.fail(saveResult.error); + } + + return Result.ok(proforma); + } +} diff --git a/modules/customer-invoices/src/api/application/proformas/services/proforma-document-generator.interface.ts b/modules/customer-invoices/src/api/application/proformas/services/proforma-document-generator.interface.ts new file mode 100644 index 00000000..0233dc50 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/services/proforma-document-generator.interface.ts @@ -0,0 +1,6 @@ +import type { DocumentGenerationService } from "@erp/core/api"; + +import type { ProformaReportSnapshot } from "../application-models"; + +export interface ProformaDocumentGeneratorService + extends DocumentGenerationService {} diff --git a/modules/customer-invoices/src/api/application/proformas/services/proforma-document-metadata-factory.ts b/modules/customer-invoices/src/api/application/proformas/services/proforma-document-metadata-factory.ts new file mode 100644 index 00000000..503ca0fa --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/services/proforma-document-metadata-factory.ts @@ -0,0 +1,47 @@ +import type { IDocumentMetadata, IDocumentMetadataFactory } from "@erp/core/api"; + +import type { ProformaReportSnapshot } from "../application-models"; + +/** + * Construye los metadatos del documento PDF de una factura emitida. + * + * - Application-level + * - Determinista + * - Sin IO + */ +export class ProformaDocumentMetadataFactory + implements IDocumentMetadataFactory +{ + build(snapshot: ProformaReportSnapshot): IDocumentMetadata { + if (!snapshot.id) { + throw new Error("ProformaReportSnapshot.id is required"); + } + + if (!snapshot.company_id) { + throw new Error("ProformaReportSnapshot.companyId is required"); + } + + return { + documentType: "proforma", + documentId: snapshot.id, + companyId: snapshot.company_id, + companySlug: snapshot.company_slug, + format: "PDF", + languageCode: snapshot.language_code ?? "es", + filename: this.buildFilename(snapshot), + storageKey: this.buildCacheKey(snapshot), + }; + } + + private buildFilename(snapshot: ProformaReportSnapshot): string { + // Ejemplo: factura-F2024-000123-FULANITO.pdf + return `factura-${snapshot.series}${snapshot.invoice_number}-${snapshot.recipient.name}.pdf`; + } + + private buildCacheKey(snapshot: ProformaReportSnapshot): string { + // Versionado explícito para invalidaciones futuras + return ["proforma", snapshot.company_id, snapshot.series, snapshot.invoice_number, "v1"].join( + ":" + ); + } +} diff --git a/modules/customer-invoices/src/api/application/proformas/services/proforma-document-properties-factory.ts b/modules/customer-invoices/src/api/application/proformas/services/proforma-document-properties-factory.ts new file mode 100644 index 00000000..7a42fdf1 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/services/proforma-document-properties-factory.ts @@ -0,0 +1,23 @@ +import type { IDocumentProperties, IDocumentPropertiesFactory } from "@erp/core/api"; + +import type { ProformaReportSnapshot } from "../application-models"; + +/** + * Construye los metadatos del documento PDF de una factura emitida. + * + * - Application-level + * - Determinista + * - Sin IO + */ +export class ProformaDocumentPropertiesFactory + implements IDocumentPropertiesFactory +{ + build(snapshot: ProformaReportSnapshot): IDocumentProperties { + return { + title: snapshot.reference, + subject: "proforma", + author: snapshot.company_slug, + creator: "FactuGES ERP", + }; + } +} 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 new file mode 100644 index 00000000..b6eaeafd --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/services/proforma-finder.ts @@ -0,0 +1,57 @@ +import type { CustomerInvoiceListDTO } from "@erp/customer-invoices/api/infrastructure"; +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 { ICustomerInvoiceRepository, Proforma } from "../../../domain"; + +export interface IProformaFinder { + findProformaById( + companyId: UniqueID, + invoiceId: UniqueID, + transaction?: Transaction + ): Promise>; + + proformaExists( + companyId: UniqueID, + invoiceId: UniqueID, + transaction?: Transaction + ): Promise>; + + findProformasByCriteria( + companyId: UniqueID, + criteria: Criteria, + transaction?: Transaction + ): Promise, Error>>; +} + +export class ProformaFinder implements IProformaFinder { + constructor(private readonly repository: ICustomerInvoiceRepository) {} + + async findProformaById( + companyId: UniqueID, + proformaId: UniqueID, + transaction?: Transaction + ): Promise> { + return this.repository.getProformaByIdInCompany(companyId, proformaId, transaction, {}); + } + + async proformaExists( + companyId: UniqueID, + proformaId: UniqueID, + transaction?: Transaction + ): Promise> { + return this.repository.existsByIdInCompany(companyId, proformaId, transaction, { + is_proforma: true, + }); + } + + async findProformasByCriteria( + companyId: UniqueID, + criteria: Criteria, + transaction?: Transaction + ): Promise, Error>> { + return this.repository.findProformasByCriteriaInCompany(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 new file mode 100644 index 00000000..80ec781d --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/services/proforma-issuer.ts @@ -0,0 +1,44 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; +import type { Transaction } from "sequelize"; + +import type { IProformaToIssuedInvoiceMaterializer } from "../../issued-invoices"; + +export class ProformaIssuer implements IProformaIssuer { + private readonly proformaRepository: IProformaRepository; + private readonly issuedInvoiceFactory: IIssuedInvoiceFactory; + private readonly issuedInvoiceRepository: IIssuedInvoiceRepository; + private readonly materializer: IProformaToIssuedInvoiceMaterializer; + + constructor(deps: ProformaIssuerDeps) { + this.proformaRepository = deps.proformaRepository; + this.issuedInvoiceFactory = deps.issuedInvoiceFactory; + this.issuedInvoiceRepository = deps.issuedInvoiceRepository; + this.materializer = deps.materializer; + } + + public async issue( + proforma: Proforma, + issuedInvoiceId: UniqueID, + transaction: Transaction + ): Promise> { + 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); + } + + await this.issuedInvoiceRepository.save(proforma.companyId, invoiceResult.data, transaction); + + await this.proformaRepository.save(proforma.companyId, proforma, transaction); + + return Result.ok(proforma); + } +} diff --git a/modules/customer-invoices/src/api/domain/services/customer-invoice-number-generator.interface.ts b/modules/customer-invoices/src/api/application/proformas/services/proforma-number-generator.interface.ts similarity index 68% rename from modules/customer-invoices/src/api/domain/services/customer-invoice-number-generator.interface.ts rename to modules/customer-invoices/src/api/application/proformas/services/proforma-number-generator.interface.ts index 873b668d..23558770 100644 --- a/modules/customer-invoices/src/api/domain/services/customer-invoice-number-generator.interface.ts +++ b/modules/customer-invoices/src/api/application/proformas/services/proforma-number-generator.interface.ts @@ -1,12 +1,11 @@ +import type { InvoiceNumber, InvoiceSerie } from "@erp/customer-invoices/api/domain"; import type { UniqueID } from "@repo/rdx-ddd"; import type { Maybe, Result } from "@repo/rdx-utils"; -import type { CustomerInvoiceNumber, CustomerInvoiceSerie } from "../value-objects"; - /** * Servicio de dominio que define cómo se genera el siguiente número de factura. */ -export interface ICustomerInvoiceNumberGenerator { +export interface IProformaNumberGenerator { /** * Devuelve el siguiente número de factura disponible para una empresa dentro de una "serie" de factura. * @@ -14,9 +13,9 @@ export interface ICustomerInvoiceNumberGenerator { * @param serie - Serie por la que buscar la última factura * @param transaction - Transacción activa */ - nextForCompany( + getNextForCompany( companyId: UniqueID, - series: Maybe, + series: Maybe, transaction: any - ): Promise>; + ): Promise>; } diff --git a/modules/customer-invoices/src/api/application/services/proforma-write-service.ts b/modules/customer-invoices/src/api/application/proformas/services/proforma-write-service.ts similarity index 92% rename from modules/customer-invoices/src/api/application/services/proforma-write-service.ts rename to modules/customer-invoices/src/api/application/proformas/services/proforma-write-service.ts index 110015f0..5955ada9 100644 --- a/modules/customer-invoices/src/api/application/services/proforma-write-service.ts +++ b/modules/customer-invoices/src/api/application/proformas/services/proforma-write-service.ts @@ -4,18 +4,17 @@ import type { Transaction } from "sequelize"; import { CustomerInvoiceIsProformaSpecification, - type CustomerInvoiceStatus, + type InvoiceStatus, ProformaCannotBeDeletedError, StatusInvoiceIsDraftSpecification, -} from "../../domain"; +} from "../../../domain"; import type { CustomerInvoice, CustomerInvoicePatchProps, CustomerInvoiceProps, -} from "../../domain/aggregates"; -import type { ICustomerInvoiceRepository } from "../../domain/repositories"; - -import type { IProformaFactory } from "./proforma-factory"; +} from "../../../domain/aggregates"; +import type { ICustomerInvoiceRepository } from "../../../domain/repositories"; +import type { IProformaFactory } from "../../services/proforma-factory"; export type IIssuedInvoiceWriteService = {}; @@ -122,7 +121,7 @@ export class IssuedInvoiceWriteService implements IIssuedInvoiceWriteService { async updateProformaStatus( companyId: UniqueID, proformaId: UniqueID, - newStatus: CustomerInvoiceStatus, + newStatus: InvoiceStatus, transaction?: Transaction ): Promise> { return this.repository.updateProformaStatusByIdInCompany( diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/index.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/index.ts new file mode 100644 index 00000000..080ddf96 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/index.ts @@ -0,0 +1,6 @@ +export * from "./proforma-full-snapshot.interface"; +export * from "./proforma-full-snapshot-builder"; +export * from "./proforma-item-full-snapshot.interface"; +export * from "./proforma-items-full-snapshot-builder"; +export * from "./proforma-recipient-full-snapshot.interface"; +export * from "./proforma-recipient-full-snapshot-builder"; diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-full-snapshot-builder.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-full-snapshot-builder.ts new file mode 100644 index 00000000..af718ef1 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-full-snapshot-builder.ts @@ -0,0 +1,130 @@ +import type { ISnapshotBuilder } from "@erp/core/api"; +import { toEmptyString } from "@repo/rdx-ddd"; + +import { InvoiceAmount, type Proforma } from "../../../../domain"; + +import type { IProformaFullSnapshot } from "./proforma-full-snapshot.interface"; +import type { IProformaItemsFullSnapshotBuilder } from "./proforma-items-full-snapshot-builder"; +import type { IProformaRecipientFullSnapshotBuilder } from "./proforma-recipient-full-snapshot-builder"; + +export interface IProformaFullSnapshotBuilder + extends ISnapshotBuilder {} + +export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder { + constructor( + private readonly itemsBuilder: IProformaItemsFullSnapshotBuilder, + private readonly recipientBuilder: IProformaRecipientFullSnapshotBuilder + ) {} + + toOutput(invoice: Proforma): IProformaFullSnapshot { + const items = this.itemsBuilder.toOutput(invoice.items); + const recipient = this.recipientBuilder.toOutput(invoice); + + const allAmounts = invoice.calculateAllAmounts(); + + const payment = invoice.paymentMethod.match( + (payment) => { + const { id, payment_description } = payment.toObjectString(); + return { + payment_id: id, + payment_description, + }; + }, + () => undefined + ); + + let totalIvaAmount = InvoiceAmount.zero(invoice.currencyCode.code); + let totalRecAmount = InvoiceAmount.zero(invoice.currencyCode.code); + let totalRetentionAmount = InvoiceAmount.zero(invoice.currencyCode.code); + + const invoiceTaxes = invoice.getTaxes().map((taxGroup) => { + const { ivaAmount, recAmount, retentionAmount, totalAmount } = taxGroup.calculateAmounts(); + + totalIvaAmount = totalIvaAmount.add(ivaAmount); + totalRecAmount = totalRecAmount.add(recAmount); + totalRetentionAmount = totalRetentionAmount.add(retentionAmount); + + return { + taxable_amount: taxGroup.taxableAmount.toObjectString(), + + iva_code: taxGroup.iva.code, + iva_percentage: taxGroup.iva.percentage.toObjectString(), + iva_amount: ivaAmount.toObjectString(), + + rec_code: taxGroup.rec.match( + (rec) => rec.code, + () => "" + ), + + rec_percentage: taxGroup.rec.match( + (rec) => rec.percentage.toObjectString(), + () => ({ value: "", scale: "" }) + ), + + rec_amount: recAmount.toObjectString(), + + retention_code: taxGroup.retention.match( + (retention) => retention.code, + () => "" + ), + + retention_percentage: taxGroup.retention.match( + (retention) => retention.percentage.toObjectString(), + () => ({ value: "", scale: "" }) + ), + + retention_amount: retentionAmount.toObjectString(), + + taxes_amount: totalAmount.toObjectString(), + }; + }); + + return { + id: invoice.id.toString(), + company_id: invoice.companyId.toString(), + + is_proforma: invoice.isProforma ? "true" : "false", + invoice_number: invoice.invoiceNumber.toString(), + status: invoice.status.toPrimitive(), + series: toEmptyString(invoice.series, (value) => value.toString()), + + invoice_date: invoice.invoiceDate.toDateString(), + operation_date: toEmptyString(invoice.operationDate, (value) => value.toDateString()), + + reference: toEmptyString(invoice.reference, (value) => value.toString()), + description: toEmptyString(invoice.description, (value) => value.toString()), + notes: toEmptyString(invoice.notes, (value) => value.toString()), + + language_code: invoice.languageCode.toString(), + currency_code: invoice.currencyCode.toString(), + + customer_id: invoice.customerId.toString(), + recipient, + + payment_method: payment, + + subtotal_amount: allAmounts.subtotalAmount.toObjectString(), + items_discount_amount: allAmounts.itemDiscountAmount.toObjectString(), + + discount_percentage: invoice.discountPercentage.toObjectString(), + discount_amount: allAmounts.globalDiscountAmount.toObjectString(), + + taxable_amount: allAmounts.taxableAmount.toObjectString(), + + iva_amount: totalIvaAmount.toObjectString(), + rec_amount: totalRecAmount.toObjectString(), + retention_amount: totalRetentionAmount.toObjectString(), + + taxes_amount: allAmounts.taxesAmount.toObjectString(), + total_amount: allAmounts.totalAmount.toObjectString(), + + taxes: invoiceTaxes, + + items, + + metadata: { + entity: "proformas", + }, + }; + } +} diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-full-snapshot.interface.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-full-snapshot.interface.ts new file mode 100644 index 00000000..2caaa0b3 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-full-snapshot.interface.ts @@ -0,0 +1,67 @@ +import type { IProformaItemFullSnapshot } from "./proforma-item-full-snapshot.interface"; +import type { IProformaRecipientFullSnapshot } from "./proforma-recipient-full-snapshot.interface"; + +export interface IProformaFullSnapshot { + id: string; + company_id: string; + + is_proforma: "true" | "false"; + invoice_number: string; + status: string; + series: string; + + invoice_date: string; + operation_date: string; + + reference: string; + description: string; + notes: string; + + language_code: string; + currency_code: string; + + customer_id: string; + recipient: IProformaRecipientFullSnapshot; + + payment_method?: { + payment_id: string; + payment_description: string; + }; + + subtotal_amount: { value: string; scale: string; currency_code: string }; + items_discount_amount: { value: string; scale: string; currency_code: string }; + + discount_percentage: { value: string; scale: string }; + discount_amount: { value: string; scale: string; currency_code: string }; + + taxable_amount: { value: string; scale: string; currency_code: string }; + + iva_amount: { value: string; scale: string; currency_code: string }; + rec_amount: { value: string; scale: string; currency_code: string }; + retention_amount: { value: string; scale: string; currency_code: string }; + + taxes_amount: { value: string; scale: string; currency_code: string }; + total_amount: { value: string; scale: string; currency_code: string }; + + taxes: Array<{ + taxable_amount: { value: string; scale: string; currency_code: string }; + + iva_code: string; + iva_percentage: { value: string; scale: string }; + iva_amount: { value: string; scale: string; currency_code: string }; + + rec_code: string; + rec_percentage: { value: string; scale: string }; + rec_amount: { value: string; scale: string; currency_code: string }; + + retention_code: string; + retention_percentage: { value: string; scale: string }; + retention_amount: { value: string; scale: string; currency_code: string }; + + taxes_amount: { value: string; scale: string; currency_code: string }; + }>; + + items: IProformaItemFullSnapshot[]; + + metadata?: Record; +} 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 new file mode 100644 index 00000000..647bb32c --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-item-full-snapshot.interface.ts @@ -0,0 +1,34 @@ +export interface IProformaItemFullSnapshot { + id: string; + is_valued: string; + position: string; + description: string; + + quantity: { value: string; scale: string }; + unit_amount: { value: string; scale: string; currency_code: string }; + + subtotal_amount: { value: string; scale: string; currency_code: string }; + + discount_percentage: { value: string; scale: string }; + 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 }; + + taxable_amount: { value: string; scale: string; currency_code: string }; + + iva_code: string; + iva_percentage: { value: string; scale: string }; + iva_amount: { value: string; scale: string; currency_code: string }; + + rec_code: string; + rec_percentage: { value: string; scale: string }; + rec_amount: { value: string; scale: string; currency_code: string }; + + retention_code: string; + retention_percentage: { value: string; scale: string }; + retention_amount: { value: string; scale: string; currency_code: string }; + + taxes_amount: { value: string; scale: string; currency_code: string }; + total_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 new file mode 100644 index 00000000..6d28f202 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-items-full-snapshot-builder.ts @@ -0,0 +1,94 @@ +import type { ISnapshotBuilder } from "@erp/core/api"; +import { toEmptyString } from "@repo/rdx-ddd"; + +import type { CustomerInvoiceItems, IssuedInvoiceItem } from "../../../../domain"; + +import type { IProformaItemFullSnapshot } from "./proforma-item-full-snapshot.interface"; + +export interface IProformaItemsFullSnapshotBuilder + extends ISnapshotBuilder {} + +export class ProformaItemsFullSnapshotBuilder implements IProformaItemsFullSnapshotBuilder { + private mapItem(invoiceItem: IssuedInvoiceItem, index: number): IProformaItemFullSnapshot { + const allAmounts = invoiceItem.calculateAllAmounts(); + + return { + id: invoiceItem.id.toPrimitive(), + is_valued: String(invoiceItem.isValued), + position: String(index), + description: toEmptyString(invoiceItem.description, (value) => value.toPrimitive()), + + quantity: invoiceItem.quantity.match( + (quantity) => quantity.toObjectString(), + () => ({ value: "", scale: "" }) + ), + + unit_amount: invoiceItem.unitAmount.match( + (unitAmount) => unitAmount.toObjectString(), + () => ({ value: "", scale: "", currency_code: "" }) + ), + + subtotal_amount: allAmounts.subtotalAmount.toObjectString(), + + discount_percentage: invoiceItem.itemDiscountPercentage.match( + (discountPercentage) => discountPercentage.toObjectString(), + () => ({ value: "", scale: "" }) + ), + + discount_amount: allAmounts.itemDiscountAmount.toObjectString(), + + global_discount_percentage: invoiceItem.globalDiscountPercentage.match( + (discountPercentage) => discountPercentage.toObjectString(), + () => ({ value: "", scale: "" }) + ), + + global_discount_amount: allAmounts.globalDiscountAmount.toObjectString(), + + taxable_amount: allAmounts.taxableAmount.toObjectString(), + + iva_code: invoiceItem.taxes.iva.match( + (iva) => iva.code, + () => "" + ), + + iva_percentage: invoiceItem.taxes.iva.match( + (iva) => iva.percentage.toObjectString(), + () => ({ value: "", scale: "" }) + ), + + iva_amount: allAmounts.ivaAmount.toObjectString(), + + rec_code: invoiceItem.taxes.rec.match( + (rec) => rec.code, + () => "" + ), + + rec_percentage: invoiceItem.taxes.rec.match( + (rec) => rec.percentage.toObjectString(), + () => ({ value: "", scale: "" }) + ), + + rec_amount: allAmounts.recAmount.toObjectString(), + + retention_code: invoiceItem.taxes.retention.match( + (retention) => retention.code, + () => "" + ), + + retention_percentage: invoiceItem.taxes.retention.match( + (retention) => retention.percentage.toObjectString(), + () => ({ value: "", scale: "" }) + ), + + retention_amount: allAmounts.retentionAmount.toObjectString(), + + taxes_amount: allAmounts.taxesAmount.toObjectString(), + + total_amount: allAmounts.totalAmount.toObjectString(), + }; + } + + toOutput(invoiceItems: CustomerInvoiceItems): IProformaItemFullSnapshot[] { + return invoiceItems.map((item, index) => this.mapItem(item, index)); + } +} diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-recipient-full-snapshot-builder.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-recipient-full-snapshot-builder.ts new file mode 100644 index 00000000..ff224915 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-recipient-full-snapshot-builder.ts @@ -0,0 +1,43 @@ +import type { ISnapshotBuilder } from "@erp/core/api"; +import { DomainValidationError, toEmptyString } from "@repo/rdx-ddd"; + +import type { InvoiceRecipient, Proforma } from "../../../../domain"; +import type { ProformaRecipientFullSnapshot } from "../../application-models"; + +export interface IProformaRecipientFullSnapshotBuilder + extends ISnapshotBuilder {} + +export class ProformaRecipientFullSnapshotBuilder implements IProformaRecipientFullSnapshotBuilder { + toOutput(invoice: Proforma): ProformaRecipientFullSnapshot { + if (!invoice.recipient) { + throw DomainValidationError.requiredValue("recipient", { + cause: invoice, + }); + } + + return invoice.recipient.match( + (recipient: InvoiceRecipient) => ({ + id: invoice.customerId.toString(), + name: recipient.name.toString(), + tin: recipient.tin.toString(), + street: toEmptyString(recipient.street, (v) => v.toString()), + street2: toEmptyString(recipient.street2, (v) => v.toString()), + city: toEmptyString(recipient.city, (v) => v.toString()), + province: toEmptyString(recipient.province, (v) => v.toString()), + postal_code: toEmptyString(recipient.postalCode, (v) => v.toString()), + country: toEmptyString(recipient.country, (v) => v.toString()), + }), + () => ({ + id: "", + name: "", + tin: "", + street: "", + street2: "", + city: "", + province: "", + postal_code: "", + country: "", + }) + ); + } +} diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-recipient-full-snapshot.interface.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-recipient-full-snapshot.interface.ts new file mode 100644 index 00000000..cb390028 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-recipient-full-snapshot.interface.ts @@ -0,0 +1,11 @@ +export interface IProformaRecipientFullSnapshot { + id: string; + name: string; + tin: string; + street: string; + street2: string; + city: string; + province: string; + postal_code: string; + country: string; +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/index.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/index.ts similarity index 100% rename from modules/customer-invoices/src/api/application/issued-invoices/application-models/snapshots/index.ts rename to modules/customer-invoices/src/api/application/proformas/snapshot-builders/index.ts diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/list/index.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/list/index.ts new file mode 100644 index 00000000..062b00a2 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/list/index.ts @@ -0,0 +1,2 @@ +export * from "./proforma-list-item-snapshot.interface"; +export * from "./proforma-list-item-snapshot-builder"; diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/list/proforma-list-item-snapshot-builder.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/list/proforma-list-item-snapshot-builder.ts new file mode 100644 index 00000000..e0cb852e --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/list/proforma-list-item-snapshot-builder.ts @@ -0,0 +1,46 @@ +import type { ISnapshotBuilder } from "@erp/core/api"; +import { toEmptyString } from "@repo/rdx-ddd"; + +import type { CustomerInvoiceListDTO } from "../../../../infrastructure"; +import type { ProformaListItemSnapshot } from "../../application-models"; + +export interface IProformaListItemSnapshotBuilder + extends ISnapshotBuilder {} + +export class ProformaListItemSnapshotBuilder implements IProformaListItemSnapshotBuilder { + toOutput(proforma: CustomerInvoiceListDTO): ProformaListItemSnapshot { + const recipient = proforma.recipient.toObjectString(); + + return { + id: proforma.id.toString(), + company_id: proforma.companyId.toString(), + is_proforma: proforma.isProforma, + customer_id: proforma.customerId.toString(), + + invoice_number: proforma.invoiceNumber.toString(), + status: proforma.status.toPrimitive(), + series: toEmptyString(proforma.series, (value) => value.toString()), + + invoice_date: proforma.invoiceDate.toDateString(), + operation_date: toEmptyString(proforma.operationDate, (value) => value.toDateString()), + reference: toEmptyString(proforma.reference, (value) => value.toString()), + description: toEmptyString(proforma.description, (value) => value.toString()), + + recipient, + + language_code: proforma.languageCode.code, + currency_code: proforma.currencyCode.code, + + subtotal_amount: proforma.subtotalAmount.toObjectString(), + discount_percentage: proforma.discountPercentage.toObjectString(), + discount_amount: proforma.discountAmount.toObjectString(), + taxable_amount: proforma.taxableAmount.toObjectString(), + taxes_amount: proforma.taxesAmount.toObjectString(), + total_amount: proforma.totalAmount.toObjectString(), + + metadata: { + entity: "proforma", + }, + }; + } +} diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/list/proforma-list-item-snapshot.interface.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/list/proforma-list-item-snapshot.interface.ts new file mode 100644 index 00000000..02a5050a --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/list/proforma-list-item-snapshot.interface.ts @@ -0,0 +1,40 @@ +export interface ProformaListItemSnapshot { + id: string; + company_id: string; + is_proforma: boolean; + + customer_id: string; + + invoice_number: string; + status: string; + series: string; + + invoice_date: string; + operation_date: string; + + language_code: string; + currency_code: string; + + reference: string; + description: string; + + recipient: { + tin: string; + name: string; + street: string; + street2: string; + city: string; + postal_code: string; + province: string; + country: string; + }; + + subtotal_amount: { value: string; scale: string; currency_code: string }; + discount_percentage: { value: string; scale: string }; + discount_amount: { value: string; scale: string; currency_code: string }; + taxable_amount: { value: string; scale: string; currency_code: string }; + taxes_amount: { value: string; scale: string; currency_code: string }; + total_amount: { value: string; scale: string; currency_code: string }; + + metadata?: Record; +} diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/index.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/index.ts new file mode 100644 index 00000000..d1b38528 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/index.ts @@ -0,0 +1,6 @@ +export * from "./proforma-items-report-snapshot-builder"; +export * from "./proforma-report-item-snapshot.interface"; +export * from "./proforma-report-snapshot.interface"; +export * from "./proforma-report-snapshot-builder"; +export * from "./proforma-report-tax-snapshot.interface"; +export * from "./proforma-tax-report-snapshot-builder"; 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 new file mode 100644 index 00000000..78cfd35c --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-items-report-snapshot-builder.ts @@ -0,0 +1,35 @@ +import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/core"; +import type { ISnapshotBuilder, ISnapshotBuilderParams } from "@erp/core/api"; + +import type { ProformaFullSnapshot, ProformaReportItemSnapshot } from "../../application-models"; + +export interface IProformaItemReportSnapshotBuilder + extends ISnapshotBuilder {} + +export class ProformaItemReportSnapshotBuilder implements IProformaItemReportSnapshotBuilder { + toOutput( + items: ProformaFullSnapshot["items"], + params?: ISnapshotBuilderParams + ): ProformaReportItemSnapshot[] { + const locale = params?.locale as string; + + const moneyOptions = { + hideZeros: true, + minimumFractionDigits: 2, + }; + + return items.map((item) => ({ + description: item.description, + 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, { + minimumFractionDigits: 0, + }), + discount_amount: MoneyDTOHelper.format(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/proformas/snapshot-builders/report/proforma-report-item-snapshot.interface.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-report-item-snapshot.interface.ts new file mode 100644 index 00000000..ac9bac5c --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-report-item-snapshot.interface.ts @@ -0,0 +1,11 @@ +export interface ProformaReportItemSnapshot { + description: string; + quantity: string; + unit_amount: string; + subtotal_amount: string; + discount_percentage: string; + discount_amount: string; + taxable_amount: string; + taxes_amount: string; + total_amount: 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 new file mode 100644 index 00000000..e5220270 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-report-snapshot-builder.ts @@ -0,0 +1,92 @@ +import { DateHelper, MoneyDTOHelper, PercentageDTOHelper } from "@erp/core"; +import type { ISnapshotBuilder, ISnapshotBuilderParams } from "@erp/core/api"; + +import type { + ProformaFullSnapshot, + ProformaReportItemSnapshot, + ProformaReportSnapshot, + ProformaReportTaxSnapshot, +} from "../../application-models"; + +export interface IProformaReportSnapshotBuilder + extends ISnapshotBuilder {} + +export class ProformaReportSnapshotBuilder implements IProformaReportSnapshotBuilder { + constructor( + private readonly itemsBuilder: ISnapshotBuilder< + ProformaFullSnapshot["items"], + ProformaReportItemSnapshot[] + >, + private readonly taxesBuilder: ISnapshotBuilder< + ProformaFullSnapshot["taxes"], + ProformaReportTaxSnapshot[] + > + ) {} + + toOutput( + snapshot: ProformaFullSnapshot, + params?: ISnapshotBuilderParams + ): ProformaReportSnapshot { + const locale = params?.locale as string; + + const moneyOptions = { + hideZeros: true, + minimumFractionDigits: 2, + }; + + return { + id: snapshot.id, + company_id: snapshot.company_id, + company_slug: "rodax", + invoice_number: snapshot.invoice_number, + series: snapshot.series, + status: snapshot.status, + reference: snapshot.reference, + + language_code: snapshot.language_code, + currency_code: snapshot.currency_code, + + invoice_date: DateHelper.format(snapshot.invoice_date, locale), + + payment_method: snapshot.payment_method?.payment_description ?? "", + notes: snapshot.notes, + + recipient: { + name: snapshot.recipient.name, + tin: snapshot.recipient.tin, + format_address: this.formatAddress(snapshot.recipient), + }, + + items: this.itemsBuilder.toOutput(snapshot.items, { locale }), + 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), + 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 { + const lines: string[] = []; + + if (recipient.street) lines.push(recipient.street); + if (recipient.street2) lines.push(recipient.street2); + + const cityLine = [recipient.postal_code, recipient.city].filter(Boolean).join(" "); + + if (cityLine) lines.push(cityLine); + if (recipient.province && recipient.province !== recipient.city) { + lines.push(recipient.province); + } + if (recipient.country && recipient.country !== "es") { + lines.push(recipient.country); + } + + return lines.join("\n"); + } +} 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 new file mode 100644 index 00000000..c933444a --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-report-snapshot.interface.ts @@ -0,0 +1,35 @@ +import type { ProformaReportItemSnapshot } from "./proforma-report-item-snapshot.interface"; +import type { ProformaReportTaxSnapshot } from "./proforma-report-tax-snapshot.interface"; + +export interface ProformaReportSnapshot { + id: string; + company_id: string; + company_slug: string; + invoice_number: string; + series: string; + status: string; + reference: string; + + language_code: string; + currency_code: string; + + invoice_date: string; + payment_method: string; + notes: string; + + recipient: { + name: string; + tin: string; + format_address: string; + }; + + items: ProformaReportItemSnapshot[]; + taxes: ProformaReportTaxSnapshot[]; + + subtotal_amount: string; + discount_percentage: string; + discount_amount: string; + taxable_amount: string; + taxes_amount: string; + total_amount: string; +} diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-report-tax-snapshot.interface.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-report-tax-snapshot.interface.ts new file mode 100644 index 00000000..ee649393 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-report-tax-snapshot.interface.ts @@ -0,0 +1,17 @@ +export interface ProformaReportTaxSnapshot { + taxable_amount: string; + + iva_code: string; + iva_percentage: string; + iva_amount: string; + + rec_code: string; + rec_percentage: string; + rec_amount: string; + + retention_code: string; + retention_percentage: string; + retention_amount: string; + + taxes_amount: string; +} diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-tax-report-snapshot-builder.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-tax-report-snapshot-builder.ts new file mode 100644 index 00000000..2b55ec3d --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/report/proforma-tax-report-snapshot-builder.ts @@ -0,0 +1,39 @@ +import { MoneyDTOHelper, PercentageDTOHelper } from "@erp/core"; +import type { ISnapshotBuilder, ISnapshotBuilderParams } from "@erp/core/api"; + +import type { ProformaFullSnapshot, ProformaReportTaxSnapshot } from "../../application-models"; + +export interface IProformaTaxReportSnapshotBuilder + extends ISnapshotBuilder {} + +export class ProformaTaxReportSnapshotBuilder implements IProformaTaxReportSnapshotBuilder { + toOutput( + taxes: ProformaFullSnapshot["taxes"], + params?: ISnapshotBuilderParams + ): ProformaReportTaxSnapshot[] { + const locale = params?.locale as string; + + const moneyOptions = { + hideZeros: true, + minimumFractionDigits: 2, + }; + + return taxes.map((tax) => ({ + taxable_amount: MoneyDTOHelper.format(tax.taxable_amount, locale, moneyOptions), + + iva_code: tax.iva_code, + iva_percentage: PercentageDTOHelper.format(tax.iva_percentage, locale), + iva_amount: MoneyDTOHelper.format(tax.iva_amount, locale, moneyOptions), + + rec_code: tax.rec_code, + rec_percentage: PercentageDTOHelper.format(tax.rec_percentage, locale), + rec_amount: MoneyDTOHelper.format(tax.rec_amount, locale, moneyOptions), + + retention_code: tax.retention_code, + retention_percentage: PercentageDTOHelper.format(tax.retention_percentage, locale), + retention_amount: MoneyDTOHelper.format(tax.retention_amount, locale, moneyOptions), + + taxes_amount: MoneyDTOHelper.format(tax.taxes_amount, locale, moneyOptions), + })); + } +} diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/change-status-proforma.use-case.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/change-status-proforma.use-case.ts similarity index 100% rename from modules/customer-invoices/src/api/application/use-cases/proformas/change-status-proforma.use-case.ts rename to modules/customer-invoices/src/api/application/proformas/use-cases/change-status-proforma.use-case.ts 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 new file mode 100644 index 00000000..c10abd94 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/use-cases/create-proforma/create-proforma.use-case.ts @@ -0,0 +1,62 @@ +import type { ITransactionManager } from "@erp/core/api"; +import type { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { CreateProformaRequestDTO } from "../../../../../common"; +import type { CreateProformaPropsMapper } from "../../mappers"; +import type { IProformaCreator } from "../../services"; +import type { IProformaFullSnapshotBuilder } from "../../snapshot-builders"; + +type CreateProformaUseCaseInput = { + companyId: UniqueID; + dto: CreateProformaRequestDTO; +}; + +type CreateProformaUseCaseDeps = { + mapper: CreateProformaPropsMapper; + creator: IProformaCreator; + fullSnapshotBuilder: IProformaFullSnapshotBuilder; + transactionManager: ITransactionManager; +}; + +export class CreateProformaUseCase { + private readonly mapper: CreateProformaPropsMapper; + private readonly creator: IProformaCreator; + private readonly fullSnapshotBuilder: IProformaFullSnapshotBuilder; + private readonly transactionManager: ITransactionManager; + + constructor(deps: CreateProformaUseCaseDeps) { + this.mapper = deps.mapper; + this.creator = deps.creator; + this.fullSnapshotBuilder = deps.fullSnapshotBuilder; + this.transactionManager = deps.transactionManager; + } + + public async execute(params: CreateProformaUseCaseInput) { + const { dto, companyId } = params; + + // 1) Mapear DTO → props de dominio + const mappedResult = this.mapper.map(dto, companyId); + if (mappedResult.isFailure) { + return Result.fail(mappedResult.error); + } + + const { props, id } = mappedResult.data; + + return this.transactionManager.complete(async (transaction) => { + try { + const createResult = await this.creator.create(companyId, id, props, transaction); + + if (createResult.isFailure) { + return Result.fail(createResult.error); + } + + const snapshot = this.fullSnapshotBuilder.toOutput(createResult.data); + + return Result.ok(snapshot); + } catch (error: unknown) { + return Result.fail(error as Error); + } + }); + } +} diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/create-proforma/index.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/create-proforma/index.ts similarity index 100% rename from modules/customer-invoices/src/api/application/use-cases/proformas/create-proforma/index.ts rename to modules/customer-invoices/src/api/application/proformas/use-cases/create-proforma/index.ts diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/delete-proforma.use-case.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/delete-proforma.use-case.ts similarity index 100% rename from modules/customer-invoices/src/api/application/use-cases/proformas/delete-proforma.use-case.ts rename to modules/customer-invoices/src/api/application/proformas/use-cases/delete-proforma.use-case.ts diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/get-proforma-by-id.use-case.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/get-proforma-by-id.use-case.ts new file mode 100644 index 00000000..babd3659 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/use-cases/get-proforma-by-id.use-case.ts @@ -0,0 +1,49 @@ +import type { ITransactionManager } from "@erp/core/api"; +import { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { IProformaFinder } from "../services"; +import type { IProformaFullSnapshotBuilder } from "../snapshot-builders"; + +type GetProformaUseCaseInput = { + companyId: UniqueID; + proforma_id: string; +}; + +export class GetProformaByIdUseCase { + constructor( + private readonly finder: IProformaFinder, + private readonly fullSnapshotBuilder: IProformaFullSnapshotBuilder, + private readonly transactionManager: ITransactionManager + ) {} + + public execute(params: GetProformaUseCaseInput) { + const { proforma_id, companyId } = params; + + const idOrError = UniqueID.create(proforma_id); + if (idOrError.isFailure) { + return Result.fail(idOrError.error); + } + + const proformaId = idOrError.data; + + return this.transactionManager.complete(async (transaction) => { + try { + const proformaResult = await this.finder.findProformaById( + companyId, + proformaId, + transaction + ); + if (proformaResult.isFailure) { + return Result.fail(proformaResult.error); + } + + const fullSnapshot = this.fullSnapshotBuilder.toOutput(proformaResult.data); + + return Result.ok(fullSnapshot); + } catch (error: unknown) { + return Result.fail(error as Error); + } + }); + } +} diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/index.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/index.ts new file mode 100644 index 00000000..e252b3be --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/use-cases/index.ts @@ -0,0 +1,8 @@ +//export * from "./change-status-proforma.use-case"; +//export * from "./create-proforma"; +//export * from "./delete-proforma.use-case"; +export * from "./get-proforma-by-id.use-case"; +//export * from "./issue-proforma.use-case"; +export * from "./list-proformas.use-case"; +export * from "./report-proforma.use-case"; +//export * from "./update-proforma"; diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/issue-proforma.use-case.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/issue-proforma.use-case.ts similarity index 100% rename from modules/customer-invoices/src/api/application/use-cases/proformas/issue-proforma.use-case.ts rename to modules/customer-invoices/src/api/application/proformas/use-cases/issue-proforma.use-case.ts 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 new file mode 100644 index 00000000..b6fd2e4d --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/use-cases/list-proformas.use-case.ts @@ -0,0 +1,56 @@ +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 { IProformaListItemSnapshotBuilder } from "../snapshot-builders"; + +type ListProformasUseCaseInput = { + companyId: UniqueID; + criteria: Criteria; +}; + +export class ListProformasUseCase { + constructor( + private readonly finder: IProformaFinder, + private readonly listItemSnapshotBuilder: IProformaListItemSnapshotBuilder, + private readonly transactionManager: ITransactionManager + ) {} + + public execute(params: ListProformasUseCaseInput) { + const { criteria, companyId } = params; + + return this.transactionManager.complete(async (transaction: Transaction) => { + try { + const result = await this.finder.findProformasByCriteria(companyId, criteria, transaction); + + if (result.isFailure) { + return Result.fail(result.error); + } + + const proformas = result.data; + const totalProformas = proformas.total(); + + const items = proformas.map((item) => this.listItemSnapshotBuilder.toOutput(item)); + + const snapshot = { + page: criteria.pageNumber, + per_page: criteria.pageSize, + total_pages: Math.ceil(totalProformas / criteria.pageSize), + total_items: totalProformas, + items: items, + metadata: { + entity: "proformas", + criteria: criteria.toJSON(), + }, + }; + + return Result.ok(snapshot); + } catch (error: unknown) { + return Result.fail(error as Error); + } + }); + } +} diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma.use-case.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma.use-case.ts new file mode 100644 index 00000000..285ea945 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma.use-case.ts @@ -0,0 +1,74 @@ +import type { ITransactionManager, RendererFormat } from "@erp/core/api"; +import { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { IProformaFinder, ProformaDocumentGeneratorService } from "../services"; +import type { IProformaFullSnapshotBuilder } from "../snapshot-builders"; +import type { IProformaReportSnapshotBuilder } from "../snapshot-builders/report"; + +type ReportProformaUseCaseInput = { + companyId: UniqueID; + companySlug: string; + invoice_id: string; + format: RendererFormat; +}; + +export class ReportProformaUseCase { + constructor( + private readonly finder: IProformaFinder, + private readonly fullSnapshotBuilder: IProformaFullSnapshotBuilder, + private readonly reportSnapshotBuilder: IProformaReportSnapshotBuilder, + private readonly documentGenerationService: ProformaDocumentGeneratorService, + private readonly transactionManager: ITransactionManager + ) {} + + public async execute(params: ReportProformaUseCaseInput) { + const { invoice_id, companyId } = params; + + const idOrError = UniqueID.create(invoice_id); + + if (idOrError.isFailure) { + return Result.fail(idOrError.error); + } + + const invoiceId = idOrError.data; + + return this.transactionManager.complete(async (transaction) => { + try { + const invoiceResult = await this.finder.findProformaById(companyId, invoiceId, transaction); + + if (invoiceResult.isFailure) { + return Result.fail(invoiceResult.error); + } + + const invoice = invoiceResult.data; + + // Snapshot completo de la entidad + const fullSnapshot = this.fullSnapshotBuilder.toOutput(invoice); + + // Snapshot para informe a partir del anterior + const reportSnapshot = this.reportSnapshotBuilder.toOutput(fullSnapshot, { + languageCode: invoice.languageCode, + }); + + // Llamar al servicio y que se apañe + const documentResult = await this.documentGenerationService.generate(reportSnapshot, { + companySlug: "rodax", + }); + + if (documentResult.isFailure) { + return Result.fail(documentResult.error); + } + + // 5. Devolver artefacto firmado + return Result.ok({ + payload: documentResult.data.payload, + mimeType: "application/pdf", + filename: documentResult.data.filename, + }); + } catch (error: unknown) { + return Result.fail(error as Error); + } + }); + } +} diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/index.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/index.ts similarity index 100% rename from modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/index.ts rename to modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/index.ts diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/report-proforma.use-case.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/report-proforma.use-case.ts similarity index 100% rename from modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/report-proforma.use-case.ts rename to modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/report-proforma.use-case.ts diff --git a/modules/customer-invoices/src/api/infrastructure/documents/renderers/fastreport/proforma-fastreport.renderer.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/reporter/index.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/documents/renderers/fastreport/proforma-fastreport.renderer.ts rename to modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/reporter/index.ts diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/proforma.report.html.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/reporter/proforma.report.html.ts similarity index 86% rename from modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/proforma.report.html.ts rename to modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/reporter/proforma.report.html.ts index 1abcc36b..efcc8b39 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/proforma.report.html.ts +++ b/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/reporter/proforma.report.html.ts @@ -1,10 +1,10 @@ import { TemplatePresenter } from "@erp/core/api"; -import type { CustomerInvoice } from "../../../../../domain"; +import type { Proforma } from "../../../../../domain"; import type { ProformaFullPresenter, ProformaReportPresenter } from "../../../../snapshot-builders"; export class ProformaReportHTMLPresenter extends TemplatePresenter { - toOutput(proforma: CustomerInvoice, params: { companySlug: string }): string { + toOutput(proforma: Proforma, params: { companySlug: string }): string { const { companySlug } = params; const dtoPresenter = this.presenterRegistry.getPresenter({ resource: "proforma", diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/proforma.report.pdf.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/reporter/proforma.report.pdf.ts similarity index 91% rename from modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/proforma.report.pdf.ts rename to modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/reporter/proforma.report.pdf.ts index 14f7e307..8b38e294 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/report-proforma/reporter/proforma.report.pdf.ts +++ b/modules/customer-invoices/src/api/application/proformas/use-cases/report-proforma2/reporter/proforma.report.pdf.ts @@ -1,19 +1,16 @@ import { Presenter } from "@erp/core/api"; import puppeteer from "puppeteer"; -import type { CustomerInvoice } from "../../../../../domain"; +import type { Proforma } from "../../../../../domain"; import type { ProformaReportHTMLPresenter } from "./proforma.report.html"; // https://plnkr.co/edit/lWk6Yd?preview // https://latenode.com/es/blog/web-automation-scraping/puppeteer-fundamentals-setup/complete-guide-to-pdf-generation-with-puppeteer-from-simple-documents-to-complex-reports -export class ProformaReportPDFPresenter extends Presenter< - CustomerInvoice, - Promise> -> { +export class ProformaReportPDFPresenter extends Presenter>> { async toOutput( - proforma: CustomerInvoice, + proforma: Proforma, params: { companySlug: string } ): Promise> { try { diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/update-proforma/index.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/update-proforma/index.ts similarity index 100% rename from modules/customer-invoices/src/api/application/use-cases/proformas/update-proforma/index.ts rename to modules/customer-invoices/src/api/application/proformas/use-cases/update-proforma/index.ts diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/update-proforma/update-proforma.use-case.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/update-proforma/update-proforma.use-case.ts similarity index 90% rename from modules/customer-invoices/src/api/application/use-cases/proformas/update-proforma/update-proforma.use-case.ts rename to modules/customer-invoices/src/api/application/proformas/use-cases/update-proforma/update-proforma.use-case.ts index 17c40c7a..7af377ce 100644 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/update-proforma/update-proforma.use-case.ts +++ b/modules/customer-invoices/src/api/application/proformas/use-cases/update-proforma/update-proforma.use-case.ts @@ -4,12 +4,10 @@ import { Result } from "@repo/rdx-utils"; import type { Transaction } from "sequelize"; import type { UpdateProformaByIdRequestDTO } from "../../../../../common"; -import type { CustomerInvoicePatchProps } from "../../../../domain"; +import type { ProformaPatchProps } from "../../../../domain"; import type { CustomerInvoiceApplicationService } from "../../../services/customer-invoice-application.service"; import type { ProformaFullPresenter } from "../../../snapshot-builders"; -import { mapDTOToUpdateCustomerInvoicePatchProps } from "./map-dto-to-update-customer-invoice-props"; - type UpdateProformaUseCaseInput = { companyId: UniqueID; proforma_id: string; @@ -43,7 +41,7 @@ export class UpdateProformaUseCase { return Result.fail(patchPropsResult.error); } - const patchProps: CustomerInvoicePatchProps = patchPropsResult.data; + const patchProps: ProformaPatchProps = patchPropsResult.data; return this.transactionManager.complete(async (transaction: Transaction) => { try { 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 33d699f6..1e64100b 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 @@ -5,10 +5,10 @@ import type { Transaction } from "sequelize"; import { CustomerInvoiceIsProformaSpecification, - type CustomerInvoiceNumber, - type CustomerInvoiceSerie, - type CustomerInvoiceStatus, type ICustomerInvoiceNumberGenerator, + type InvoiceNumber, + type InvoiceSerie, + type InvoiceStatus, ProformaCannotBeDeletedError, StatusInvoiceIsDraftSpecification, } from "../../domain"; @@ -36,7 +36,7 @@ export class CustomerInvoiceApplicationService { async getNextProformaNumber( companyId: UniqueID, transaction: Transaction - ): Promise> { + ): Promise> { return await this.numberGenerator.nextForCompany(companyId, Maybe.none(), transaction); } @@ -50,9 +50,9 @@ export class CustomerInvoiceApplicationService { */ async getNextIssuedInvoiceNumber( companyId: UniqueID, - series: Maybe, + series: Maybe, transaction: Transaction - ): Promise> { + ): Promise> { return await this.numberGenerator.nextForCompany(companyId, series, transaction); } @@ -328,7 +328,7 @@ export class CustomerInvoiceApplicationService { async updateProformaStatusByIdInCompany( companyId: UniqueID, proformaId: UniqueID, - newStatus: CustomerInvoiceStatus, + newStatus: InvoiceStatus, transaction?: Transaction ): Promise> { return this.repository.updateProformaStatusByIdInCompany( diff --git a/modules/customer-invoices/src/api/application/services/document-signing-service.interface.ts b/modules/customer-invoices/src/api/application/services/document-signing-service.interface.ts deleted file mode 100644 index 0c51785b..00000000 --- a/modules/customer-invoices/src/api/application/services/document-signing-service.interface.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { UniqueID } from "@repo/rdx-ddd"; - -export interface ISignDocumentCommand { - /** PDF sin firmar */ - readonly file: Buffer; - - /** Identidad estable de la empresa */ - readonly companyId: UniqueID; -} - -export interface IDocumentSigningService { - /** - * Firma un documento PDF. - * - * Invariantes: - * - El input NO se persiste. - * - El output ES el documento legal. - * - No se realizan validaciones de negocio aquí. - * - * Errores: - * - Lanza excepción si la firma falla. - */ - sign(command: ISignDocumentCommand): Promise; -} diff --git a/modules/customer-invoices/src/api/application/services/index.ts b/modules/customer-invoices/src/api/application/services/index.ts index 2e41928f..92829664 100644 --- a/modules/customer-invoices/src/api/application/services/index.ts +++ b/modules/customer-invoices/src/api/application/services/index.ts @@ -1,2 +1 @@ export * from "./customer-invoice-application.service"; -export * from "./document-signing-service.interface"; diff --git a/modules/customer-invoices/src/api/application/services/proforma-factory.ts b/modules/customer-invoices/src/api/application/services/proforma-factory.ts deleted file mode 100644 index 8ae192f5..00000000 --- a/modules/customer-invoices/src/api/application/services/proforma-factory.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { UniqueID } from "@repo/rdx-ddd"; -import type { Result } from "@repo/rdx-utils"; - -import { CustomerInvoice, type CustomerInvoiceProps } from "../../domain/aggregates"; - -export interface IProformaFactory { - /** - * Crea una proforma válida para una empresa a partir de props ya validadas. - * - * No persiste el agregado. - */ - createProforma( - companyId: UniqueID, - props: Omit, - proformaId?: UniqueID - ): Result; -} - -export class CustomerInvoiceFactory implements IProformaFactory { - createProforma( - companyId: UniqueID, - props: Omit, - proformaId?: UniqueID - ): Result { - return CustomerInvoice.create({ ...props, companyId }, proformaId); - } -} diff --git a/modules/customer-invoices/src/api/application/services/proforma-number-service.ts b/modules/customer-invoices/src/api/application/services/proforma-number-service.ts deleted file mode 100644 index 5122a125..00000000 --- a/modules/customer-invoices/src/api/application/services/proforma-number-service.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { UniqueID } from "@repo/rdx-ddd"; -import type { Maybe, Result } from "@repo/rdx-utils"; - -import type { - CustomerInvoiceNumber, - CustomerInvoiceSerie, - ICustomerInvoiceNumberGenerator, -} from "../../domain"; - -export interface IProformaNumberService { - /** - * Devuelve el siguiente número disponible para una factura emitida. - */ - nextProformaNumber( - companyId: UniqueID, - series: Maybe, - transaction: unknown - ): Promise>; -} - -export class ProformaNumberService implements IProformaNumberService { - constructor(private readonly numberGenerator: ICustomerInvoiceNumberGenerator) {} - - async nextProformaNumber( - companyId: UniqueID, - series: Maybe, - transaction: unknown - ): Promise> { - return this.numberGenerator.nextForCompany(companyId, series, transaction); - } -} diff --git a/modules/customer-invoices/src/api/application/snapshot-builders/domain/proformas/proforma-items.full.presenter.ts b/modules/customer-invoices/src/api/application/snapshot-builders/domain/proformas/proforma-items.full.presenter.ts index 9bcc3c88..70f9b370 100644 --- a/modules/customer-invoices/src/api/application/snapshot-builders/domain/proformas/proforma-items.full.presenter.ts +++ b/modules/customer-invoices/src/api/application/snapshot-builders/domain/proformas/proforma-items.full.presenter.ts @@ -3,15 +3,12 @@ import type { GetProformaByIdResponseDTO } from "@erp/customer-invoices/common"; import { toEmptyString } from "@repo/rdx-ddd"; import type { ArrayElement } from "@repo/rdx-utils"; -import type { CustomerInvoiceItem, CustomerInvoiceItems } from "../../../../domain"; +import type { CustomerInvoiceItems, IssuedInvoiceItem } from "../../../../domain"; type GetProformaItemByIdResponseDTO = ArrayElement; export class ProformaItemsFullPresenter extends SnapshotBuilder { - private _mapItem( - proformaItem: CustomerInvoiceItem, - index: number - ): GetProformaItemByIdResponseDTO { + private _mapItem(proformaItem: IssuedInvoiceItem, index: number): GetProformaItemByIdResponseDTO { const allAmounts = proformaItem.calculateAllAmounts(); return { diff --git a/modules/customer-invoices/src/api/application/snapshot-builders/domain/proformas/proforma-recipient.full.presenter.ts b/modules/customer-invoices/src/api/application/snapshot-builders/domain/proformas/proforma-recipient.full.presenter.ts index 3a4c0bba..0a043eb5 100644 --- a/modules/customer-invoices/src/api/application/snapshot-builders/domain/proformas/proforma-recipient.full.presenter.ts +++ b/modules/customer-invoices/src/api/application/snapshot-builders/domain/proformas/proforma-recipient.full.presenter.ts @@ -2,12 +2,12 @@ import { SnapshotBuilder } from "@erp/core/api"; import { DomainValidationError, toEmptyString } from "@repo/rdx-ddd"; import type { GetIssuedInvoiceByIdResponseDTO as GetProformaByIdResponseDTO } from "../../../../../common/dto"; -import type { CustomerInvoice, InvoiceRecipient } from "../../../../domain"; +import type { InvoiceRecipient, Proforma } from "../../../../domain"; type GetProformaRecipientByIdResponseDTO = GetProformaByIdResponseDTO["recipient"]; export class ProformaRecipientFullPresenter extends SnapshotBuilder { - toOutput(proforma: CustomerInvoice): GetProformaRecipientByIdResponseDTO { + toOutput(proforma: Proforma): GetProformaRecipientByIdResponseDTO { if (!proforma.recipient) { throw DomainValidationError.requiredValue("recipient", { cause: proforma, diff --git a/modules/customer-invoices/src/api/application/snapshot-builders/domain/proformas/proforma.full.presenter.ts b/modules/customer-invoices/src/api/application/snapshot-builders/domain/proformas/proforma.full.presenter.ts index 03af9c01..3e50d7d6 100644 --- a/modules/customer-invoices/src/api/application/snapshot-builders/domain/proformas/proforma.full.presenter.ts +++ b/modules/customer-invoices/src/api/application/snapshot-builders/domain/proformas/proforma.full.presenter.ts @@ -2,13 +2,13 @@ import { Presenter } from "@erp/core/api"; import { toEmptyString } from "@repo/rdx-ddd"; import type { GetProformaByIdResponseDTO } from "../../../../../common/dto"; -import { type CustomerInvoice, InvoiceAmount } from "../../../../domain"; +import { InvoiceAmount, type Proforma } from "../../../../domain"; import type { ProformaItemsFullPresenter } from "./proforma-items.full.presenter"; import type { ProformaRecipientFullPresenter } from "./proforma-recipient.full.presenter"; -export class ProformaFullPresenter extends Presenter { - toOutput(proforma: CustomerInvoice): GetProformaByIdResponseDTO { +export class ProformaFullPresenter extends Presenter { + toOutput(proforma: Proforma): GetProformaByIdResponseDTO { const itemsPresenter = this.presenterRegistry.getPresenter({ resource: "proforma-items", projection: "FULL", @@ -125,7 +125,6 @@ export class ProformaFullPresenter extends Presenter { - try { - // 2) Generar nuevo nº de proforma - const nextNumberResult = await this.service.getNextProformaNumber(companyId, transaction); - if (nextNumberResult.isFailure) { - return Result.fail(nextNumberResult.error); - } - - const newProformaNumber = nextNumberResult.data; - - // 3) Construir entidad de dominio - const proformaProps = { - ...props, - invoiceNumber: newProformaNumber, - }; - - const buildResult = this.service.buildProformaInCompany(companyId, proformaProps, id); - if (buildResult.isFailure) { - return Result.fail(buildResult.error); - } - - const newProforma = buildResult.data; - - const existsGuard = await this.ensureNotExists(companyId, id, transaction); - if (existsGuard.isFailure) { - return Result.fail(existsGuard.error); - } - - const saveResult = await this.service.createProformaInCompany( - companyId, - newProforma, - transaction - ); - if (saveResult.isFailure) { - return Result.fail(saveResult.error); - } - - const proforma = saveResult.data; - const dto = presenter.toOutput(proforma); - - return Result.ok(dto); - } catch (error: unknown) { - return Result.fail(error as Error); - } - }); - } - - /** - Verifica que no exista uana factura con el mismo id en la companyId. - */ - private async ensureNotExists( - companyId: UniqueID, - id: UniqueID, - transaction: Transaction - ): Promise> { - const existsResult = await this.service.existsProformaByIdInCompany(companyId, id, transaction); - if (existsResult.isFailure) { - return Result.fail(existsResult.error); - } - - if (existsResult.data) { - return Result.fail(new DuplicateEntityError("Customer invoice", "id", String(id))); - } - - return Result.ok(undefined); - } -} diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/get-proforma.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/proformas/get-proforma.use-case.ts deleted file mode 100644 index 157f83f1..00000000 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/get-proforma.use-case.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api"; -import { UniqueID } from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; - -import type { CustomerInvoiceApplicationService } from "../../services"; -import type { ProformaFullPresenter } from "../../snapshot-builders/domain"; - -type GetProformaUseCaseInput = { - companyId: UniqueID; - proforma_id: string; -}; - -export class GetProformaUseCase { - constructor( - private readonly service: CustomerInvoiceApplicationService, - private readonly transactionManager: ITransactionManager, - private readonly presenterRegistry: IPresenterRegistry - ) {} - - public execute(params: GetProformaUseCaseInput) { - const { proforma_id, companyId } = params; - - const idOrError = UniqueID.create(proforma_id); - if (idOrError.isFailure) { - return Result.fail(idOrError.error); - } - - const proformaId = idOrError.data; - const presenter = this.presenterRegistry.getPresenter({ - resource: "proforma", - projection: "FULL", - }) as ProformaFullPresenter; - - return this.transactionManager.complete(async (transaction) => { - try { - const proformaOrError = await this.service.getProformaByIdInCompany( - companyId, - proformaId, - transaction - ); - if (proformaOrError.isFailure) { - return Result.fail(proformaOrError.error); - } - - const proforma = proformaOrError.data; - const dto = presenter.toOutput(proforma); - - return Result.ok(dto); - } catch (error: unknown) { - return Result.fail(error as Error); - } - }); - } -} diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/index.ts b/modules/customer-invoices/src/api/application/use-cases/proformas/index.ts deleted file mode 100644 index 7df91db2..00000000 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from "./change-status-proforma.use-case"; -export * from "./create-proforma"; -export * from "./delete-proforma.use-case"; -export * from "./get-proforma.use-case"; -export * from "./issue-proforma.use-case"; -export * from "./list-proformas.use-case"; -export * from "./report-proforma"; -export * from "./update-proforma"; diff --git a/modules/customer-invoices/src/api/application/use-cases/proformas/list-proformas.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/proformas/list-proformas.use-case.ts deleted file mode 100644 index c3b13d40..00000000 --- a/modules/customer-invoices/src/api/application/use-cases/proformas/list-proformas.use-case.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api"; -import type { ListProformasResponseDTO } from "@erp/customer-invoices/common"; -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 { CustomerInvoiceApplicationService } from "../../services"; -import type { ProformaListPresenter } from "../../snapshot-builders"; - -type ListProformasUseCaseInput = { - companyId: UniqueID; - criteria: Criteria; -}; - -export class ListProformasUseCase { - constructor( - private readonly service: CustomerInvoiceApplicationService, - private readonly transactionManager: ITransactionManager, - private readonly presenterRegistry: IPresenterRegistry - ) {} - - public execute( - params: ListProformasUseCaseInput - ): Promise> { - const { criteria, companyId } = params; - const presenter = this.presenterRegistry.getPresenter({ - resource: "proforma", - projection: "LIST", - }) as ProformaListPresenter; - - return this.transactionManager.complete(async (transaction: Transaction) => { - try { - const result = await this.service.findProformasByCriteriaInCompany( - companyId, - criteria, - transaction - ); - - if (result.isFailure) { - return Result.fail(result.error); - } - - const proformas = result.data; - const dto = presenter.toOutput({ - proformas, - criteria, - }); - - return Result.ok(dto); - } catch (error: unknown) { - return Result.fail(error as Error); - } - }); - } -} diff --git a/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts b/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.aggregate.ts similarity index 95% rename from modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts rename to modules/customer-invoices/src/api/domain/aggregates/customer-invoice.aggregate.ts index b068996e..a65f1e79 100644 --- a/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts +++ b/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.aggregate.ts @@ -10,27 +10,31 @@ import { } from "@repo/rdx-ddd"; import { Collection, type Maybe, Result } from "@repo/rdx-utils"; -import { CustomerInvoiceItems, type InvoicePaymentMethod, type VerifactuRecord } from "../entities"; import { - type CustomerInvoiceNumber, - type CustomerInvoiceSerie, - type CustomerInvoiceStatus, + CustomerInvoiceItems, + type InvoicePaymentMethod, + type VerifactuRecord, +} from "../common/entities"; +import { InvoiceAmount, + type InvoiceNumber, type InvoiceRecipient, + type InvoiceSerie, + type InvoiceStatus, InvoiceTaxGroup, type ItemAmount, -} from "../value-objects"; +} from "../common/value-objects"; export interface CustomerInvoiceProps { companyId: UniqueID; isProforma: boolean; - status: CustomerInvoiceStatus; + status: InvoiceStatus; proformaId: Maybe; // <- proforma padre en caso de issue - series: Maybe; - invoiceNumber: CustomerInvoiceNumber; + series: Maybe; + invoiceNumber: InvoiceNumber; invoiceDate: UtcDate; operationDate: Maybe; @@ -127,7 +131,7 @@ export class CustomerInvoice return this.props.proformaId; } - public get status(): CustomerInvoiceStatus { + public get status(): InvoiceStatus { return this.props.status; } @@ -135,7 +139,7 @@ export class CustomerInvoice return this.props.status.canTransitionTo(nextStatus); } - public get series(): Maybe { + public get series(): Maybe { return this.props.series; } diff --git a/modules/customer-invoices/src/api/domain/aggregates/index.ts b/modules/customer-invoices/src/api/domain/aggregates/index.ts deleted file mode 100644 index 8fdd6983..00000000 --- a/modules/customer-invoices/src/api/domain/aggregates/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./customer-invoice"; diff --git a/modules/customer-invoices/src/api/domain/common/customer-invoice-base.aggregate.NOVALE b/modules/customer-invoices/src/api/domain/common/customer-invoice-base.aggregate.NOVALE new file mode 100644 index 00000000..c768a4aa --- /dev/null +++ b/modules/customer-invoices/src/api/domain/common/customer-invoice-base.aggregate.NOVALE @@ -0,0 +1,73 @@ +import { + AggregateRoot, + type CurrencyCode, + type LanguageCode, + type Percentage, + type TextValue, + type UniqueID, + type UtcDate, +} from "@repo/rdx-ddd"; +import type { Maybe } from "@repo/rdx-utils"; + +import type { CustomerInvoiceItems, InvoicePaymentMethod } from "./entities"; +import type { InvoiceNumber, InvoiceRecipient, InvoiceSerie, InvoiceStatus } from "./value-objects"; + +export interface ICustomerInvoiceBaseProps { + companyId: UniqueID; + invoiceNumber: InvoiceNumber; + invoiceDate: UtcDate; + + status: InvoiceStatus; + + series: Maybe; + + operationDate: Maybe; + + customerId: UniqueID; + recipient: Maybe; + + reference: Maybe; + description: Maybe; + notes: Maybe; + + languageCode: LanguageCode; + currencyCode: CurrencyCode; + + items: CustomerInvoiceItems; + + paymentMethod: Maybe; + + discountPercentage: Percentage; +} + +export abstract class CustomerInvoiceBase< + TProps extends ICustomerInvoiceBaseProps, +> extends AggregateRoot { + protected constructor(props: TProps, id?: UniqueID) { + super(props, id); + } + + public get companyId(): UniqueID { + return this.props.companyId; + } + + public get invoiceNumber(): InvoiceNumber { + return this.props.invoiceNumber; + } + + public get invoiceDate(): UtcDate { + return this.props.invoiceDate; + } + + public get currencyCode(): CurrencyCode { + return this.props.currencyCode; + } + + public get languageCode(): LanguageCode { + return this.props.languageCode; + } + + public get recipient(): Maybe { + return this.props.recipient; + } +} diff --git a/modules/customer-invoices/src/api/domain/common/entities/index.ts b/modules/customer-invoices/src/api/domain/common/entities/index.ts new file mode 100644 index 00000000..b7f4fc64 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/common/entities/index.ts @@ -0,0 +1,2 @@ +export * from "./invoice-payment-method"; +export * from "./invoice-taxes"; diff --git a/modules/customer-invoices/src/api/domain/entities/invoice-payment-method/index.ts b/modules/customer-invoices/src/api/domain/common/entities/invoice-payment-method/index.ts similarity index 100% rename from modules/customer-invoices/src/api/domain/entities/invoice-payment-method/index.ts rename to modules/customer-invoices/src/api/domain/common/entities/invoice-payment-method/index.ts diff --git a/modules/customer-invoices/src/api/domain/entities/invoice-payment-method/invoice-payment-method.ts b/modules/customer-invoices/src/api/domain/common/entities/invoice-payment-method/invoice-payment-method.ts similarity index 92% rename from modules/customer-invoices/src/api/domain/entities/invoice-payment-method/invoice-payment-method.ts rename to modules/customer-invoices/src/api/domain/common/entities/invoice-payment-method/invoice-payment-method.ts index bd5b2f42..5c6507ca 100644 --- a/modules/customer-invoices/src/api/domain/entities/invoice-payment-method/invoice-payment-method.ts +++ b/modules/customer-invoices/src/api/domain/common/entities/invoice-payment-method/invoice-payment-method.ts @@ -1,4 +1,4 @@ -import { DomainEntity, UniqueID } from "@repo/rdx-ddd"; +import { DomainEntity, type UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; export interface InvoicePaymentMethodProps { diff --git a/modules/customer-invoices/src/api/domain/entities/invoice-taxes/index.ts b/modules/customer-invoices/src/api/domain/common/entities/invoice-taxes/index.ts similarity index 100% rename from modules/customer-invoices/src/api/domain/entities/invoice-taxes/index.ts rename to modules/customer-invoices/src/api/domain/common/entities/invoice-taxes/index.ts diff --git a/modules/customer-invoices/src/api/domain/entities/invoice-taxes/invoice-tax.ts b/modules/customer-invoices/src/api/domain/common/entities/invoice-taxes/invoice-tax.ts similarity index 98% rename from modules/customer-invoices/src/api/domain/entities/invoice-taxes/invoice-tax.ts rename to modules/customer-invoices/src/api/domain/common/entities/invoice-taxes/invoice-tax.ts index 9a07598d..eccd8a76 100644 --- a/modules/customer-invoices/src/api/domain/entities/invoice-taxes/invoice-tax.ts +++ b/modules/customer-invoices/src/api/domain/common/entities/invoice-taxes/invoice-tax.ts @@ -2,7 +2,7 @@ import type { Tax } from "@erp/core/api"; import { DomainEntity, type UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import type { InvoiceAmount } from "../../value-objects/invoice-amount"; +import type { InvoiceAmount } from "../../value-objects/invoice-amount.vo"; export interface InvoiceTaxProps { tax: Tax; diff --git a/modules/customer-invoices/src/api/domain/entities/invoice-taxes/invoice-taxes.ts b/modules/customer-invoices/src/api/domain/common/entities/invoice-taxes/invoice-taxes.ts similarity index 100% rename from modules/customer-invoices/src/api/domain/entities/invoice-taxes/invoice-taxes.ts rename to modules/customer-invoices/src/api/domain/common/entities/invoice-taxes/invoice-taxes.ts diff --git a/modules/customer-invoices/src/api/domain/common/index.ts b/modules/customer-invoices/src/api/domain/common/index.ts new file mode 100644 index 00000000..6ed21835 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/common/index.ts @@ -0,0 +1,2 @@ +export * from "./entities"; +export * from "./value-objects"; diff --git a/modules/customer-invoices/src/api/domain/common/value-objects/index.ts b/modules/customer-invoices/src/api/domain/common/value-objects/index.ts new file mode 100644 index 00000000..72ab96d5 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/common/value-objects/index.ts @@ -0,0 +1,12 @@ +export * from "./invoice-address-type.vo"; +export * from "./invoice-amount.vo"; +export * from "./invoice-number.vo"; +export * from "./invoice-recipient"; +export * from "./invoice-serie.vo"; +export * from "./invoice-status.vo"; +export * from "./invoice-tax-group.vo"; +export * from "./item-amount.vo"; +export * from "./item-description.vo"; +export * from "./item-discount.vo"; +export * from "./item-quantity.vo"; +export * from "./item-tax-group.vo"; diff --git a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-address-type.ts b/modules/customer-invoices/src/api/domain/common/value-objects/invoice-address-type.vo.ts similarity index 58% rename from modules/customer-invoices/src/api/domain/value-objects/customer-invoice-address-type.ts rename to modules/customer-invoices/src/api/domain/common/value-objects/invoice-address-type.vo.ts index 8a46a563..afa29eff 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-address-type.ts +++ b/modules/customer-invoices/src/api/domain/common/value-objects/invoice-address-type.vo.ts @@ -1,27 +1,27 @@ import { ValueObject } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -interface ICustomerInvoiceAddressTypeProps { +type InvoiceAddressTypeProps = { value: string; -} +}; export enum INVOICE_ADDRESS_TYPE { SHIPPING = "shipping", BILLING = "billing", } -export class CustomerInvoiceAddressType extends ValueObject { +export class InvoiceAddressType extends ValueObject { private static readonly ALLOWED_TYPES = ["shipping", "billing"]; - static create(value: string): Result { - if (!CustomerInvoiceAddressType.ALLOWED_TYPES.includes(value)) { + static create(value: string): Result { + if (!InvoiceAddressType.ALLOWED_TYPES.includes(value)) { return Result.fail( new Error( - `Invalid address type: ${value}. Allowed types are: ${CustomerInvoiceAddressType.ALLOWED_TYPES.join(", ")}` + `Invalid address type: ${value}. Allowed types are: ${InvoiceAddressType.ALLOWED_TYPES.join(", ")}` ) ); } - return Result.ok(new CustomerInvoiceAddressType({ value })); + return Result.ok(new InvoiceAddressType({ value })); } getProps(): string { diff --git a/modules/customer-invoices/src/api/domain/value-objects/invoice-amount.ts b/modules/customer-invoices/src/api/domain/common/value-objects/invoice-amount.vo.ts similarity index 100% rename from modules/customer-invoices/src/api/domain/value-objects/invoice-amount.ts rename to modules/customer-invoices/src/api/domain/common/value-objects/invoice-amount.vo.ts diff --git a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-number.ts b/modules/customer-invoices/src/api/domain/common/value-objects/invoice-number.vo.ts similarity index 59% rename from modules/customer-invoices/src/api/domain/value-objects/customer-invoice-number.ts rename to modules/customer-invoices/src/api/domain/common/value-objects/invoice-number.vo.ts index acb8f116..68d0aa12 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-number.ts +++ b/modules/customer-invoices/src/api/domain/common/value-objects/invoice-number.vo.ts @@ -2,11 +2,11 @@ import { DomainValidationError, ValueObject } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { z } from "zod/v4"; -interface ICustomerInvoiceNumberProps { +type InvoiceNumberProps = { value: string; -} +}; -export class CustomerInvoiceNumber extends ValueObject { +export class InvoiceNumber extends ValueObject { private static readonly MAX_LENGTH = 12; private static readonly FIELD = "invoiceNumber"; private static readonly ERROR_CODE = "INVALID_INVOICE_NUMBER"; @@ -15,26 +15,22 @@ export class CustomerInvoiceNumber extends ValueObject; @@ -20,7 +20,7 @@ export interface InvoiceRecipientProps { postalCode: Maybe; province: Maybe; country: Maybe; -} +}; export class InvoiceRecipient extends ValueObject { protected static validate(values: InvoiceRecipientProps) { diff --git a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-serie.ts b/modules/customer-invoices/src/api/domain/common/value-objects/invoice-serie.vo.ts similarity index 52% rename from modules/customer-invoices/src/api/domain/value-objects/customer-invoice-serie.ts rename to modules/customer-invoices/src/api/domain/common/value-objects/invoice-serie.vo.ts index 6a0ae141..4daa29ec 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-serie.ts +++ b/modules/customer-invoices/src/api/domain/common/value-objects/invoice-serie.vo.ts @@ -2,11 +2,11 @@ import { DomainValidationError, ValueObject } from "@repo/rdx-ddd"; import { Maybe, Result } from "@repo/rdx-utils"; import { z } from "zod/v4"; -interface ICustomerInvoiceSerieProps { +type InvoiceSerieProps = { value: string; -} +}; -export class CustomerInvoiceSerie extends ValueObject { +export class InvoiceSerie extends ValueObject { private static readonly MAX_LENGTH = 10; private static readonly FIELD = "invoiceSeries"; private static readonly ERROR_CODE = "INVALID_INVOICE_SERIE"; @@ -15,34 +15,30 @@ export class CustomerInvoiceSerie extends ValueObject, Error> { + static createNullable(value?: string): Result, Error> { if (!value || value.trim() === "") { - return Result.ok(Maybe.none()); + return Result.ok(Maybe.none()); } - return CustomerInvoiceSerie.create(value).map((value) => Maybe.some(value)); + return InvoiceSerie.create(value).map((value) => Maybe.some(value)); } getProps(): string { diff --git a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-status.ts b/modules/customer-invoices/src/api/domain/common/value-objects/invoice-status.vo.ts similarity index 57% rename from modules/customer-invoices/src/api/domain/value-objects/customer-invoice-status.ts rename to modules/customer-invoices/src/api/domain/common/value-objects/invoice-status.vo.ts index 15dc1450..a7ae5319 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-status.ts +++ b/modules/customer-invoices/src/api/domain/common/value-objects/invoice-status.vo.ts @@ -1,9 +1,9 @@ import { DomainValidationError, ValueObject } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -interface ICustomerInvoiceStatusProps { +type IInvoiceStatusProps = { value: string; -} +}; export enum INVOICE_STATUS { DRAFT = "draft", // <- Proforma @@ -24,54 +24,50 @@ const INVOICE_TRANSITIONS: Record = { issued: [], }; -export class CustomerInvoiceStatus extends ValueObject { +export class InvoiceStatus extends ValueObject { private static readonly ALLOWED_STATUSES = ["draft", "sent", "approved", "rejected", "issued"]; private static readonly FIELD = "invoiceStatus"; private static readonly ERROR_CODE = "INVALID_INVOICE_STATUS"; - static create(value: string): Result { - if (!CustomerInvoiceStatus.ALLOWED_STATUSES.includes(value)) { + static create(value: string): Result { + if (!InvoiceStatus.ALLOWED_STATUSES.includes(value)) { const detail = `Estado de la factura no válido: ${value}`; return Result.fail( - new DomainValidationError( - CustomerInvoiceStatus.ERROR_CODE, - CustomerInvoiceStatus.FIELD, - detail - ) + new DomainValidationError(InvoiceStatus.ERROR_CODE, InvoiceStatus.FIELD, detail) ); } return Result.ok( value === "rejected" - ? CustomerInvoiceStatus.createRejected() + ? InvoiceStatus.createRejected() : value === "sent" - ? CustomerInvoiceStatus.createSent() + ? InvoiceStatus.createSent() : value === "issued" - ? CustomerInvoiceStatus.createIssued() + ? InvoiceStatus.createIssued() : value === "approved" - ? CustomerInvoiceStatus.createApproved() - : CustomerInvoiceStatus.createDraft() + ? InvoiceStatus.createApproved() + : InvoiceStatus.createDraft() ); } - public static createDraft(): CustomerInvoiceStatus { - return new CustomerInvoiceStatus({ value: INVOICE_STATUS.DRAFT }); + public static createDraft(): InvoiceStatus { + return new InvoiceStatus({ value: INVOICE_STATUS.DRAFT }); } - public static createIssued(): CustomerInvoiceStatus { - return new CustomerInvoiceStatus({ value: INVOICE_STATUS.ISSUED }); + public static createIssued(): InvoiceStatus { + return new InvoiceStatus({ value: INVOICE_STATUS.ISSUED }); } - public static createSent(): CustomerInvoiceStatus { - return new CustomerInvoiceStatus({ value: INVOICE_STATUS.SENT }); + public static createSent(): InvoiceStatus { + return new InvoiceStatus({ value: INVOICE_STATUS.SENT }); } - public static createApproved(): CustomerInvoiceStatus { - return new CustomerInvoiceStatus({ value: INVOICE_STATUS.APPROVED }); + public static createApproved(): InvoiceStatus { + return new InvoiceStatus({ value: INVOICE_STATUS.APPROVED }); } - public static createRejected(): CustomerInvoiceStatus { - return new CustomerInvoiceStatus({ value: INVOICE_STATUS.REJECTED }); + public static createRejected(): InvoiceStatus { + return new InvoiceStatus({ value: INVOICE_STATUS.REJECTED }); } isDraft(): boolean { diff --git a/modules/customer-invoices/src/api/domain/value-objects/invoice-tax-group.ts b/modules/customer-invoices/src/api/domain/common/value-objects/invoice-tax-group.vo.ts similarity index 95% rename from modules/customer-invoices/src/api/domain/value-objects/invoice-tax-group.ts rename to modules/customer-invoices/src/api/domain/common/value-objects/invoice-tax-group.vo.ts index 4f0db471..14ac872e 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/invoice-tax-group.ts +++ b/modules/customer-invoices/src/api/domain/common/value-objects/invoice-tax-group.vo.ts @@ -2,15 +2,15 @@ import type { Tax } from "@erp/core/api"; import { ValueObject } from "@repo/rdx-ddd"; import { type Maybe, Result } from "@repo/rdx-utils"; -import { InvoiceAmount } from "./invoice-amount"; -import type { ItemTaxGroup } from "./item-tax-group"; +import { InvoiceAmount } from "./invoice-amount.vo"; +import type { ItemTaxGroup } from "./item-tax-group.vo"; -export interface InvoiceTaxGroupProps { +export type InvoiceTaxGroupProps = { taxableAmount: InvoiceAmount; iva: Tax; rec: Maybe; // si existe retention: Maybe; // si existe -} +}; export class InvoiceTaxGroup extends ValueObject { static create(props: InvoiceTaxGroupProps) { diff --git a/modules/customer-invoices/src/api/domain/value-objects/item-amount.ts b/modules/customer-invoices/src/api/domain/common/value-objects/item-amount.vo.ts similarity index 100% rename from modules/customer-invoices/src/api/domain/value-objects/item-amount.ts rename to modules/customer-invoices/src/api/domain/common/value-objects/item-amount.vo.ts diff --git a/modules/customer-invoices/src/api/domain/common/value-objects/item-description.vo.ts b/modules/customer-invoices/src/api/domain/common/value-objects/item-description.vo.ts new file mode 100644 index 00000000..fba04dff --- /dev/null +++ b/modules/customer-invoices/src/api/domain/common/value-objects/item-description.vo.ts @@ -0,0 +1,55 @@ +import { DomainValidationError, ValueObject } from "@repo/rdx-ddd"; +import { Maybe, Result } from "@repo/rdx-utils"; +import { z } from "zod/v4"; + +type ItemDescriptionProps = { + value: string; +}; + +export class ItemDescription extends ValueObject { + private static readonly MAX_LENGTH = 2000; + private static readonly FIELD = "itemDescription"; + private static readonly ERROR_CODE = "INVALID_ITEM_DESCRIPTION"; + + protected static validate(value: string) { + const schema = z + .string() + .trim() + .max(ItemDescription.MAX_LENGTH, { + message: `Description must be at most ${ItemDescription.MAX_LENGTH} characters long`, + }); + return schema.safeParse(value); + } + + static create(value: string) { + const valueIsValid = ItemDescription.validate(value); + + if (!valueIsValid.success) { + const detail = valueIsValid.error.message; + return Result.fail( + new DomainValidationError(ItemDescription.ERROR_CODE, ItemDescription.FIELD, detail) + ); + } + return Result.ok(new ItemDescription({ value })); + } + + static createNullable(value?: string): Result, Error> { + if (!value || value.trim() === "") { + return Result.ok(Maybe.none()); + } + + return ItemDescription.create(value).map((value) => Maybe.some(value)); + } + + getProps(): string { + return this.props.value; + } + + toString() { + return String(this.props.value); + } + + toPrimitive() { + return this.getProps(); + } +} diff --git a/modules/customer-invoices/src/api/domain/value-objects/item-discount.ts b/modules/customer-invoices/src/api/domain/common/value-objects/item-discount.vo.ts similarity index 100% rename from modules/customer-invoices/src/api/domain/value-objects/item-discount.ts rename to modules/customer-invoices/src/api/domain/common/value-objects/item-discount.vo.ts diff --git a/modules/customer-invoices/src/api/domain/value-objects/item-quantity.ts b/modules/customer-invoices/src/api/domain/common/value-objects/item-quantity.vo.ts similarity index 100% rename from modules/customer-invoices/src/api/domain/value-objects/item-quantity.ts rename to modules/customer-invoices/src/api/domain/common/value-objects/item-quantity.vo.ts diff --git a/modules/customer-invoices/src/api/domain/value-objects/item-tax-group.ts b/modules/customer-invoices/src/api/domain/common/value-objects/item-tax-group.vo.ts similarity index 100% rename from modules/customer-invoices/src/api/domain/value-objects/item-tax-group.ts rename to modules/customer-invoices/src/api/domain/common/value-objects/item-tax-group.vo.ts diff --git a/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/index.ts b/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/index.ts deleted file mode 100644 index 32b0f623..00000000 --- a/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./customer-invoice-item"; -export * from "./customer-invoice-items"; diff --git a/modules/customer-invoices/src/api/domain/entities/index.ts b/modules/customer-invoices/src/api/domain/entities/index.ts deleted file mode 100644 index a6986742..00000000 --- a/modules/customer-invoices/src/api/domain/entities/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./customer-invoice-items"; -export * from "./invoice-payment-method"; -export * from "./invoice-taxes"; -export * from "./verifactu-record"; diff --git a/modules/customer-invoices/src/api/domain/index.ts b/modules/customer-invoices/src/api/domain/index.ts index 06917160..cece438d 100644 --- a/modules/customer-invoices/src/api/domain/index.ts +++ b/modules/customer-invoices/src/api/domain/index.ts @@ -1,7 +1,3 @@ -export * from "./aggregates"; -export * from "./entities"; -export * from "./errors"; -export * from "./repositories"; -export * from "./services"; -export * from "./specs"; -export * from "./value-objects"; +export * from "./common"; +export * from "./issued-invoices"; +export * from "./proformas"; diff --git a/modules/customer-invoices/src/api/domain/issued-invoices/entities/index.ts b/modules/customer-invoices/src/api/domain/issued-invoices/entities/index.ts new file mode 100644 index 00000000..655ac636 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/issued-invoices/entities/index.ts @@ -0,0 +1,4 @@ +export * from "./issued-invoice-items"; +export * from "./issued-invoice-tax-group.entity"; +export * from "./issued-invoice-taxes.entity"; +export * from "./verifactu-record.entity"; diff --git a/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-items/index.ts b/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-items/index.ts new file mode 100644 index 00000000..c4980e98 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-items/index.ts @@ -0,0 +1,2 @@ +export * from "./issued-invoice-item.entity"; +export * from "./issued-invoice-items.collection"; 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 new file mode 100644 index 00000000..9953c304 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-items/issued-invoice-item.entity.ts @@ -0,0 +1,161 @@ +import { type CurrencyCode, DomainEntity, type LanguageCode, type UniqueID } from "@repo/rdx-ddd"; +import { type Maybe, Result } from "@repo/rdx-utils"; + +import type { + InvoiceAmount, + ItemAmount, + ItemDescription, + ItemDiscount, + ItemQuantity, + ItemTaxGroup, +} from "../../../common"; + +/** + * + * Entidad de línea de factura. + * + * Representa una fotografía exacta de los importes en el momento + * de emisión. No contiene lógica de cálculo. + * + * Todos los importes están previamente calculados y congelados. + */ + +export type IssuedInvoiceItemProps = { + description: Maybe; + + quantity: Maybe; + unitAmount: Maybe; + + subtotalAmount: InvoiceAmount; + + itemDiscountPercentage: Maybe; + itemDiscountAmount: InvoiceAmount; + + globalDiscountPercentage: Maybe; + globalDiscountAmount: InvoiceAmount; + + totalDiscountAmount: InvoiceAmount; + + taxableAmount: InvoiceAmount; + + ivaAmount: InvoiceAmount; + recAmount: InvoiceAmount; + retentionAmount: InvoiceAmount; + + taxesAmount: InvoiceAmount; + totalAmount: InvoiceAmount; + + taxes: ItemTaxGroup; + + languageCode: LanguageCode; + currencyCode: CurrencyCode; +}; + +export class IssuedInvoiceItem extends DomainEntity { + protected _isValued!: boolean; + + public static create( + props: IssuedInvoiceItemProps, + id?: UniqueID + ): Result { + const item = new IssuedInvoiceItem(props, id); + + // Reglas de negocio / validaciones + // ... + // ... + + return Result.ok(item); + } + + protected constructor(props: IssuedInvoiceItemProps, id?: UniqueID) { + super(props, id); + + this._isValued = this.quantity.isSome() || this.unitAmount.isSome(); + } + + // Getters + + get isValued(): boolean { + return this._isValued; + } + + get description() { + return this.props.description; + } + + get quantity() { + return this.props.quantity; + } + + get unitAmount() { + return this.props.unitAmount; + } + + get itemDiscountPercentage() { + return this.props.itemDiscountPercentage; + } + + get itemDiscountAmount() { + return this.props.itemDiscountAmount; + } + + get globalDiscountPercentage() { + return this.props.globalDiscountPercentage; + } + + get globalDiscountAmount() { + return this.props.globalDiscountAmount; + } + + get taxes() { + return this.props.taxes; + } + + get languageCode() { + return this.props.languageCode; + } + + get currencyCode() { + return this.props.currencyCode; + } + + public get subtotalAmount(): ItemAmount { + return this.props.subtotalAmount; + } + + public get totalDiscountAmount(): ItemAmount { + return this.props.totalDiscountAmount; + } + + public get taxableAmount(): ItemAmount { + return this.props.taxableAmount; + } + + public get ivaAmount(): InvoiceAmount { + return this.props.ivaAmount; + } + + public get recAmount(): InvoiceAmount { + return this.props.recAmount; + } + + public get retentionAmount(): InvoiceAmount { + return this.props.retentionAmount; + } + + public get taxesAmount(): ItemAmount { + return this.props.taxesAmount; + } + + public get totalAmount(): ItemAmount { + return this.props.totalAmount; + } + + getProps(): IssuedInvoiceItemProps { + return this.props; + } + + toPrimitive() { + return this.getProps(); + } +} diff --git a/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-items/issued-invoice-items.collection.ts b/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-items/issued-invoice-items.collection.ts new file mode 100644 index 00000000..9cd84309 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-items/issued-invoice-items.collection.ts @@ -0,0 +1,66 @@ +import type { CurrencyCode, LanguageCode, Percentage } from "@repo/rdx-ddd"; +import { Collection } from "@repo/rdx-utils"; + +import { InvoiceAmount, ItemDiscount } from "../../../common"; + +import type { IssuedInvoiceItem } from "./issued-invoice-item.entity"; + +export type IssuedInvoiceItemsProps = { + items?: IssuedInvoiceItem[]; + languageCode: LanguageCode; + currencyCode: CurrencyCode; + globalDiscountPercentage: Percentage; +}; + +export class IssuedInvoiceItems extends Collection { + private _languageCode!: LanguageCode; + private _currencyCode!: CurrencyCode; + private _globalDiscountPercentage!: Percentage; + + constructor(props: IssuedInvoiceItemsProps) { + super(props.items ?? []); + this._languageCode = props.languageCode; + this._currencyCode = props.currencyCode; + this._globalDiscountPercentage = props.globalDiscountPercentage; + } + + public static create(props: IssuedInvoiceItemsProps): IssuedInvoiceItems { + return new IssuedInvoiceItems(props); + } + + /** + * @summary Añade un nuevo ítem a la colección. + * @param item - El ítem de factura a añadir. + * @returns `true` si el ítem fue añadido correctamente; `false` si fue rechazado. + * @remarks + * Sólo se aceptan ítems cuyo `LanguageCode` y `CurrencyCode` coincidan con + * los de la colección. Si no coinciden, el método devuelve `false` sin modificar + * la colección. + */ + add(item: IssuedInvoiceItem): boolean { + // Antes de añadir un nuevo item, debo comprobar que el item a añadir + // tiene el mismo "currencyCode" y "languageCode" que la colección de items. + if ( + !( + this._languageCode.equals(item.languageCode) && + this._currencyCode.equals(item.currencyCode) && + this._globalDiscountPercentage.equals( + item.globalDiscountPercentage.match( + (v) => v, + () => ItemDiscount.zero() + ) + ) + ) + ) { + return false; + } + return super.add(item); + } + + public getTotalAmount(): InvoiceAmount { + return this.getAll().reduce( + (acc, item) => acc.add(item.getProps().totalAmount), + InvoiceAmount.zero(this.getAll()[0]?.getProps().totalAmount.currencyCode ?? "EUR") + ); + } +} diff --git a/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-tax-group.entity.ts b/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-tax-group.entity.ts new file mode 100644 index 00000000..382ed87f --- /dev/null +++ b/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-tax-group.entity.ts @@ -0,0 +1,35 @@ +import { DomainEntity, type Percentage, type UniqueID } from "@repo/rdx-ddd"; +import { type Maybe, Result } from "@repo/rdx-utils"; + +import type { InvoiceAmount } from "../../common"; + +export type IssuedInvoiceTaxGroupProps = { + taxableAmount: InvoiceAmount; + + ivaCode: string; + ivaPercentage: Percentage; + ivaAmount: InvoiceAmount; + + recCode: Maybe; + recPercentage: Maybe; + recAmount: InvoiceAmount; + + retentionCode: Maybe; + retentionPercentage: Maybe; + retentionAmount: InvoiceAmount; + + totalAmount: InvoiceAmount; +}; + +export class IssuedInvoiceTaxGroup extends DomainEntity { + public static create( + props: IssuedInvoiceTaxGroupProps, + id?: UniqueID + ): Result { + return Result.ok(new IssuedInvoiceTaxGroup(props, id)); + } + + public getProps(): IssuedInvoiceTaxGroupProps { + return this.props; + } +} diff --git a/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-taxes.entity.ts b/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-taxes.entity.ts new file mode 100644 index 00000000..a93d2d6a --- /dev/null +++ b/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-taxes.entity.ts @@ -0,0 +1,13 @@ +import { Collection } from "@repo/rdx-utils"; + +import type { IssuedInvoiceTaxGroup } from "./issued-invoice-tax-group.entity"; + +export class IssuedInvoiceTaxes extends Collection { + constructor(items: IssuedInvoiceTaxGroup[] = []) { + super(items); + } + + public static create(items: IssuedInvoiceTaxGroup[] = []): IssuedInvoiceTaxes { + return new IssuedInvoiceTaxes(items); + } +} diff --git a/modules/customer-invoices/src/api/domain/entities/verifactu-record.ts b/modules/customer-invoices/src/api/domain/issued-invoices/entities/verifactu-record.entity.ts similarity index 92% rename from modules/customer-invoices/src/api/domain/entities/verifactu-record.ts rename to modules/customer-invoices/src/api/domain/issued-invoices/entities/verifactu-record.entity.ts index f472ed08..236a177b 100644 --- a/modules/customer-invoices/src/api/domain/entities/verifactu-record.ts +++ b/modules/customer-invoices/src/api/domain/issued-invoices/entities/verifactu-record.entity.ts @@ -1,15 +1,15 @@ import { DomainEntity, type URLAddress, type UniqueID, toEmptyString } from "@repo/rdx-ddd"; import { type Maybe, Result } from "@repo/rdx-utils"; -import type { VerifactuRecordEstado } from "../value-objects"; +import type { VerifactuRecordEstado } from "../value-objects/verifactu-status.vo"; -export interface VerifactuRecordProps { +export type VerifactuRecordProps = { estado: VerifactuRecordEstado; url: Maybe; qrCode: Maybe; uuid: Maybe; operacion: Maybe; -} +}; export class VerifactuRecord extends DomainEntity { public static create(props: VerifactuRecordProps, id?: UniqueID): Result { diff --git a/modules/customer-invoices/src/api/domain/issued-invoices/index.ts b/modules/customer-invoices/src/api/domain/issued-invoices/index.ts new file mode 100644 index 00000000..867a6457 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/issued-invoices/index.ts @@ -0,0 +1,3 @@ +export * from "./entities"; +export * from "./issued-invoice.aggregate"; +export * from "./value-objects"; diff --git a/modules/customer-invoices/src/api/domain/issued-invoices/issued-invoice.aggregate.ts b/modules/customer-invoices/src/api/domain/issued-invoices/issued-invoice.aggregate.ts new file mode 100644 index 00000000..1c9a5f49 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/issued-invoices/issued-invoice.aggregate.ts @@ -0,0 +1,197 @@ +import { + AggregateRoot, + type CurrencyCode, + DomainValidationError, + type LanguageCode, + type Percentage, + type TextValue, + type UniqueID, + type UtcDate, +} from "@repo/rdx-ddd"; +import { type Maybe, Result } from "@repo/rdx-utils"; + +import type { InvoicePaymentMethod } from "../common/entities"; +import type { + InvoiceAmount, + InvoiceNumber, + InvoiceRecipient, + InvoiceSerie, + InvoiceStatus, +} from "../common/value-objects"; + +import { IssuedInvoiceItems, type IssuedInvoiceTaxes, type VerifactuRecord } from "./entities"; + +export type IssuedInvoiceProps = { + companyId: UniqueID; + status: InvoiceStatus; + + proformaId: Maybe; // <- proforma padre en caso de issue + + series: Maybe; + invoiceNumber: InvoiceNumber; + + invoiceDate: UtcDate; + operationDate: Maybe; + + customerId: UniqueID; + recipient: Maybe; + + reference: Maybe; + description: Maybe; + notes: Maybe; + + languageCode: LanguageCode; + currencyCode: CurrencyCode; + + paymentMethod: Maybe; + + items: IssuedInvoiceItems; + taxes: IssuedInvoiceTaxes; + + subtotalAmount: InvoiceAmount; + + itemDiscountAmount: InvoiceAmount; + discountPercentage: Percentage; + globalDiscountAmount: InvoiceAmount; + totalDiscountAmount: InvoiceAmount; + + taxableAmount: InvoiceAmount; + + ivaAmount: InvoiceAmount; + recAmount: InvoiceAmount; + retentionAmount: InvoiceAmount; + + taxesAmount: InvoiceAmount; + totalAmount: InvoiceAmount; + + verifactu: Maybe; +}; + +export class IssuedInvoice extends AggregateRoot { + private _items!: IssuedInvoiceItems; + + protected constructor(props: IssuedInvoiceProps, id?: UniqueID) { + super(props, id); + this._items = + props.items || + IssuedInvoiceItems.create({ + languageCode: props.languageCode, + currencyCode: props.currencyCode, + globalDiscountPercentage: props.discountPercentage, + }); + } + + static create(props: IssuedInvoiceProps, id?: UniqueID): Result { + if (!props.recipient) { + return Result.fail( + new DomainValidationError( + "MISSING_RECIPIENT", + "recipient", + "Issued invoice requires recipient" + ) + ); + } + + const issuedInvoice = new IssuedInvoice(props, id); + + // Reglas de negocio / validaciones + // ... + + // 🔹 Disparar evento de dominio "IssuedInvoiceAuthenticatedEvent" + //const { customerInvoice } = props; + //user.addDomainEvent(new IssuedInvoiceAuthenticatedEvent(id, customerInvoice.toString())); + + return Result.ok(issuedInvoice); + } + + // Getters + + public get companyId(): UniqueID { + return this.props.companyId; + } + + public get customerId(): UniqueID { + return this.props.customerId; + } + + public get proformaId(): Maybe { + return this.props.proformaId; + } + + public get status(): InvoiceStatus { + return this.props.status; + } + + public get series(): Maybe { + return this.props.series; + } + + public get invoiceNumber() { + return this.props.invoiceNumber; + } + + public get invoiceDate(): UtcDate { + return this.props.invoiceDate; + } + + public get operationDate(): Maybe { + return this.props.operationDate; + } + + public get reference(): Maybe { + return this.props.reference; + } + + public get description(): Maybe { + return this.props.description; + } + + public get notes(): Maybe { + return this.props.notes; + } + + public get recipient(): Maybe { + return this.props.recipient; + } + + public get paymentMethod(): Maybe { + return this.props.paymentMethod; + } + + public get languageCode(): LanguageCode { + return this.props.languageCode; + } + + public get currencyCode(): CurrencyCode { + return this.props.currencyCode; + } + + public get verifactu(): Maybe { + return this.props.verifactu; + } + + public get discountPercentage(): Percentage { + return this.props.discountPercentage; + } + + public get taxes(): IssuedInvoiceTaxes { + return this.props.taxes; + } + + // Method to get the complete list of line items + public get items(): IssuedInvoiceItems { + return this._items; + } + + public get hasRecipient() { + return this.recipient.isSome(); + } + + public get hasPaymentMethod() { + return this.paymentMethod.isSome(); + } + + public getProps(): IssuedInvoiceProps { + return this.props; + } +} diff --git a/modules/customer-invoices/src/api/domain/issued-invoices/value-objects/index.ts b/modules/customer-invoices/src/api/domain/issued-invoices/value-objects/index.ts new file mode 100644 index 00000000..3afe84c7 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/issued-invoices/value-objects/index.ts @@ -0,0 +1,2 @@ +export * from "./invoice-tax-group.vo"; +export * from "./verifactu-status.vo"; diff --git a/modules/customer-invoices/src/api/domain/issued-invoices/value-objects/invoice-tax-group.vo.ts b/modules/customer-invoices/src/api/domain/issued-invoices/value-objects/invoice-tax-group.vo.ts new file mode 100644 index 00000000..6db53fd6 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/issued-invoices/value-objects/invoice-tax-group.vo.ts @@ -0,0 +1,117 @@ +import { type Percentage, ValueObject } from "@repo/rdx-ddd"; +import { type Maybe, Result } from "@repo/rdx-utils"; + +import type { InvoiceAmount } from "../../common"; + +export type IssuedInvoiceTaxGroupProps = { + ivaCode: string; + ivaPercentage: Percentage; + ivaAmount: InvoiceAmount; + + recCode: Maybe; + recPercentage: Maybe; + recAmount: InvoiceAmount; + + retentionCode: Maybe; + retentionPercentage: Maybe; + retentionAmount: InvoiceAmount; + + taxableAmount: InvoiceAmount; + totalAmount: InvoiceAmount; +}; + +export class IssuedInvoiceTaxGroup extends ValueObject { + static create(props: IssuedInvoiceTaxGroupProps) { + return Result.ok(new IssuedInvoiceTaxGroup(props)); + } + + // IVA + + get ivaCode(): string { + return this.props.ivaCode; + } + get ivaPercentage(): Percentage { + return this.props.ivaPercentage; + } + get ivaAmount(): InvoiceAmount { + return this.props.ivaAmount; + } + + // Recargo de equivalencia (rec) + + get recCode(): Maybe { + return this.props.recCode; + } + get recPercentage(): Maybe { + return this.props.recPercentage; + } + + get recAmount(): Maybe { + return this.props.recAmount; + } + + // Retención (ret) + + get retentionCode(): Maybe { + return this.props.retentionCode; + } + + get retentionPercentage(): Maybe { + return this.props.retentionPercentage; + } + + get retentionAmount(): Maybe { + return this.props.retentionAmount; + } + + // + + get taxableAmount(): InvoiceAmount { + return this.props.taxableAmount; + } + + get totalAmount(): InvoiceAmount { + return this.props.totalAmount; + } + + /** + * Devuelve únicamente los códigos existentes: ["iva_21", "rec_5_2"] + */ + public getCodesArray(): string[] { + const codes: string[] = []; + + // IVA + codes.push(this.props.ivaCode); + + this.props.rec.match( + (t) => codes.push(t.code), + () => { + // + } + ); + + this.props.retention.match( + (t) => codes.push(t.code), + () => { + // + } + ); + + return codes; + } + + /** + * Devuelve una cadena tipo: "iva_21, rec_5_2" + */ + public getCodesToString(): string { + return this.getCodesArray().join(", "); + } + + getProps() { + return this.props; + } + + toPrimitive() { + return this.getProps(); + } +} diff --git a/modules/customer-invoices/src/api/domain/value-objects/verifactu-status.ts b/modules/customer-invoices/src/api/domain/issued-invoices/value-objects/verifactu-status.vo.ts similarity index 100% rename from modules/customer-invoices/src/api/domain/value-objects/verifactu-status.ts rename to modules/customer-invoices/src/api/domain/issued-invoices/value-objects/verifactu-status.vo.ts diff --git a/modules/customer-invoices/src/api/domain/proformas/index.ts b/modules/customer-invoices/src/api/domain/proformas/index.ts new file mode 100644 index 00000000..162cb4a5 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/proformas/index.ts @@ -0,0 +1,2 @@ +export * from "./proforma.aggregate"; +export * from "./proforma-items"; diff --git a/modules/customer-invoices/src/api/domain/proformas/proforma-items/index.ts b/modules/customer-invoices/src/api/domain/proformas/proforma-items/index.ts new file mode 100644 index 00000000..3787383f --- /dev/null +++ b/modules/customer-invoices/src/api/domain/proformas/proforma-items/index.ts @@ -0,0 +1,2 @@ +export * from "./proforma-item.entity"; +export * from "./proforma-items.collection"; diff --git a/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.ts b/modules/customer-invoices/src/api/domain/proformas/proforma-items/proforma-item.entity.ts similarity index 90% rename from modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.ts rename to modules/customer-invoices/src/api/domain/proformas/proforma-items/proforma-item.entity.ts index aef308b1..2da63918 100644 --- a/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.ts +++ b/modules/customer-invoices/src/api/domain/proformas/proforma-items/proforma-item.entity.ts @@ -2,12 +2,12 @@ import { type CurrencyCode, DomainEntity, type LanguageCode, type UniqueID } fro import { type Maybe, Result } from "@repo/rdx-utils"; import { - type CustomerInvoiceItemDescription, ItemAmount, + type ItemDescription, ItemDiscount, ItemQuantity, -} from "../../value-objects"; -import type { ItemTaxGroup } from "../../value-objects/item-tax-group"; + type ItemTaxGroup, +} from "../../common"; /** * @@ -26,8 +26,8 @@ import type { ItemTaxGroup } from "../../value-objects/item-tax-group"; * */ -export interface CustomerInvoiceItemProps { - description: Maybe; +export type ProformaItemProps = { + description: Maybe; quantity: Maybe; // Cantidad de unidades unitAmount: Maybe; // Precio unitario en la moneda de la factura @@ -38,16 +38,13 @@ export interface CustomerInvoiceItemProps { languageCode: LanguageCode; currencyCode: CurrencyCode; -} +}; -export class CustomerInvoiceItem extends DomainEntity { +export class ProformaItem extends DomainEntity { protected _isValued!: boolean; - public static create( - props: CustomerInvoiceItemProps, - id?: UniqueID - ): Result { - const item = new CustomerInvoiceItem(props, id); + public static create(props: ProformaItemProps, id?: UniqueID): Result { + const item = new ProformaItem(props, id); // Reglas de negocio / validaciones // ... @@ -56,7 +53,7 @@ export class CustomerInvoiceItem extends DomainEntity return Result.ok(item); } - protected constructor(props: CustomerInvoiceItemProps, id?: UniqueID) { + protected constructor(props: ProformaItemProps, id?: UniqueID) { super(props, id); this._isValued = this.quantity.isSome() || this.unitAmount.isSome(); @@ -100,7 +97,7 @@ export class CustomerInvoiceItem extends DomainEntity return this.props.currencyCode; } - getProps(): CustomerInvoiceItemProps { + getProps(): ProformaItemProps { return this.props; } diff --git a/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-items.ts b/modules/customer-invoices/src/api/domain/proformas/proforma-items/proforma-items.collection.ts similarity index 63% rename from modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-items.ts rename to modules/customer-invoices/src/api/domain/proformas/proforma-items/proforma-items.collection.ts index 88a94e0e..8f1288ba 100644 --- a/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-items.ts +++ b/modules/customer-invoices/src/api/domain/proformas/proforma-items/proforma-items.collection.ts @@ -1,39 +1,39 @@ import type { CurrencyCode, LanguageCode, Percentage } from "@repo/rdx-ddd"; import { Collection } from "@repo/rdx-utils"; -import { ItemAmount, ItemDiscount, type ItemTaxGroup } from "../../value-objects"; +import { ItemAmount, ItemDiscount, type ItemTaxGroup } from "../../common"; -import type { CustomerInvoiceItem } from "./customer-invoice-item"; +import type { ProformaItem } from "./proforma-item.entity"; -export interface CustomerInvoiceItemsProps { - items?: CustomerInvoiceItem[]; +export type ProformaItemsProps = { + items?: ProformaItem[]; languageCode: LanguageCode; currencyCode: CurrencyCode; globalDiscountPercentage: Percentage; -} +}; -export class CustomerInvoiceItems extends Collection { - private _languageCode!: LanguageCode; - private _currencyCode!: CurrencyCode; - private _globalDiscountPercentage!: Percentage; +export class ProformaItems extends Collection { + private languageCode!: LanguageCode; + private currencyCode!: CurrencyCode; + private globalDiscountPercentage!: Percentage; - constructor(props: CustomerInvoiceItemsProps) { + constructor(props: ProformaItemsProps) { super(props.items ?? []); - this._languageCode = props.languageCode; - this._currencyCode = props.currencyCode; - this._globalDiscountPercentage = props.globalDiscountPercentage; + this.languageCode = props.languageCode; + this.currencyCode = props.currencyCode; + this.globalDiscountPercentage = props.globalDiscountPercentage; } - public static create(props: CustomerInvoiceItemsProps): CustomerInvoiceItems { - return new CustomerInvoiceItems(props); + public static create(props: ProformaItemsProps): ProformaItems { + return new ProformaItems(props); } // Helpers - private _sumAmounts(selector: (item: CustomerInvoiceItem) => ItemAmount): ItemAmount { + private _sumAmounts(selector: (item: ProformaItem) => ItemAmount): ItemAmount { return this.getAll().reduce( (acc, item) => acc.add(selector(item)), - ItemAmount.zero(this._currencyCode.code) + ItemAmount.zero(this.currencyCode.code) ); } @@ -41,9 +41,9 @@ export class CustomerInvoiceItems extends Collection { * @summary Helper puro para sumar impuestos individuales por tipo. */ private _calculateIndividualTaxes() { - let iva = ItemAmount.zero(this._currencyCode.code); - let rec = ItemAmount.zero(this._currencyCode.code); - let retention = ItemAmount.zero(this._currencyCode.code); + let iva = ItemAmount.zero(this.currencyCode.code); + let rec = ItemAmount.zero(this.currencyCode.code); + let retention = ItemAmount.zero(this.currencyCode.code); for (const item of this.getAll()) { const { ivaAmount, recAmount, retentionAmount } = item.getIndividualTaxAmounts(); @@ -67,23 +67,21 @@ export class CustomerInvoiceItems extends Collection { * los de la colección. Si no coinciden, el método devuelve `false` sin modificar * la colección. */ - add(item: CustomerInvoiceItem): boolean { + add(item: ProformaItem): boolean { // Antes de añadir un nuevo item, debo comprobar que el item a añadir // tiene el mismo "currencyCode" y "languageCode" que la colección de items. - if ( - !( - this._languageCode.equals(item.languageCode) && - this._currencyCode.equals(item.currencyCode) && - this._globalDiscountPercentage.equals( - item.globalDiscountPercentage.match( - (v) => v, - () => ItemDiscount.zero() - ) + const same = + this.languageCode.equals(item.languageCode) && + this.currencyCode.equals(item.currencyCode) && + this.globalDiscountPercentage.equals( + item.globalDiscountPercentage.match( + (v) => v, + () => ItemDiscount.zero() ) - ) - ) { - return false; - } + ); + + if (!same) return false; + return super.add(item); } @@ -95,20 +93,20 @@ export class CustomerInvoiceItems extends Collection { * Delega en los ítems individuales (DDD correcto) pero evita múltiples recorridos. */ public calculateAllAmounts() { - let subtotalAmount = ItemAmount.zero(this._currencyCode.code); + let subtotalAmount = ItemAmount.zero(this.currencyCode.code); - let itemDiscountAmount = ItemAmount.zero(this._currencyCode.code); - let globalDiscountAmount = ItemAmount.zero(this._currencyCode.code); - let totalDiscountAmount = ItemAmount.zero(this._currencyCode.code); + let itemDiscountAmount = ItemAmount.zero(this.currencyCode.code); + let globalDiscountAmount = ItemAmount.zero(this.currencyCode.code); + let totalDiscountAmount = ItemAmount.zero(this.currencyCode.code); - let taxableAmount = ItemAmount.zero(this._currencyCode.code); + let taxableAmount = ItemAmount.zero(this.currencyCode.code); - let ivaAmount = ItemAmount.zero(this._currencyCode.code); - let recAmount = ItemAmount.zero(this._currencyCode.code); - let retentionAmount = ItemAmount.zero(this._currencyCode.code); + let ivaAmount = ItemAmount.zero(this.currencyCode.code); + let recAmount = ItemAmount.zero(this.currencyCode.code); + let retentionAmount = ItemAmount.zero(this.currencyCode.code); - let taxesAmount = ItemAmount.zero(this._currencyCode.code); - let totalAmount = ItemAmount.zero(this._currencyCode.code); + let taxesAmount = ItemAmount.zero(this.currencyCode.code); + let totalAmount = ItemAmount.zero(this.currencyCode.code); for (const item of this.getAll()) { const amounts = item.calculateAllAmounts(); @@ -213,13 +211,13 @@ export class CustomerInvoiceItems extends Collection { (t) => t.code, () => "" ); - const retentionCode = taxes.retention.match( + const retCode = taxes.retention.match( (t) => t.code, () => "" ); // Clave del grupo: combinación IVA|REC|RET - const key = `${ivaCode}|${recCode}|${retentionCode}`; + const key = `${ivaCode}|${recCode}|${retCode}`; const prev = map.get(key) ?? { taxes, @@ -241,5 +239,27 @@ export class CustomerInvoiceItems extends Collection { } return map; + + // Devuelve grupos dinámicos del VO existente (InvoiceTaxGroup) + // Nota: necesitas construir InvoiceTaxGroup aquí o en Proforma.getTaxes(). + // Para mantener el ejemplo acotado, se devuelve el map y Proforma lo transforma, + // pero puedes construir aquí directamente si prefieres. + /*return new Collection( + [...map.values()].map((entry) => { + const iva = entry.taxes.iva.unwrap(); + const rec = entry.taxes.rec; + const retention = entry.taxes.retention; + + // Convertimos a InvoiceAmount en el agregado (o aquí si tienes acceso) + // Aquí asumimos que InvoiceTaxGroup acepta ItemAmount/InvoiceAmount según tu implementación. + // Ajusta según tu VO real. + return InvoiceTaxGroup.create({ + iva, + rec, + retention, + taxableAmount: entry.taxable.toInvoiceAmount(), // si existe helper; si no, lo haces en Proforma + }).data; + }) + );*/ } } diff --git a/modules/customer-invoices/src/api/domain/proformas/proforma.aggregate.ts b/modules/customer-invoices/src/api/domain/proformas/proforma.aggregate.ts new file mode 100644 index 00000000..b7c7c1c0 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/proformas/proforma.aggregate.ts @@ -0,0 +1,309 @@ +import { + AggregateRoot, + type CurrencyCode, + DomainValidationError, + type LanguageCode, + type Percentage, + type TextValue, + type UniqueID, + type UtcDate, +} from "@repo/rdx-ddd"; +import { Collection, type Maybe, Result } from "@repo/rdx-utils"; + +import type { InvoicePaymentMethod } from "../common/entities"; +import { + InvoiceAmount, + type InvoiceNumber, + type InvoiceRecipient, + type InvoiceSerie, + type InvoiceStatus, + InvoiceTaxGroup, + type ItemAmount, +} from "../common/value-objects"; + +import { ProformaItems } from "./proforma-items"; + +export type ProformaProps = { + companyId: UniqueID; + + isProforma: boolean; + status: InvoiceStatus; + + series: Maybe; + invoiceNumber: InvoiceNumber; + + invoiceDate: UtcDate; + operationDate: Maybe; + + customerId: UniqueID; + recipient: Maybe; + + reference: Maybe; + description: Maybe; + notes: Maybe; + + languageCode: LanguageCode; + currencyCode: CurrencyCode; + + items: ProformaItems; + + paymentMethod: Maybe; + + discountPercentage: Percentage; +}; + +export type ProformaPatchProps = Partial> & { + items?: ProformaItems; +}; + +export class Proforma extends AggregateRoot { + private _items!: ProformaItems; + + protected constructor(props: ProformaProps, id?: UniqueID) { + super(props, id); + this._items = + props.items || + ProformaItems.create({ + languageCode: props.languageCode, + currencyCode: props.currencyCode, + globalDiscountPercentage: props.discountPercentage, + }); + } + + static create(props: ProformaProps, id?: UniqueID): Result { + const proforma = new Proforma(props, id); + + // Reglas de negocio / validaciones + + // 🔹 Disparar evento de dominio "CustomerInvoiceAuthenticatedEvent" + //const { customerInvoice } = props; + //user.addDomainEvent(new CustomerInvoiceAuthenticatedEvent(id, customerInvoice.toString())); + + return Result.ok(proforma); + } + + // Mutabilidad + + public update( + partialProforma: Partial> + ): Result { + const updatedProps = { + ...this.props, + ...partialProforma, + } as ProformaProps; + + return Proforma.create(updatedProps, this.id); + } + + public issue(): Result { + if (!this.props.status.canTransitionTo("ISSUED")) { + return Result.fail( + new DomainValidationError( + "INVALID_STATE", + "status", + "Proforma cannot be issued from current state" + ) + ); + } + + // Falta + //this.props.status = this.props.status.canTransitionTo("ISSUED"); + return Result.ok(); + } + + // Getters + + public get companyId(): UniqueID { + return this.props.companyId; + } + + public get customerId(): UniqueID { + return this.props.customerId; + } + + public get isProforma(): boolean { + return this.props.isProforma; + } + + public get status(): InvoiceStatus { + return this.props.status; + } + + canTransitionTo(nextStatus: string): boolean { + return this.props.status.canTransitionTo(nextStatus); + } + + public get series(): Maybe { + return this.props.series; + } + + public get invoiceNumber() { + return this.props.invoiceNumber; + } + + public get invoiceDate(): UtcDate { + return this.props.invoiceDate; + } + + public get operationDate(): Maybe { + return this.props.operationDate; + } + + public get reference(): Maybe { + return this.props.reference; + } + + public get description(): Maybe { + return this.props.description; + } + + public get notes(): Maybe { + return this.props.notes; + } + + public get recipient(): Maybe { + return this.props.recipient; + } + + public get paymentMethod(): Maybe { + return this.props.paymentMethod; + } + + public get languageCode(): LanguageCode { + return this.props.languageCode; + } + + public get currencyCode(): CurrencyCode { + return this.props.currencyCode; + } + + public get discountPercentage(): Percentage { + return this.props.discountPercentage; + } + + // Method to get the complete list of line items + public get items(): ProformaItems { + return this._items; + } + + public get hasRecipient() { + return this.recipient.isSome(); + } + + public get hasPaymentMethod() { + return this.paymentMethod.isSome(); + } + + // Helpers + + /** + * @summary Convierte un ItemAmount a InvoiceAmount (mantiene moneda y escala homogénea). + */ + private toInvoiceAmount(itemAmount: ItemAmount): InvoiceAmount { + return InvoiceAmount.create({ + value: itemAmount.convertScale(InvoiceAmount.DEFAULT_SCALE).value, + currency_code: this.currencyCode.code, + }).data; + } + + // Cálculos + + /** + * @summary Calcula todos los totales de factura a partir de los totales de las líneas. + * La cabecera NO recalcula lógica de porcentaje — toda la lógica está en Item/Items. + */ + public calculateAllAmounts() { + const itemsTotals = this.items.calculateAllAmounts(); + + const subtotalAmount = this.toInvoiceAmount(itemsTotals.subtotalAmount); + + const itemDiscountAmount = this.toInvoiceAmount(itemsTotals.itemDiscountAmount); + const globalDiscountAmount = this.toInvoiceAmount(itemsTotals.globalDiscountAmount); + const totalDiscountAmount = this.toInvoiceAmount(itemsTotals.totalDiscountAmount); + + const taxableAmount = this.toInvoiceAmount(itemsTotals.taxableAmount); + const taxesAmount = this.toInvoiceAmount(itemsTotals.taxesAmount); + const totalAmount = this.toInvoiceAmount(itemsTotals.totalAmount); + + const taxGroups = this.getTaxes(); + + return { + subtotalAmount, + itemDiscountAmount, + globalDiscountAmount, + totalDiscountAmount, + taxableAmount, + taxesAmount, + totalAmount, + taxGroups, + } as const; + } + + // Métodos públicos + + public getProps(): ProformaProps { + return this.props; + } + + public getSubtotalAmount(): InvoiceAmount { + return this.calculateAllAmounts().subtotalAmount; + } + + public getItemDiscountAmount(): InvoiceAmount { + return this.calculateAllAmounts().itemDiscountAmount; + } + + public getGlobalDiscountAmount(): InvoiceAmount { + return this.calculateAllAmounts().globalDiscountAmount; + } + + public getTotalDiscountAmount(): InvoiceAmount { + return this.calculateAllAmounts().totalDiscountAmount; + } + + public getTaxableAmount(): InvoiceAmount { + return this.calculateAllAmounts().taxableAmount; + } + + public getTaxesAmount(): InvoiceAmount { + return this.calculateAllAmounts().taxesAmount; + } + + public getTotalAmount(): InvoiceAmount { + return this.calculateAllAmounts().totalAmount; + } + + /** + * @summary Agrupa impuestos a nivel factura usando el trío (iva|rec|ret), + * construyendo InvoiceTaxGroup desde los datos de los ítems. + */ + /** + * @summary Agrupa impuestos a nivel factura usando el trío (iva|rec|ret), + * construyendo InvoiceTaxGroup desde los datos de los ítems. + */ + public getTaxes(): Collection { + const map = this.items.groupTaxesByCode(); + const groups: InvoiceTaxGroup[] = []; + + for (const [, entry] of map.entries()) { + const { taxes, taxable } = entry; + + const iva = taxes.iva.unwrap(); // IVA siempre obligatorio + const rec = taxes.rec; // Maybe + const retention = taxes.retention; // Maybe + + const taxableAmount = this.toInvoiceAmount(taxable); + + const group = InvoiceTaxGroup.create({ + iva, + rec, + retention, + taxableAmount, + }).data; + + groups.push(group); + } + + return new Collection(groups); + } +} diff --git a/modules/customer-invoices/src/api/domain/repositories/customer-invoice-repository.interface.ts b/modules/customer-invoices/src/api/domain/repositories/customer-invoice-repository.interface.ts deleted file mode 100644 index 3ab451a2..00000000 --- a/modules/customer-invoices/src/api/domain/repositories/customer-invoice-repository.interface.ts +++ /dev/null @@ -1,138 +0,0 @@ -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 { CustomerInvoiceListDTO } from "../../infrastructure"; -import type { CustomerInvoice } from "../aggregates"; -import type { CustomerInvoiceStatus } from "../value-objects"; - -/** - * Interfaz del repositorio para el agregado `CustomerInvoice`. - * El escopado multitenant está representado por `companyId`. - */ -export interface ICustomerInvoiceRepository { - /** - * - * Crea una nueva factura. - * - * @param invoice - El agregado a guardar. - * @param transaction - Transacción activa para la operación. - * @returns Result - */ - create(invoice: CustomerInvoice, transaction?: unknown): Promise>; - - /** - * Actualiza una factura existente. - * - * @param invoice - El agregado a actualizar. - * @param transaction - Transacción activa para la operación. - * @returns Result - */ - update(invoice: CustomerInvoice, transaction?: unknown): Promise>; - - /** - * Comprueba si existe una factura con un `id` dentro de una `company`. - */ - existsByIdInCompany( - companyId: UniqueID, - id: UniqueID, - transaction: unknown, - options: unknown - ): Promise>; - - /** - * Recupera una proforma por su ID y companyId. - * Devuelve un `NotFoundError` si no se encuentra. - */ - getProformaByIdInCompany( - companyId: UniqueID, - id: UniqueID, - transaction: unknown, - options: unknown - ): Promise>; - - /** - * Recupera una factura por su ID y companyId. - * Devuelve un `NotFoundError` si no se encuentra. - */ - getIssuedInvoiceByIdInCompany( - companyId: UniqueID, - id: UniqueID, - transaction: unknown, - options: unknown - ): Promise>; - - /** - * - * Consulta proformas dentro de una empresa usando un - * objeto Criteria (filtros, orden, paginación). - * El resultado está encapsulado en un objeto `Collection`. - * - * @param companyId - ID de la empresa. - * @param criteria - Criterios de búsqueda. - * @param transaction - Transacción activa para la operación. - * @param options - Opciones adicionales para la consulta (Sequelize FindOptions) - * @returns Result, Error> - * - * @see Criteria - */ - findProformasByCriteriaInCompany( - companyId: UniqueID, - criteria: Criteria, - transaction: unknown, - options: unknown - ): Promise, Error>>; - - /** - * - * Consulta facturas dentro de una empresa usando un - * objeto Criteria (filtros, orden, paginación). - * El resultado está encapsulado en un objeto `Collection`. - * - * @param companyId - ID de la empresa. - * @param criteria - Criterios de búsqueda. - * @param transaction - Transacción activa para la operación. - * @param options - Opciones adicionales para la consulta (Sequelize FindOptions) - * @returns Result, Error> - * - * @see Criteria - */ - findIssuedInvoicesByCriteriaInCompany( - companyId: UniqueID, - criteria: Criteria, - transaction: unknown, - options: unknown - ): Promise, Error>>; - - /** - * - * Elimina o marca como eliminada una proforma dentro de una empresa. - * - * @param companyId - ID de la empresa. - * @param id - UUID de la proforma a eliminar. - * @param transaction - Transacción activa para la operación. - * @returns Result - */ - deleteProformaByIdInCompany( - companyId: UniqueID, - id: UniqueID, - transaction: unknown - ): Promise>; - - /** - * - * Actualiza el "status" de una proforma - * - * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. - * @param id - UUID de la factura a eliminar. - * @param newStatus - nuevo estado - * @param transaction - Transacción activa para la operación. - * @returns Result - */ - updateProformaStatusByIdInCompany( - companyId: UniqueID, - id: UniqueID, - newStatus: CustomerInvoiceStatus, - transaction: unknown - ): Promise>; -} diff --git a/modules/customer-invoices/src/api/domain/repositories/index.ts b/modules/customer-invoices/src/api/domain/repositories/index.ts deleted file mode 100644 index d149dde6..00000000 --- a/modules/customer-invoices/src/api/domain/repositories/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./customer-invoice-repository.interface"; diff --git a/modules/customer-invoices/src/api/domain/services/index.ts b/modules/customer-invoices/src/api/domain/services/index.ts index 0f7cf9cf..aa1f213d 100644 --- a/modules/customer-invoices/src/api/domain/services/index.ts +++ b/modules/customer-invoices/src/api/domain/services/index.ts @@ -1,3 +1,2 @@ -export * from "./customer-invoice-number-generator.interface"; export * from "./issue-customer-invoice-domain-service"; export * from "./proforma-customer-invoice-domain-service"; 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 525f259e..909729b9 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 @@ -2,17 +2,13 @@ import { UniqueID, type UtcDate } from "@repo/rdx-ddd"; import { Maybe, Result } from "@repo/rdx-utils"; import { CustomerInvoice } from "../aggregates"; -import { VerifactuRecord } from "../entities"; +import { VerifactuRecord } from "../common/entities"; +import { type InvoiceNumber, InvoiceStatus, VerifactuRecordEstado } from "../common/value-objects"; import { EntityIsNotProformaError, ProformaCannotBeConvertedToInvoiceError } from "../errors"; import { CustomerInvoiceIsProformaSpecification, ProformaCanTranstionToIssuedSpecification, } from "../specs"; -import { - type CustomerInvoiceNumber, - CustomerInvoiceStatus, - VerifactuRecordEstado, -} from "../value-objects"; /** * Servicio de dominio que encapsula la lógica de emisión de factura definitiva desde una proforma. @@ -32,7 +28,7 @@ export class IssueCustomerInvoiceDomainService { public async issueFromProforma( proforma: CustomerInvoice, params: { - issueNumber: CustomerInvoiceNumber; + issueNumber: InvoiceNumber; issueDate: UtcDate; } ): Promise> { @@ -71,7 +67,7 @@ export class IssueCustomerInvoiceDomainService { ...proformaProps, isProforma: false, proformaId: Maybe.some(proforma.id), - status: CustomerInvoiceStatus.createIssued(), + status: InvoiceStatus.createIssued(), invoiceNumber: issueNumber, invoiceDate: issueDate, description: proformaProps.description.isNone() ? Maybe.some(".") : proformaProps.description, diff --git a/modules/customer-invoices/src/api/domain/services/proforma-customer-invoice-domain-service.ts b/modules/customer-invoices/src/api/domain/services/proforma-customer-invoice-domain-service.ts index a92927ca..09bbfa4a 100644 --- a/modules/customer-invoices/src/api/domain/services/proforma-customer-invoice-domain-service.ts +++ b/modules/customer-invoices/src/api/domain/services/proforma-customer-invoice-domain-service.ts @@ -1,9 +1,9 @@ import { Result } from "@repo/rdx-utils"; import { CustomerInvoice } from "../aggregates"; +import { INVOICE_STATUS, InvoiceStatus } from "../common/value-objects"; import { EntityIsNotProformaError, InvalidProformaTransitionError } from "../errors"; import { CustomerInvoiceIsProformaSpecification } from "../specs"; -import { CustomerInvoiceStatus, INVOICE_STATUS } from "../value-objects"; /** * Servicio de dominio que encapsula la lógica de emisión de factura definitiva desde una proforma. @@ -35,7 +35,7 @@ export class ProformaCustomerInvoiceDomainService { return CustomerInvoice.create({ ...proforma.getProps(), - status: CustomerInvoiceStatus.create(nextStatus).data, + status: InvoiceStatus.create(nextStatus).data, }); } diff --git a/modules/customer-invoices/src/api/domain/specs/proforma-can-transtion-to-issued.specification.ts b/modules/customer-invoices/src/api/domain/specs/proforma-can-transtion-to-issued.specification.ts index c2708b31..1d3d73a2 100644 --- a/modules/customer-invoices/src/api/domain/specs/proforma-can-transtion-to-issued.specification.ts +++ b/modules/customer-invoices/src/api/domain/specs/proforma-can-transtion-to-issued.specification.ts @@ -1,7 +1,7 @@ import { CompositeSpecification } from "@repo/rdx-ddd"; import type { CustomerInvoice } from "../aggregates"; -import { INVOICE_STATUS } from "../value-objects"; +import { INVOICE_STATUS } from "../common/value-objects"; export class ProformaCanTranstionToIssuedSpecification extends CompositeSpecification { public async isSatisfiedBy(proforma: CustomerInvoice): Promise { diff --git a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-item-description.ts b/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-item-description.ts deleted file mode 100644 index 052726b3..00000000 --- a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-item-description.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { DomainValidationError, ValueObject } from "@repo/rdx-ddd"; -import { Maybe, Result } from "@repo/rdx-utils"; -import { z } from "zod/v4"; - -interface CustomerInvoiceItemDescriptionProps { - value: string; -} - -export class CustomerInvoiceItemDescription extends ValueObject { - private static readonly MAX_LENGTH = 2000; - private static readonly FIELD = "invoiceItemDescription"; - private static readonly ERROR_CODE = "INVALID_INVOICE_ITEM_DESCRIPTION"; - - protected static validate(value: string) { - const schema = z - .string() - .trim() - .max(CustomerInvoiceItemDescription.MAX_LENGTH, { - message: `Description must be at most ${CustomerInvoiceItemDescription.MAX_LENGTH} characters long`, - }); - return schema.safeParse(value); - } - - static create(value: string) { - const valueIsValid = CustomerInvoiceItemDescription.validate(value); - - if (!valueIsValid.success) { - const detail = valueIsValid.error.message; - return Result.fail( - new DomainValidationError( - CustomerInvoiceItemDescription.ERROR_CODE, - CustomerInvoiceItemDescription.FIELD, - detail - ) - ); - } - return Result.ok(new CustomerInvoiceItemDescription({ value })); - } - - static createNullable(value?: string): Result, Error> { - if (!value || value.trim() === "") { - return Result.ok(Maybe.none()); - } - - return CustomerInvoiceItemDescription.create(value).map((value) => Maybe.some(value)); - } - - getProps(): string { - return this.props.value; - } - - toString() { - return String(this.props.value); - } - - toPrimitive() { - return this.getProps(); - } -} diff --git a/modules/customer-invoices/src/api/domain/value-objects/index.ts b/modules/customer-invoices/src/api/domain/value-objects/index.ts deleted file mode 100644 index 8e2b70af..00000000 --- a/modules/customer-invoices/src/api/domain/value-objects/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -export * from "./customer-invoice-address-type"; -export * from "./customer-invoice-item-description"; -export * from "./customer-invoice-number"; -export * from "./customer-invoice-serie"; -export * from "./customer-invoice-status"; -export * from "./invoice-amount"; -export * from "./invoice-recipient"; -export * from "./invoice-tax-group"; -export * from "./item-amount"; -export * from "./item-discount"; -export * from "./item-quantity"; -export * from "./item-tax-group"; -export * from "./verifactu-status"; diff --git a/modules/customer-invoices/src/api/domain/value-objects/invoice-recipient/index.ts b/modules/customer-invoices/src/api/domain/value-objects/invoice-recipient/index.ts deleted file mode 100644 index 5ca076cd..00000000 --- a/modules/customer-invoices/src/api/domain/value-objects/invoice-recipient/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./invoice-recipient"; - - diff --git a/modules/customer-invoices/src/api/index.ts b/modules/customer-invoices/src/api/index.ts index eb766651..cf620655 100644 --- a/modules/customer-invoices/src/api/index.ts +++ b/modules/customer-invoices/src/api/index.ts @@ -3,7 +3,7 @@ import type { IModuleServer } from "@erp/core/api"; import { type IssuedInvoicesInternalDeps, buildIssuedInvoicesDependencies, - buildIssuedInvoicesServices, + buildProformaServices, issuedInvoicesRouter, models, } from "./infrastructure"; @@ -28,7 +28,7 @@ export const customerInvoicesAPIModule: IModuleServer = { //const proformasInternalDeps = buildProformasDependencies(params); // 2) Servicios públicos (Application Services) - const issuedInvoicesServices = buildIssuedInvoicesServices(issuedInvoicesInternalDeps); + const issuedInvoicesServices = buildProformaServices(issuedInvoicesInternalDeps); //const proformasServices = buildProformasServices(proformasInternalDeps); logger.info("🚀 CustomerInvoices module dependencies registered", { label: this.name }); diff --git a/modules/customer-invoices/src/api/infrastructure/common/di/index.ts b/modules/customer-invoices/src/api/infrastructure/common/di/index.ts new file mode 100644 index 00000000..dcede61b --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/common/di/index.ts @@ -0,0 +1 @@ +export * from "./repositories.di"; diff --git a/modules/customer-invoices/src/api/infrastructure/common/di/repositories.di.ts b/modules/customer-invoices/src/api/infrastructure/common/di/repositories.di.ts new file mode 100644 index 00000000..1ac1eef7 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/common/di/repositories.di.ts @@ -0,0 +1,13 @@ +import { SpainTaxCatalogProvider } from "@erp/core"; +import type { Sequelize } from "sequelize"; + +export const buildIssuedInvoiceRepository = (database: Sequelize) => { + const taxCatalog = SpainTaxCatalogProvider(); + + const domainMapper = new SequelizeIssuedInvoiceDomainMapper({ + taxCatalog, + }); + const listMapper = new SequelizeIssuedInvoiceListMapper(); + + return new IssuedInvoiceRepository(domainMapper, listMapper, database); +}; diff --git a/modules/customer-invoices/src/api/infrastructure/common/index.ts b/modules/customer-invoices/src/api/infrastructure/common/index.ts new file mode 100644 index 00000000..96e04610 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/common/index.ts @@ -0,0 +1,2 @@ +export * from "./di"; +export * from "./persistence"; diff --git a/modules/customer-invoices/src/api/infrastructure/persistence/index.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/index.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/persistence/index.ts rename to modules/customer-invoices/src/api/infrastructure/common/persistence/index.ts diff --git a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/index.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/index.ts similarity index 79% rename from modules/customer-invoices/src/api/infrastructure/persistence/sequelize/index.ts rename to modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/index.ts index dbc54f46..050ae799 100644 --- a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/index.ts @@ -3,10 +3,7 @@ import customerInvoiceItemModelInit from "./models/customer-invoice-item.model"; import customerInvoiceTaxesModelInit from "./models/customer-invoice-tax.model"; import verifactuRecordModelInit from "./models/verifactu-record.model"; -export * from "./mappers"; export * from "./models"; -export * from "./repositories/customer-invoice.repository"; -export * from "./sequelize-invoice-number-generator"; // Array de inicializadores para que registerModels() lo use export const models = [ diff --git a/modules/customer-invoices/src/api/infrastructure/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 similarity index 93% rename from modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/customer-invoice-item.mapper.ts rename to modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/customer-invoice-item.mapper.ts index 4ce85708..bb9ee051 100644 --- a/modules/customer-invoices/src/api/infrastructure/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 @@ -16,16 +16,16 @@ import { import { Result } from "@repo/rdx-utils"; import { - type CustomerInvoice, - CustomerInvoiceItem, - CustomerInvoiceItemDescription, - type CustomerInvoiceItemProps, - type CustomerInvoiceProps, + type IProformaProps, + IssuedInvoiceItem, + type IssuedInvoiceItemProps, ItemAmount, + ItemDescription, ItemDiscount, ItemQuantity, ItemTaxGroup, -} from "../../../../../domain"; + type Proforma, +} from "../../../../../../domain"; import type { CustomerInvoiceItemCreationAttributes, CustomerInvoiceItemModel, @@ -35,14 +35,14 @@ export interface ICustomerInvoiceItemDomainMapper extends ISequelizeDomainMapper< CustomerInvoiceItemModel, CustomerInvoiceItemCreationAttributes, - CustomerInvoiceItem + IssuedInvoiceItem > {} export class CustomerInvoiceItemDomainMapper extends SequelizeDomainMapper< CustomerInvoiceItemModel, CustomerInvoiceItemCreationAttributes, - CustomerInvoiceItem + IssuedInvoiceItem > implements ICustomerInvoiceItemDomainMapper { @@ -64,11 +64,11 @@ export class CustomerInvoiceItemDomainMapper private mapAttributesToDomain( source: 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( @@ -78,7 +78,7 @@ export class CustomerInvoiceItemDomainMapper ); const description = extractOrPushError( - maybeFromNullableVO(source.description, (v) => CustomerInvoiceItemDescription.create(v)), + maybeFromNullableVO(source.description, (v) => ItemDescription.create(v)), `items[${index}].description`, errors ); @@ -155,11 +155,11 @@ export class CustomerInvoiceItemDomainMapper public mapToDomain( source: CustomerInvoiceItemModel, params?: MapperParamsType - ): Result { + ): Result { const { errors, index } = params as { index: number; errors: ValidationErrorDetail[]; - attributes: Partial; + attributes: Partial; }; // 1) Valores escalares (atributos generales) @@ -173,7 +173,7 @@ export class CustomerInvoiceItemDomainMapper } // 2) Construcción del elemento de dominio - const createResult = CustomerInvoiceItem.create( + const createResult = IssuedInvoiceItem.create( { languageCode: attributes.languageCode!, currencyCode: attributes.currencyCode!, @@ -199,12 +199,12 @@ export class CustomerInvoiceItemDomainMapper } public mapToPersistence( - source: CustomerInvoiceItem, + source: IssuedInvoiceItem, params?: MapperParamsType ): Result { const { errors, index, parent } = params as { index: number; - parent: CustomerInvoice; + parent: Proforma; errors: ValidationErrorDetail[]; }; diff --git a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/customer-invoice-taxes.mapper.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/customer-invoice-taxes.mapper.ts similarity index 97% rename from modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/customer-invoice-taxes.mapper.ts rename to modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/customer-invoice-taxes.mapper.ts index 6b918bee..521f827c 100644 --- a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/customer-invoice-taxes.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/customer-invoice-taxes.mapper.ts @@ -3,7 +3,7 @@ import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api"; import { UniqueID, type ValidationErrorDetail, toNullable } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import type { CustomerInvoice, InvoiceTaxGroup } from "../../../../../domain"; +import type { InvoiceTaxGroup, Proforma } from "../../../../../../domain"; import type { CustomerInvoiceTaxCreationAttributes, CustomerInvoiceTaxModel, @@ -93,7 +93,7 @@ export class CustomerInvoiceTaxesDomainMapper extends SequelizeDomainMapper< params?: MapperParamsType ): Result { const { errors, parent } = params as { - parent: CustomerInvoice; + parent: Proforma; errors: ValidationErrorDetail[]; }; diff --git a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/customer-invoice.mapper.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/customer-invoice.mapper.ts similarity index 93% rename from modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/customer-invoice.mapper.ts rename to modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/customer-invoice.mapper.ts index 5fe8fa49..4ac9bc9b 100644 --- a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/customer-invoice.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/customer-invoice.mapper.ts @@ -19,14 +19,14 @@ import { import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils"; import { - CustomerInvoice, CustomerInvoiceItems, - CustomerInvoiceNumber, - type CustomerInvoiceProps, - CustomerInvoiceSerie, - CustomerInvoiceStatus, + type IProformaProps, + InvoiceNumber, InvoicePaymentMethod, -} from "../../../../../domain"; + InvoiceSerie, + InvoiceStatus, + Proforma, +} from "../../../../../../domain"; import type { CustomerInvoiceCreationAttributes, CustomerInvoiceModel, @@ -41,15 +41,11 @@ export interface ICustomerInvoiceDomainMapper extends ISequelizeDomainMapper< CustomerInvoiceModel, CustomerInvoiceCreationAttributes, - CustomerInvoice + Proforma > {} export class CustomerInvoiceDomainMapper - extends SequelizeDomainMapper< - CustomerInvoiceModel, - CustomerInvoiceCreationAttributes, - CustomerInvoice - > + extends SequelizeDomainMapper implements ICustomerInvoiceDomainMapper { private _itemsMapper: CustomerInvoiceItemDomainMapper; @@ -88,20 +84,16 @@ export class CustomerInvoiceDomainMapper errors ); - const status = extractOrPushError( - CustomerInvoiceStatus.create(source.status), - "status", - errors - ); + const status = extractOrPushError(InvoiceStatus.create(source.status), "status", errors); const series = extractOrPushError( - maybeFromNullableVO(source.series, (v) => CustomerInvoiceSerie.create(v)), + maybeFromNullableVO(source.series, (v) => InvoiceSerie.create(v)), "series", errors ); const invoiceNumber = extractOrPushError( - CustomerInvoiceNumber.create(source.invoice_number), + InvoiceNumber.create(source.invoice_number), "invoice_number", errors ); @@ -209,7 +201,7 @@ export class CustomerInvoiceDomainMapper public mapToDomain( source: CustomerInvoiceModel, params?: MapperParamsType - ): Result { + ): Result { try { const errors: ValidationErrorDetail[] = []; @@ -257,7 +249,7 @@ export class CustomerInvoiceDomainMapper items: itemsResults.data.getAll(), }); - const invoiceProps: CustomerInvoiceProps = { + const invoiceProps: IProformaProps = { companyId: attributes.companyId!, isProforma: attributes.isProforma, @@ -286,7 +278,7 @@ export class CustomerInvoiceDomainMapper verifactu: verifactuResult.data, }; - const createResult = CustomerInvoice.create(invoiceProps, attributes.invoiceId); + const createResult = Proforma.create(invoiceProps, attributes.invoiceId); if (createResult.isFailure) { return Result.fail( @@ -303,7 +295,7 @@ export class CustomerInvoiceDomainMapper } public mapToPersistence( - source: CustomerInvoice, + source: Proforma, params?: MapperParamsType ): Result { const errors: ValidationErrorDetail[] = []; diff --git a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/index.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/index.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/index.ts rename to modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/index.ts diff --git a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/invoice-recipient.mapper.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/invoice-recipient.mapper.ts similarity index 96% rename from modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/invoice-recipient.mapper.ts rename to modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/invoice-recipient.mapper.ts index 444296c5..db80a9d3 100644 --- a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/invoice-recipient.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/invoice-recipient.mapper.ts @@ -15,11 +15,7 @@ import { } from "@repo/rdx-ddd"; import { Maybe, Result } from "@repo/rdx-utils"; -import { - type CustomerInvoice, - type CustomerInvoiceProps, - InvoiceRecipient, -} from "../../../../../domain"; +import { type IProformaProps, InvoiceRecipient, type Proforma } from "../../../../../../domain"; import type { CustomerInvoiceModel } from "../../../../sequelize"; export class InvoiceRecipientDomainMapper { @@ -34,7 +30,7 @@ export class InvoiceRecipientDomainMapper { const { errors, attributes } = params as { errors: ValidationErrorDetail[]; - attributes: Partial; + attributes: Partial; }; const { isProforma } = attributes; @@ -131,7 +127,7 @@ export class InvoiceRecipientDomainMapper { */ mapToPersistence(source: Maybe, params?: MapperParamsType) { const { errors, parent } = params as { - parent: CustomerInvoice; + parent: Proforma; errors: ValidationErrorDetail[]; }; diff --git a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/invoice-verifactu.mapper.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/invoice-verifactu.mapper.ts similarity index 95% rename from modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/invoice-verifactu.mapper.ts rename to modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/invoice-verifactu.mapper.ts index 6c692486..b11268bd 100644 --- a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/domain/invoice-verifactu.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/invoice-verifactu.mapper.ts @@ -12,11 +12,11 @@ import { import { Maybe, Result } from "@repo/rdx-utils"; import { - type CustomerInvoice, - type CustomerInvoiceProps, + type IProformaProps, + type Proforma, VerifactuRecord, VerifactuRecordEstado, -} from "../../../../../domain"; +} from "../../../../../../domain"; import type { VerifactuRecordCreationAttributes, VerifactuRecordModel, @@ -43,7 +43,7 @@ export class CustomerInvoiceVerifactuDomainMapper ): Result, Error> { const { errors, attributes } = params as { errors: ValidationErrorDetail[]; - attributes: Partial; + attributes: Partial; }; if (!source) { @@ -114,7 +114,7 @@ export class CustomerInvoiceVerifactuDomainMapper params?: MapperParamsType ): Result { const { errors, parent } = params as { - parent: CustomerInvoice; + parent: Proforma; errors: ValidationErrorDetail[]; }; diff --git a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/index.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/index.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/index.ts rename to modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/index.ts diff --git a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/list/customer-invoice.list.mapper.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/list/customer-invoice.list.mapper.ts similarity index 94% rename from modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/list/customer-invoice.list.mapper.ts rename to modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/list/customer-invoice.list.mapper.ts index 28104398..c38b4b8f 100644 --- a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/list/customer-invoice.list.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/list/customer-invoice.list.mapper.ts @@ -17,13 +17,13 @@ import { import { Maybe, Result } from "@repo/rdx-utils"; import { - CustomerInvoiceNumber, - CustomerInvoiceSerie, - CustomerInvoiceStatus, InvoiceAmount, + InvoiceNumber, type InvoiceRecipient, + InvoiceSerie, + InvoiceStatus, type VerifactuRecord, -} from "../../../../../domain"; +} from "../../../../../../domain"; import type { CustomerInvoiceModel } from "../../../../sequelize"; import { InvoiceRecipientListMapper } from "./invoice-recipient.list.mapper"; @@ -34,9 +34,9 @@ export type CustomerInvoiceListDTO = { companyId: UniqueID; isProforma: boolean; - invoiceNumber: CustomerInvoiceNumber; - status: CustomerInvoiceStatus; - series: Maybe; + invoiceNumber: InvoiceNumber; + status: InvoiceStatus; + series: Maybe; invoiceDate: UtcDate; operationDate: Maybe; @@ -164,16 +164,16 @@ export class CustomerInvoiceListMapper const isProforma = Boolean(raw.is_proforma); - const status = extractOrPushError(CustomerInvoiceStatus.create(raw.status), "status", errors); + const status = extractOrPushError(InvoiceStatus.create(raw.status), "status", errors); const series = extractOrPushError( - maybeFromNullableVO(raw.series, (value) => CustomerInvoiceSerie.create(value)), + maybeFromNullableVO(raw.series, (value) => InvoiceSerie.create(value)), "serie", errors ); const invoiceNumber = extractOrPushError( - CustomerInvoiceNumber.create(raw.invoice_number), + InvoiceNumber.create(raw.invoice_number), "invoice_number", errors ); diff --git a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/list/index.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/list/index.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/list/index.ts rename to modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/list/index.ts diff --git a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/list/invoice-recipient.list.mapper.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/list/invoice-recipient.list.mapper.ts similarity index 98% rename from modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/list/invoice-recipient.list.mapper.ts rename to modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/list/invoice-recipient.list.mapper.ts index f47025fa..1fef5207 100644 --- a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/list/invoice-recipient.list.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/list/invoice-recipient.list.mapper.ts @@ -17,7 +17,7 @@ import { } from "@repo/rdx-ddd"; import type { Result } from "@repo/rdx-utils"; -import { InvoiceRecipient } from "../../../../../domain"; +import { InvoiceRecipient } from "../../../../../../domain"; import type { CustomerInvoiceModel } from "../../../../sequelize"; import type { CustomerInvoiceListDTO } from "./customer-invoice.list.mapper"; diff --git a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/list/verifactu-record.list.mapper.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/list/verifactu-record.list.mapper.ts similarity index 99% rename from modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/list/verifactu-record.list.mapper.ts rename to modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/list/verifactu-record.list.mapper.ts index fc98ec56..9a691a21 100644 --- a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/mappers/list/verifactu-record.list.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/list/verifactu-record.list.mapper.ts @@ -13,7 +13,7 @@ import { } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import { VerifactuRecord, VerifactuRecordEstado } from "../../../../../domain"; +import { VerifactuRecord, VerifactuRecordEstado } from "../../../../../../domain"; import type { VerifactuRecordModel } from "../../../../sequelize"; export interface IVerifactuRecordListMapper diff --git a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/models/customer-invoice-criteria-whitelist.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice-criteria-whitelist.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/persistence/sequelize/models/customer-invoice-criteria-whitelist.ts rename to modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice-criteria-whitelist.ts diff --git a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/models/customer-invoice-item.model.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice-item.model.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/persistence/sequelize/models/customer-invoice-item.model.ts rename to modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice-item.model.ts diff --git a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/models/customer-invoice-tax.model.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice-tax.model.ts similarity index 98% rename from modules/customer-invoices/src/api/infrastructure/persistence/sequelize/models/customer-invoice-tax.model.ts rename to modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice-tax.model.ts index 9d4182e5..e0fa9d92 100644 --- a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/models/customer-invoice-tax.model.ts +++ b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice-tax.model.ts @@ -8,7 +8,7 @@ import { type Sequelize, } from "sequelize"; -import type { CustomerInvoice } from "../../../../domain"; +import type { Proforma } from "../../../../../domain"; export type CustomerInvoiceTaxCreationAttributes = InferCreationAttributes< CustomerInvoiceTaxModel, @@ -64,7 +64,7 @@ export class CustomerInvoiceTaxModel extends Model< declare taxes_amount_scale: number; // Relaciones - declare invoice: NonAttribute; + declare invoice: NonAttribute; static associate(database: Sequelize) { const models = database.models; diff --git a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/models/customer-invoice.model.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice.model.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/persistence/sequelize/models/customer-invoice.model.ts rename to modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice.model.ts diff --git a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/models/index.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/index.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/persistence/sequelize/models/index.ts rename to modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/index.ts diff --git a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/models/verifactu-record.model.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/verifactu-record.model.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/persistence/sequelize/models/verifactu-record.model.ts rename to modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/verifactu-record.model.ts diff --git a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/sequelize-invoice-number-generator.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/sequelize-invoice-number-generator.ts similarity index 85% rename from modules/customer-invoices/src/api/infrastructure/persistence/sequelize/sequelize-invoice-number-generator.ts rename to modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/sequelize-invoice-number-generator.ts index 69ccf133..21806c12 100644 --- a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/sequelize-invoice-number-generator.ts +++ b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/sequelize-invoice-number-generator.ts @@ -2,11 +2,7 @@ import type { UniqueID } from "@repo/rdx-ddd"; import { type Maybe, Result } from "@repo/rdx-utils"; import { type Transaction, type WhereOptions, literal } from "sequelize"; -import { - CustomerInvoiceNumber, - type CustomerInvoiceSerie, - type ICustomerInvoiceNumberGenerator, -} from "../../../domain"; +import { InvoiceNumber, type InvoiceSerie } from "../../../../domain"; import { CustomerInvoiceModel } from "./models"; @@ -16,9 +12,9 @@ import { CustomerInvoiceModel } from "./models"; export class SequelizeInvoiceNumberGenerator implements ICustomerInvoiceNumberGenerator { public async nextForCompany( companyId: UniqueID, - series: Maybe, + series: Maybe, transaction: Transaction - ): Promise> { + ): Promise> { const where: WhereOptions = { company_id: companyId.toString(), is_proforma: false, @@ -53,7 +49,7 @@ export class SequelizeInvoiceNumberGenerator implements ICustomerInvoiceNumberGe nextValue = String(next).padStart(3, "0"); } - const numberResult = CustomerInvoiceNumber.create(nextValue); + const numberResult = InvoiceNumber.create(nextValue); if (numberResult.isFailure) { return Result.fail(numberResult.error); } diff --git a/modules/customer-invoices/src/api/infrastructure/di/index.ts b/modules/customer-invoices/src/api/infrastructure/di/index.ts deleted file mode 100644 index 99c2c9c5..00000000 --- a/modules/customer-invoices/src/api/infrastructure/di/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./issued-invoices.di"; -export * from "./issued-invoices-services"; diff --git a/modules/customer-invoices/src/api/infrastructure/di/proformas.di.ts b/modules/customer-invoices/src/api/infrastructure/di/proformas.di.ts deleted file mode 100644 index bdd56c93..00000000 --- a/modules/customer-invoices/src/api/infrastructure/di/proformas.di.ts +++ /dev/null @@ -1,158 +0,0 @@ -// modules/invoice/infrastructure/invoice-dependencies.factory.ts - -import { type JsonTaxCatalogProvider, SpainTaxCatalogProvider } from "@erp/core"; -import { - HandlebarsTemplateResolver, - type IMapperRegistry, - type IPresenterRegistry, - InMemoryMapperRegistry, - InMemoryPresenterRegistry, - type ModuleParams, - SequelizeTransactionManager, -} from "@erp/core/api"; - -import { - ChangeStatusProformaUseCase, - CreateProformaUseCase, - CustomerInvoiceApplicationService, - DeleteProformaUseCase, - GetProformaUseCase, - IssueProformaUseCase, - ListProformasUseCase, - ProformaFullPresenter, - ProformaListPresenter, - ReportProformaUseCase, - UpdateProformaUseCase, -} from "../application"; -import { ProformaItemsFullPresenter } from "../application/snapshot-builders/domain/proformas/proforma-items.full.presenter"; -import { ProformaRecipientFullPresenter } from "../application/snapshot-builders/domain/proformas/proforma-recipient.full.presenter"; -import { - IssuedInvoiceTaxesReportPresenter, - ProformaItemsReportPresenter, - ProformaReportPresenter, -} from "../application/snapshot-builders/reports"; - -import { SequelizeInvoiceNumberGenerator } from "./persistence/sequelize"; -import { - CustomerInvoiceDomainMapper, - CustomerInvoiceListMapper, -} from "./persistence/sequelize/mappers"; -import { CustomerInvoiceRepository } from "./sequelize"; - -export type ProformasDeps = { - transactionManager: SequelizeTransactionManager; - mapperRegistry: IMapperRegistry; - presenterRegistry: IPresenterRegistry; - repo: CustomerInvoiceRepository; - appService: CustomerInvoiceApplicationService; - catalogs: { - taxes: JsonTaxCatalogProvider; - }; - useCases: { - list_proformas: () => ListProformasUseCase; - get_proforma: () => GetProformaUseCase; - create_proforma: () => CreateProformaUseCase; - update_proforma: () => UpdateProformaUseCase; - delete_proforma: () => DeleteProformaUseCase; - report_proforma: () => ReportProformaUseCase; - issue_proforma: () => IssueProformaUseCase; - changeStatus_proforma: () => ChangeStatusProformaUseCase; - }; -}; - -export function buildProformasDependencies(params: ModuleParams): ProformasDeps { - const { database, env } = params; - const templateRootPath = env.TEMPLATES_PATH; - - /** Dominio */ - const catalogs = { taxes: SpainTaxCatalogProvider() }; - - /** Infraestructura */ - const templateResolver = new HandlebarsTemplateResolver(templateRootPath); - const transactionManager = new SequelizeTransactionManager(database); - - const mapperRegistry = new InMemoryMapperRegistry(); - mapperRegistry - .registerDomainMapper( - { resource: "customer-invoice" }, - new CustomerInvoiceDomainMapper({ taxCatalog: catalogs.taxes }) - ) - .registerQueryMappers([ - { - key: { resource: "customer-invoice", query: "LIST" }, - mapper: new CustomerInvoiceListMapper(), - }, - ]); - - // Repository & Services - const repository = new CustomerInvoiceRepository({ mapperRegistry, database }); - const numberGenerator = new SequelizeInvoiceNumberGenerator(); - - /** Aplicación */ - const appService = new CustomerInvoiceApplicationService(repository, numberGenerator); - - // Presenter Registry - const presenterRegistry = new InMemoryPresenterRegistry(); - presenterRegistry.registerPresenters([ - // FULL - { - key: { resource: "proforma-items", projection: "FULL" }, - presenter: new ProformaItemsFullPresenter(presenterRegistry), - }, - { - key: { resource: "proforma-recipient", projection: "FULL" }, - presenter: new ProformaRecipientFullPresenter(presenterRegistry), - }, - { - key: { resource: "proforma", projection: "FULL" }, - presenter: new ProformaFullPresenter(presenterRegistry), - }, - - // LIST - { - key: { resource: "proforma", projection: "LIST" }, - presenter: new ProformaListPresenter(presenterRegistry), - }, - - // REPORT - { - key: { resource: "proforma", projection: "REPORT" }, - presenter: new ProformaReportPresenter(presenterRegistry), - }, - { - key: { resource: "proforma-taxes", projection: "REPORT" }, - presenter: new IssuedInvoiceTaxesReportPresenter(presenterRegistry), - }, - { - key: { resource: "proforma-items", projection: "REPORT" }, - presenter: new ProformaItemsReportPresenter(presenterRegistry), - }, - ]); - - const useCases: ProformasDeps["useCases"] = { - // Proformas - list_proformas: () => - new ListProformasUseCase(appService, transactionManager, presenterRegistry), - get_proforma: () => new GetProformaUseCase(appService, transactionManager, presenterRegistry), - create_proforma: () => - new CreateProformaUseCase(appService, transactionManager, presenterRegistry, catalogs.taxes), - update_proforma: () => - new UpdateProformaUseCase(appService, transactionManager, presenterRegistry), - delete_proforma: () => new DeleteProformaUseCase(appService, transactionManager), - report_proforma: () => - new ReportProformaUseCase(appService, transactionManager, presenterRegistry), - issue_proforma: () => - new IssueProformaUseCase(appService, transactionManager, presenterRegistry), - changeStatus_proforma: () => new ChangeStatusProformaUseCase(appService, transactionManager), - }; - - return { - transactionManager, - repo: repository, - mapperRegistry, - presenterRegistry, - appService, - catalogs, - useCases, - }; -} diff --git a/modules/customer-invoices/src/api/infrastructure/express/issued-invoices/issued-invoices.routes.ts b/modules/customer-invoices/src/api/infrastructure/express/issued-invoices/issued-invoices.routes.ts index a5180521..c04659c0 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/issued-invoices/issued-invoices.routes.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/issued-invoices/issued-invoices.routes.ts @@ -8,7 +8,7 @@ import { ReportIssueInvoiceByIdParamsRequestSchema, ReportIssueInvoiceByIdQueryRequestSchema, } from "../../../../common/dto"; -import type { IssuedInvoicesInternalDeps } from "../../di"; +import type { IssuedInvoicesInternalDeps } from "../../issued-invoices/di"; import { GetIssuedInvoiceByIdController } from "./controllers"; import { ListIssuedInvoicesController } from "./controllers/list-issued-invoices.controller"; diff --git a/modules/customer-invoices/src/api/infrastructure/express/proformas/proformas.routes.ts b/modules/customer-invoices/src/api/infrastructure/express/proformas/proformas.routes.ts index bef50a59..1ee34aad 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/proformas/proformas.routes.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/proformas/proformas.routes.ts @@ -15,7 +15,7 @@ import { UpdateProformaByIdParamsRequestSchema, UpdateProformaByIdRequestSchema, } from "../../../../common"; -import { buildProformasDependencies } from "../../di/proformas.di"; +import type { IssuedInvoicesInternalDeps } from "../../issued-invoices/di"; import { ChangeStatusProformaController, @@ -28,11 +28,9 @@ import { UpdateProformaController, } from "./controllers"; -export const proformasRouter = (params: ModuleParams) => { +export const proformasRouter = (params: ModuleParams, deps: IssuedInvoicesInternalDeps) => { const { app, config } = params; - const deps = buildProformasDependencies(params); - const router: Router = Router({ mergeParams: true }); if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "production") { // 🔐 Autenticación + Tenancy para TODO el router @@ -57,7 +55,7 @@ export const proformasRouter = (params: ModuleParams) => { //checkTabContext, validateRequest(ListProformasRequestSchema, "params"), async (req: Request, res: Response, next: NextFunction) => { - const useCase = deps.useCases.list_proformas(); + const useCase = deps.useCases.listIssuedInvoices(); const controller = new ListProformasController(useCase /*, deps.presenters.list */); return controller.execute(req, res, next); } @@ -68,7 +66,7 @@ export const proformasRouter = (params: ModuleParams) => { //checkTabContext, validateRequest(GetProformaByIdRequestSchema, "params"), (req: Request, res: Response, next: NextFunction) => { - const useCase = deps.useCases.get_proforma(); + const useCase = deps.useCases.getIssuedInvoiceById(); const controller = new GetProformaController(useCase); return controller.execute(req, res, next); } @@ -117,7 +115,7 @@ export const proformasRouter = (params: ModuleParams) => { validateRequest(ReportProformaByIdParamsRequestSchema, "params"), validateRequest(ReportProformaByIdQueryRequestSchema, "query"), (req: Request, res: Response, next: NextFunction) => { - const useCase = deps.useCases.report_proforma(); + const useCase = deps.useCases.reportIssuedInvoice(); const controller = new ReportProformaController(useCase); return controller.execute(req, res, next); } diff --git a/modules/customer-invoices/src/api/infrastructure/index.ts b/modules/customer-invoices/src/api/infrastructure/index.ts index d5b95078..29526a1d 100644 --- a/modules/customer-invoices/src/api/infrastructure/index.ts +++ b/modules/customer-invoices/src/api/infrastructure/index.ts @@ -1,5 +1,4 @@ -export * from "./di"; -export * from "./documents"; -export * from "./express"; -export * from "./persistence"; +export * from "./common/persistence"; +export * from "./issued-invoices"; +export * from "./proformas"; export * from "./renderers"; 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 new file mode 100644 index 00000000..7a95685d --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/index.ts @@ -0,0 +1,2 @@ +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-documents.di.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoice-documents.di.ts new file mode 100644 index 00000000..67785841 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoice-documents.di.ts @@ -0,0 +1,48 @@ +import { type ModuleParams, buildCoreDocumentsDI } from "@erp/core/api"; + +import { + IssuedInvoiceDocumentPipelineFactory, + type IssuedInvoiceDocumentPipelineFactoryDeps, +} from "../documents"; + +export const buildIssuedInvoiceDocumentService = (params: ModuleParams) => { + const { documentRenderers, documentSigning, documentStorage } = buildCoreDocumentsDI(params); + + const pipelineDeps: IssuedInvoiceDocumentPipelineFactoryDeps = { + fastReportRenderer: documentRenderers.fastReportRenderer, + + // + signingContextResolver: documentSigning.signingContextResolver, + documentSigningService: documentSigning.signingService, + + // + documentStorage: documentStorage.storage, + + templateResolver: documentRenderers.fastReportTemplateResolver, + }; + + const documentGeneratorPipeline = IssuedInvoiceDocumentPipelineFactory.create(pipelineDeps); + + return documentGeneratorPipeline; +}; + +export const buildproformaDocumentService = (params: ModuleParams) => { + const { documentRenderers, documentSigning, documentStorage } = buildCoreDocumentsDI(params); + + const pipelineDeps: IssuedInvoiceDocumentPipelineFactoryDeps = { + fastReportRenderer: documentRenderers.fastReportRenderer, + + // + signingContextResolver: documentSigning.signingContextResolver, + documentSigningService: documentSigning.signingService, + + // + documentStorage: documentStorage.storage, + + templateResolver: documentRenderers.fastReportTemplateResolver, + }; + + const documentGeneratorPipeline = IssuedInvoiceDocumentPipelineFactory.create(pipelineDeps); + + return documentGeneratorPipeline; +}; diff --git a/modules/customer-invoices/src/api/infrastructure/di/issued-invoices-services.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoice-public-services.ts similarity index 95% rename from modules/customer-invoices/src/api/infrastructure/di/issued-invoices-services.ts rename to modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoice-public-services.ts index 945db7d7..8a78cd22 100644 --- a/modules/customer-invoices/src/api/infrastructure/di/issued-invoices-services.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoice-public-services.ts @@ -8,7 +8,7 @@ export type IssuedInvoicesServiceslDeps = { }; }; -export function buildIssuedInvoicesServices( +export function buildIssuedInvoiceServices( deps: IssuedInvoicesInternalDeps ): IssuedInvoicesServiceslDeps { return { diff --git a/modules/customer-invoices/src/api/infrastructure/di/repositories.di.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoice-repositories.di.ts similarity index 87% rename from modules/customer-invoices/src/api/infrastructure/di/repositories.di.ts rename to modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoice-repositories.di.ts index 51ecfa47..207d95cb 100644 --- a/modules/customer-invoices/src/api/infrastructure/di/repositories.di.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoice-repositories.di.ts @@ -6,9 +6,9 @@ import { CustomerInvoiceDomainMapper, CustomerInvoiceListMapper, CustomerInvoiceRepository, -} from "../persistence"; +} from "../../common/persistence"; -export const buildRepository = (database: Sequelize) => { +export const buildIssuedInvoiceRepository = (database: Sequelize) => { const mapperRegistry = new InMemoryMapperRegistry(); const taxCatalog = SpainTaxCatalogProvider(); diff --git a/modules/customer-invoices/src/api/infrastructure/di/issued-invoices.di.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoices.di.ts similarity index 86% rename from modules/customer-invoices/src/api/infrastructure/di/issued-invoices.di.ts rename to modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoices.di.ts index d785eeed..79c8be4c 100644 --- a/modules/customer-invoices/src/api/infrastructure/di/issued-invoices.di.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/di/issued-invoices.di.ts @@ -1,5 +1,3 @@ -// modules/invoice/infrastructure/invoice-dependencies.factory.ts - import { type ModuleParams, buildTransactionManager } from "@erp/core/api"; import { @@ -11,10 +9,10 @@ import { buildIssuedInvoiceSnapshotBuilders, buildListIssuedInvoicesUseCase, buildReportIssuedInvoiceUseCase, -} from "../../application/issued-invoices"; +} from "../../../application/issued-invoices"; -import { buildIssuedInvoiceDocumentService } from "./documents.di"; -import { buildRepository } from "./repositories.di"; +import { buildIssuedInvoiceDocumentService } from "./issued-invoice-documents.di"; +import { buildIssuedInvoiceRepository } from "./issued-invoice-repositories.di"; export type IssuedInvoicesInternalDeps = { useCases: { @@ -29,7 +27,7 @@ export function buildIssuedInvoicesDependencies(params: ModuleParams): IssuedInv // Infrastructure const transactionManager = buildTransactionManager(database); - const repository = buildRepository(database); + const repository = buildIssuedInvoiceRepository(database); // Application helpers const finder = buildIssuedInvoiceFinder(repository); diff --git a/modules/customer-invoices/src/api/infrastructure/documents/index.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/documents/index.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/documents/index.ts rename to modules/customer-invoices/src/api/infrastructure/issued-invoices/documents/index.ts diff --git a/modules/customer-invoices/src/api/infrastructure/documents/pipelines/index.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/documents/pipelines/index.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/documents/pipelines/index.ts rename to modules/customer-invoices/src/api/infrastructure/issued-invoices/documents/pipelines/index.ts diff --git a/modules/customer-invoices/src/api/infrastructure/documents/pipelines/issued-invoice-document-pipeline-factory.ts.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/documents/pipelines/issued-invoice-document-pipeline-factory.ts similarity index 98% rename from modules/customer-invoices/src/api/infrastructure/documents/pipelines/issued-invoice-document-pipeline-factory.ts.ts rename to modules/customer-invoices/src/api/infrastructure/issued-invoices/documents/pipelines/issued-invoice-document-pipeline-factory.ts index 956c28d8..a35e8a6d 100644 --- a/modules/customer-invoices/src/api/infrastructure/documents/pipelines/issued-invoice-document-pipeline-factory.ts.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/documents/pipelines/issued-invoice-document-pipeline-factory.ts @@ -15,7 +15,7 @@ import { IssuedInvoiceDocumentMetadataFactory, IssuedInvoiceDocumentPropertiesFactory, type IssuedInvoiceReportSnapshot, -} from "../../../application"; +} from "../../../../application"; import { DigitalSignaturePostProcessor } from "../post-processors"; import { IssuedInvoiceSignedDocumentCachePreProcessor } from "../pre-processors"; import { IssuedInvoiceDocumentRenderer } from "../renderers"; diff --git a/modules/customer-invoices/src/api/infrastructure/documents/post-processors/digital-signature-post-processor.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/documents/post-processors/digital-signature-post-processor.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/documents/post-processors/digital-signature-post-processor.ts rename to modules/customer-invoices/src/api/infrastructure/issued-invoices/documents/post-processors/digital-signature-post-processor.ts diff --git a/modules/customer-invoices/src/api/infrastructure/documents/post-processors/index.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/documents/post-processors/index.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/documents/post-processors/index.ts rename to modules/customer-invoices/src/api/infrastructure/issued-invoices/documents/post-processors/index.ts diff --git a/modules/customer-invoices/src/api/infrastructure/documents/pre-processors/index.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/documents/pre-processors/index.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/documents/pre-processors/index.ts rename to modules/customer-invoices/src/api/infrastructure/issued-invoices/documents/pre-processors/index.ts diff --git a/modules/customer-invoices/src/api/infrastructure/documents/pre-processors/issued-invoice-signed-document-cache-pre-processor.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/documents/pre-processors/issued-invoice-signed-document-cache-pre-processor.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/documents/pre-processors/issued-invoice-signed-document-cache-pre-processor.ts rename to modules/customer-invoices/src/api/infrastructure/issued-invoices/documents/pre-processors/issued-invoice-signed-document-cache-pre-processor.ts diff --git a/modules/customer-invoices/src/api/infrastructure/documents/renderers/fastreport/index.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/documents/renderers/fastreport/index.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/documents/renderers/fastreport/index.ts rename to modules/customer-invoices/src/api/infrastructure/issued-invoices/documents/renderers/fastreport/index.ts diff --git a/modules/customer-invoices/src/api/infrastructure/documents/renderers/fastreport/issued-invoice-document-renderer.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/documents/renderers/fastreport/issued-invoice-document-renderer.ts similarity index 95% rename from modules/customer-invoices/src/api/infrastructure/documents/renderers/fastreport/issued-invoice-document-renderer.ts rename to modules/customer-invoices/src/api/infrastructure/issued-invoices/documents/renderers/fastreport/issued-invoice-document-renderer.ts index f6f0d344..54be56a9 100644 --- a/modules/customer-invoices/src/api/infrastructure/documents/renderers/fastreport/issued-invoice-document-renderer.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/documents/renderers/fastreport/issued-invoice-document-renderer.ts @@ -6,7 +6,7 @@ import type { IDocumentRenderer, } from "@erp/core/api"; -import type { IssuedInvoiceReportSnapshot } from "../../../../application"; +import type { IssuedInvoiceReportSnapshot } from "../../../../../application"; /** * Adaptador Application → Infra para la generación del documento diff --git a/modules/customer-invoices/src/api/infrastructure/documents/renderers/index.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/documents/renderers/index.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/documents/renderers/index.ts rename to modules/customer-invoices/src/api/infrastructure/issued-invoices/documents/renderers/index.ts diff --git a/modules/customer-invoices/src/api/infrastructure/documents/side-effects/index.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/documents/side-effects/index.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/documents/side-effects/index.ts rename to modules/customer-invoices/src/api/infrastructure/issued-invoices/documents/side-effects/index.ts diff --git a/modules/customer-invoices/src/api/infrastructure/documents/side-effects/persist-issued-invoice-document-side-effect.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/documents/side-effects/persist-issued-invoice-document-side-effect.ts similarity index 100% rename from modules/customer-invoices/src/api/infrastructure/documents/side-effects/persist-issued-invoice-document-side-effect.ts rename to modules/customer-invoices/src/api/infrastructure/issued-invoices/documents/side-effects/persist-issued-invoice-document-side-effect.ts diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/index.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/index.ts new file mode 100644 index 00000000..e8fe393b --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/index.ts @@ -0,0 +1,3 @@ +export * from "./di"; +export * from "./documents"; +export * from "./persistence"; diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/index.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/index.ts new file mode 100644 index 00000000..62f8ac11 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/index.ts @@ -0,0 +1 @@ +export * from "./sequelize"; 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 new file mode 100644 index 00000000..0c2e706a --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/index.ts @@ -0,0 +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/index.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/index.ts new file mode 100644 index 00000000..2b35b3f8 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/index.ts @@ -0,0 +1 @@ +export * from "./sequelize-issued-invoice-domain.mapper"; 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 new file mode 100644 index 00000000..1df3a7fd --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-domain.mapper.ts @@ -0,0 +1,409 @@ +import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api"; +import { + CurrencyCode, + LanguageCode, + Percentage, + TextValue, + UniqueID, + UtcDate, + ValidationErrorCollection, + type ValidationErrorDetail, + extractOrPushError, + maybeFromNullableVO, + toNullable, +} from "@repo/rdx-ddd"; +import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils"; + +import type { IIssuedInvoiceDomainMapper } from "../../../../../../application"; +import { + CustomerInvoiceItems, + type IIssuedInvoiceProps, + InvoiceNumber, + InvoicePaymentMethod, + InvoiceSerie, + InvoiceStatus, + IssuedInvoice, +} from "../../../../../../domain"; +import type { + CustomerInvoiceCreationAttributes, + CustomerInvoiceModel, +} from "../../../../../common"; + +import { SequelizeIssuedInvoiceItemDomainMapper } from "./sequelize-issued-invoice-item-domain.mapper"; +import { SequelizeIssuedInvoiceRecipientDomainMapper } from "./sequelize-issued-invoice-recipient-domain.mapper"; +import { SequelizeIssuedInvoiceTaxesDomainMapper } from "./sequelize-issued-invoice-taxes-domain.mapper"; +import { SequelizeIssuedInvoiceVerifactuDomainMapper } from "./sequelize-verifactu-record-domain.mapper"; + +export class SequelizeIssuedInvoiceDomainMapper + extends SequelizeDomainMapper< + CustomerInvoiceModel, + CustomerInvoiceCreationAttributes, + IssuedInvoice + > + implements IIssuedInvoiceDomainMapper +{ + private _itemsMapper: SequelizeIssuedInvoiceItemDomainMapper; + private _recipientMapper: SequelizeIssuedInvoiceRecipientDomainMapper; + private _taxesMapper: SequelizeIssuedInvoiceTaxesDomainMapper; + private _verifactuMapper: SequelizeIssuedInvoiceVerifactuDomainMapper; + + constructor(params: MapperParamsType) { + super(); + + this._itemsMapper = new SequelizeIssuedInvoiceItemDomainMapper(params); // Instanciar el mapper de items + this._recipientMapper = new SequelizeIssuedInvoiceRecipientDomainMapper(); + this._taxesMapper = new SequelizeIssuedInvoiceTaxesDomainMapper(params); + this._verifactuMapper = new SequelizeIssuedInvoiceVerifactuDomainMapper(); + } + + private _mapAttributesToDomain(source: CustomerInvoiceModel, params?: MapperParamsType) { + const { errors } = params as { + errors: ValidationErrorDetail[]; + }; + + const invoiceId = extractOrPushError(UniqueID.create(source.id), "id", errors); + const companyId = extractOrPushError(UniqueID.create(source.company_id), "company_id", errors); + + const customerId = extractOrPushError( + UniqueID.create(source.customer_id), + "customer_id", + errors + ); + + const isIssuedInvoice = Boolean(source.is_proforma); + + const proformaId = extractOrPushError( + maybeFromNullableVO(source.proforma_id, (v) => UniqueID.create(v)), + "proforma_id", + errors + ); + + const status = extractOrPushError(InvoiceStatus.create(source.status), "status", errors); + + const series = extractOrPushError( + maybeFromNullableVO(source.series, (v) => InvoiceSerie.create(v)), + "series", + errors + ); + + const invoiceNumber = extractOrPushError( + InvoiceNumber.create(source.invoice_number), + "invoice_number", + errors + ); + + // Fechas + const invoiceDate = extractOrPushError( + UtcDate.createFromISO(source.invoice_date), + "invoice_date", + errors + ); + + const operationDate = extractOrPushError( + maybeFromNullableVO(source.operation_date, (v) => UtcDate.createFromISO(v)), + "operation_date", + errors + ); + + // Idioma / divisa + const languageCode = extractOrPushError( + LanguageCode.create(source.language_code), + "language_code", + errors + ); + + const currencyCode = extractOrPushError( + CurrencyCode.create(source.currency_code), + "currency_code", + errors + ); + + // Textos opcionales + const reference = extractOrPushError( + maybeFromNullableVO(source.reference, (value) => Result.ok(String(value))), + "reference", + errors + ); + + const description = extractOrPushError( + maybeFromNullableVO(source.description, (value) => Result.ok(String(value))), + "description", + errors + ); + + const notes = extractOrPushError( + maybeFromNullableVO(source.notes, (value) => TextValue.create(value)), + "notes", + errors + ); + + // Método de pago (VO opcional con id + descripción) + let paymentMethod = Maybe.none(); + + if (!isNullishOrEmpty(source.payment_method_id)) { + const paymentId = extractOrPushError( + UniqueID.create(String(source.payment_method_id)), + "paymentMethod.id", + errors + ); + + const paymentVO = extractOrPushError( + InvoicePaymentMethod.create( + { paymentDescription: String(source.payment_method_description ?? "") }, + paymentId ?? undefined + ), + "payment_method_description", + errors + ); + + if (paymentVO) { + paymentMethod = Maybe.some(paymentVO); + } + } + + // % descuento (VO) + const discountPercentage = extractOrPushError( + Percentage.create({ + value: Number(source.discount_percentage_value ?? 0), + scale: Number(source.discount_percentage_scale ?? 2), + }), + "discount_percentage_value", + errors + ); + + return { + invoiceId, + companyId, + customerId, + isIssuedInvoice, + proformaId, + status, + series, + invoiceNumber, + invoiceDate, + operationDate, + reference, + description, + notes, + languageCode, + currencyCode, + discountPercentage, + paymentMethod, + }; + } + + public mapToDomain( + source: CustomerInvoiceModel, + params?: MapperParamsType + ): Result { + try { + const errors: ValidationErrorDetail[] = []; + + // 1) Valores escalares (atributos generales) + const attributes = this._mapAttributesToDomain(source, { errors, ...params }); + + // 2) Recipient (snapshot en la factura o include) + const recipientResult = this._recipientMapper.mapToDomain(source, { + errors, + attributes, + ...params, + }); + + // 3) Verifactu (snapshot en la factura o include) + const verifactuResult = this._verifactuMapper.mapToDomain(source.verifactu, { + errors, + attributes, + ...params, + }); + + // 4) Items (colección) + const itemsResults = this._itemsMapper.mapToDomainCollection( + source.items, + source.items.length, + { + errors, + attributes, + ...params, + } + ); + + // 5) Si hubo errores de mapeo, devolvemos colección de validación + if (errors.length > 0) { + return Result.fail( + new ValidationErrorCollection("Customer invoice mapping failed [mapToDomain]", errors) + ); + } + + // 6) Construcción del agregado (Dominio) + + const items = CustomerInvoiceItems.create({ + languageCode: attributes.languageCode!, + currencyCode: attributes.currencyCode!, + globalDiscountPercentage: attributes.discountPercentage!, + items: itemsResults.data.getAll(), + }); + + const invoiceProps: IIssuedInvoiceProps = { + companyId: attributes.companyId!, + + isIssuedInvoice: attributes.isIssuedInvoice, + proformaId: attributes.proformaId!, + status: attributes.status!, + series: attributes.series!, + invoiceNumber: attributes.invoiceNumber!, + invoiceDate: attributes.invoiceDate!, + operationDate: attributes.operationDate!, + + customerId: attributes.customerId!, + recipient: recipientResult.data, + + reference: attributes.reference!, + description: attributes.description!, + notes: attributes.notes!, + + languageCode: attributes.languageCode!, + currencyCode: attributes.currencyCode!, + + discountPercentage: attributes.discountPercentage!, + + paymentMethod: attributes.paymentMethod!, + + items, + verifactu: verifactuResult.data, + }; + + const createResult = IssuedInvoice.create(invoiceProps, attributes.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); + } catch (err: unknown) { + return Result.fail(err as Error); + } + } + + public mapToPersistence( + source: IssuedInvoice, + params?: MapperParamsType + ): Result { + const errors: ValidationErrorDetail[] = []; + + // 1) Items + const itemsResult = this._itemsMapper.mapToPersistenceArray(source.items, { + errors, + parent: source, + ...params, + }); + + if (itemsResult.isFailure) { + errors.push({ + path: "items", + message: itemsResult.error.message, + }); + } + + // 2) Taxes + const taxesResult = this._taxesMapper.mapToPersistenceArray(source.getTaxes(), { + errors, + parent: source, + ...params, + }); + + if (taxesResult.isFailure) { + errors.push({ + path: "taxes", + message: taxesResult.error.message, + }); + } + + // 3) Cliente + const recipient = this._recipientMapper.mapToPersistence(source.recipient, { + errors, + parent: source, + ...params, + }); + + // 4) Verifactu + const verifactuResult = this._verifactuMapper.mapToPersistence(source.verifactu, { + errors, + parent: source, + ...params, + }); + + // 5) Si hubo errores de mapeo, devolvemos colección de validación + if (errors.length > 0) { + return Result.fail( + new ValidationErrorCollection("Customer invoice mapping to persistence failed", errors) + ); + } + + const items = itemsResult.data; + const taxes = taxesResult.data; + const verifactu = verifactuResult.data; + + const allAmounts = source.calculateAllAmounts(); // Da los totales ya calculados + + const invoiceValues: Partial = { + // Identificación + id: source.id.toPrimitive(), + company_id: source.companyId.toPrimitive(), + + // Flags / estado / serie / número + is_proforma: false, + proforma_id: toNullable(source.proformaId, (v) => v.toPrimitive()), + status: source.status.toPrimitive(), + series: toNullable(source.series, (v) => v.toPrimitive()), + invoice_number: source.invoiceNumber.toPrimitive(), + + invoice_date: source.invoiceDate.toPrimitive(), + operation_date: toNullable(source.operationDate, (v) => v.toPrimitive()), + language_code: source.languageCode.toPrimitive(), + currency_code: source.currencyCode.toPrimitive(), + + reference: toNullable(source.reference, (reference) => reference), + description: toNullable(source.description, (description) => description), + notes: toNullable(source.notes, (v) => v.toPrimitive()), + + subtotal_amount_value: source.subtotalAmount.value, + subtotal_amount_scale: source.subtotalAmount.scale, + + discount_percentage_value: source.discountPercentage.toPrimitive().value, + discount_percentage_scale: source.discountPercentage.toPrimitive().scale, + + discount_amount_value: source.globalDiscountAmount.value, + discount_amount_scale: source.globalDiscountAmount.scale, + + taxable_amount_value: source.taxableAmount.value, + taxable_amount_scale: source.taxableAmount.scale, + + taxes_amount_value: source.taxesAmount.value, + taxes_amount_scale: source.taxesAmount.scale, + + total_amount_value: source.totalAmount.value, + total_amount_scale: source.totalAmount.scale, + + payment_method_id: toNullable(source.paymentMethod, (payment) => payment.toObjectString().id), + payment_method_description: toNullable( + source.paymentMethod, + (payment) => payment.toObjectString().payment_description + ), + + customer_id: source.customerId.toPrimitive(), + ...recipient, + + taxes, + items, + verifactu, + }; + + return Result.ok( + invoiceValues as CustomerInvoiceCreationAttributes + ); + } +} 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 new file mode 100644 index 00000000..62d748d3 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-item-domain.mapper.ts @@ -0,0 +1,306 @@ +import type { JsonTaxCatalogProvider } from "@erp/core"; +import { type MapperParamsType, SequelizeDomainMapper, Tax } from "@erp/core/api"; +import { + UniqueID, + ValidationErrorCollection, + type ValidationErrorDetail, + extractOrPushError, + maybeFromNullableVO, + toNullable, +} from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import { + type IssuedInvoice, + IssuedInvoiceItem, + type IssuedInvoiceItemProps, + type IssuedInvoiceProps, + ItemAmount, + ItemDescription, + ItemDiscount, + ItemQuantity, + ItemTaxGroup, +} from "../../../../../../domain"; +import type { + CustomerInvoiceItemCreationAttributes, + CustomerInvoiceItemModel, +} from "../../../../../common"; + +export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMapper< + CustomerInvoiceItemModel, + CustomerInvoiceItemCreationAttributes, + IssuedInvoiceItem +> { + private _taxCatalog!: JsonTaxCatalogProvider; + + constructor(params: MapperParamsType) { + super(); + const { taxCatalog } = params as { + taxCatalog: JsonTaxCatalogProvider; + }; + + if (!taxCatalog) { + throw new Error('taxCatalog not defined ("CustomerInvoiceItemDomainMapper")'); + } + + this._taxCatalog = taxCatalog; + } + + private mapAttributesToDomain( + source: CustomerInvoiceItemModel, + params?: MapperParamsType + ): Partial & { itemId?: UniqueID } { + const { errors, index, attributes } = params as { + index: number; + errors: ValidationErrorDetail[]; + attributes: Partial; + }; + + const itemId = extractOrPushError( + UniqueID.create(source.item_id), + `items[${index}].item_id`, + errors + ); + + const description = extractOrPushError( + maybeFromNullableVO(source.description, (v) => ItemDescription.create(v)), + `items[${index}].description`, + errors + ); + + const quantity = extractOrPushError( + maybeFromNullableVO(source.quantity_value, (v) => ItemQuantity.create({ value: v })), + `items[${index}].quantity`, + errors + ); + + const unitAmount = extractOrPushError( + maybeFromNullableVO(source.unit_amount_value, (value) => + ItemAmount.create({ value, currency_code: attributes.currencyCode?.code }) + ), + `items[${index}].unit_amount`, + errors + ); + + const discountPercentage = extractOrPushError( + maybeFromNullableVO(source.discount_percentage_value, (v) => + ItemDiscount.create({ value: v }) + ), + `items[${index}].discount_percentage`, + errors + ); + + const globalDiscountPercentage = extractOrPushError( + maybeFromNullableVO(source.global_discount_percentage_value, (v) => + ItemDiscount.create({ value: v }) + ), + `items[${index}].discount_percentage`, + errors + ); + + const iva = extractOrPushError( + maybeFromNullableVO(source.iva_code, (code) => Tax.createFromCode(code, this._taxCatalog)), + `items[${index}].iva_code`, + errors + ); + + const rec = extractOrPushError( + maybeFromNullableVO(source.rec_code, (code) => Tax.createFromCode(code, this._taxCatalog)), + `items[${index}].rec_code`, + errors + ); + + const retention = extractOrPushError( + maybeFromNullableVO(source.retention_code, (code) => + Tax.createFromCode(code, this._taxCatalog) + ), + `items[${index}].retention_code`, + errors + ); + + return { + itemId, + + languageCode: attributes.languageCode, + currencyCode: attributes.currencyCode, + description, + quantity, + unitAmount, + itemDiscountPercentage: discountPercentage, + globalDiscountPercentage, + + taxes: ItemTaxGroup.create({ + iva: iva!, + rec: rec!, + retention: retention!, + }).data, + }; + } + + public mapToDomain( + source: CustomerInvoiceItemModel, + params?: MapperParamsType + ): Result { + const { errors, index } = params as { + index: number; + errors: ValidationErrorDetail[]; + attributes: Partial; + }; + + // 1) Valores escalares (atributos generales) + const attributes = this.mapAttributesToDomain(source, params); + + // Si hubo errores de mapeo, devolvemos colección de validación + if (errors.length > 0) { + return Result.fail( + new ValidationErrorCollection("Customer invoice item mapping failed [mapToDomain]", errors) + ); + } + + // 2) Construcción del elemento de dominio + const createResult = IssuedInvoiceItem.create( + { + description: attributes.description!, + + quantity: attributes.quantity!, + unitAmount: attributes.unitAmount!, + + subtotalAmount: attributes.subtotalAmount!, + + itemDiscountPercentage: attributes.itemDiscountPercentage!, + itemDiscountAmount: attributes.itemDiscountAmount!, + + globalDiscountPercentage: attributes.globalDiscountPercentage!, + globalDiscountAmount: attributes.globalDiscountAmount!, + + totalDiscountAmount: attributes.totalDiscountAmount!, + + taxableAmount: attributes.taxableAmount!, + + ivaAmount: attributes.ivaAmount!, + recAmount: attributes.recAmount!, + retentionAmount: attributes.retentionAmount!, + + taxesAmount: attributes.taxesAmount!, + totalAmount: attributes.totalAmount!, + + taxes: attributes.taxes!, + + languageCode: attributes.languageCode!, + currencyCode: attributes.currencyCode!, + }, + attributes.itemId + ); + + if (createResult.isFailure) { + return Result.fail( + new ValidationErrorCollection("Invoice item entity creation failed", [ + { path: `items[${index}]`, message: createResult.error.message }, + ]) + ); + } + + return createResult; + } + + public mapToPersistence( + source: IssuedInvoiceItem, + params?: MapperParamsType + ): Result { + const { errors, index, parent } = params as { + index: number; + parent: IssuedInvoice; + errors: ValidationErrorDetail[]; + }; + + const taxesAmounts = source.taxes; + + return Result.ok({ + item_id: source.id.toPrimitive(), + invoice_id: parent.id.toPrimitive(), + position: index, + + description: toNullable(source.description, (v) => v.toPrimitive()), + + quantity_value: toNullable(source.quantity, (v) => v.toPrimitive().value), + quantity_scale: + toNullable(source.quantity, (v) => v.toPrimitive().scale) ?? ItemQuantity.DEFAULT_SCALE, + + unit_amount_value: toNullable(source.unitAmount, (v) => v.toPrimitive().value), + unit_amount_scale: + toNullable(source.unitAmount, (v) => v.toPrimitive().scale) ?? ItemAmount.DEFAULT_SCALE, + + subtotal_amount_value: source.subtotalAmount.value, + subtotal_amount_scale: source.subtotalAmount.scale, + + // + discount_percentage_value: toNullable( + source.itemDiscountPercentage, + (v) => v.toPrimitive().value + ), + discount_percentage_scale: + toNullable(source.itemDiscountPercentage, (v) => v.toPrimitive().scale) ?? + ItemDiscount.DEFAULT_SCALE, + + discount_amount_value: source.itemDiscountAmount.value, + discount_amount_scale: source.itemDiscountAmount.scale, + + // + global_discount_percentage_value: toNullable( + source.globalDiscountPercentage, + (v) => v.toPrimitive().value + ), + + global_discount_percentage_scale: + toNullable(source.globalDiscountPercentage, (v) => v.toPrimitive().scale) ?? + ItemDiscount.DEFAULT_SCALE, + + global_discount_amount_value: source.globalDiscountAmount.value, + global_discount_amount_scale: source.globalDiscountAmount.scale, + + // + total_discount_amount_value: source.totalDiscountAmount.value, + total_discount_amount_scale: source.totalDiscountAmount.scale, + + // + taxable_amount_value: source.taxableAmount.value, + taxable_amount_scale: source.taxableAmount.scale, + + // IVA + iva_code: toNullable(source.taxes.iva, (v) => v.code), + + iva_percentage_value: toNullable(source.taxes.iva, (v) => v.percentage.value), + iva_percentage_scale: toNullable(source.taxes.iva, (v) => v.percentage.scale) ?? 2, + + iva_amount_value: taxesAmounts.ivaAmount.value, + iva_amount_scale: taxesAmounts.ivaAmount.scale, + + // REC + rec_code: toNullable(source.taxes.rec, (v) => v.code), + + rec_percentage_value: toNullable(source.taxes.rec, (v) => v.percentage.value), + rec_percentage_scale: toNullable(source.taxes.rec, (v) => v.percentage.scale) ?? 2, + + rec_amount_value: taxesAmounts.recAmount.value, + rec_amount_scale: taxesAmounts.recAmount.scale, + + // RET + retention_code: toNullable(source.taxes.retention, (v) => v.code), + + retention_percentage_value: toNullable(source.taxes.retention, (v) => v.percentage.value), + retention_percentage_scale: + toNullable(source.taxes.retention, (v) => v.percentage.scale) ?? 2, + + retention_amount_value: taxesAmounts.retentionAmount.value, + retention_amount_scale: taxesAmounts.retentionAmount.scale, + + // + taxes_amount_value: source.taxesAmount.value, + taxes_amount_scale: source.taxesAmount.scale, + + // + total_amount_value: source.totalAmount.value, + total_amount_scale: source.totalAmount.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 new file mode 100644 index 00000000..b6a1a827 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-recipient-domain.mapper.ts @@ -0,0 +1,156 @@ +import type { MapperParamsType } from "@erp/core/api"; +import { + City, + Country, + Name, + PostalCode, + Province, + Street, + TINNumber, + ValidationErrorCollection, + type ValidationErrorDetail, + extractOrPushError, + maybeFromNullableVO, + toNullable, +} from "@repo/rdx-ddd"; +import { Maybe, Result } from "@repo/rdx-utils"; + +import { + InvoiceRecipient, + type IssuedInvoice, + type IssuedInvoiceProps, +} from "../../../../../../domain"; +import type { CustomerInvoiceModel } from "../../../../../common"; + +export class SequelizeIssuedInvoiceRecipientDomainMapper { + public mapToDomain( + source: CustomerInvoiceModel, + params?: MapperParamsType + ): Result, Error> { + /** + * - Issued invoice -> snapshot de los datos (campos customer_*) + */ + + const { errors, attributes } = params as { + errors: ValidationErrorDetail[]; + attributes: Partial; + }; + + const _name = source.customer_name!; + const _tin = source.customer_tin!; + const _street = source.customer_street!; + const _street2 = source.customer_street2!; + const _city = source.customer_city!; + const _postal_code = source.customer_postal_code!; + const _province = source.customer_province!; + const _country = source.customer_country!; + + // Customer (snapshot) + const customerName = extractOrPushError(Name.create(_name!), "customer_name", errors); + + const customerTin = extractOrPushError(TINNumber.create(_tin!), "customer_tin", errors); + + const customerStreet = extractOrPushError( + maybeFromNullableVO(_street, (value) => Street.create(value)), + "customer_street", + errors + ); + + const customerStreet2 = extractOrPushError( + maybeFromNullableVO(_street2, (value) => Street.create(value)), + "customer_street2", + errors + ); + + const customerCity = extractOrPushError( + maybeFromNullableVO(_city, (value) => City.create(value)), + "customer_city", + errors + ); + + const customerProvince = extractOrPushError( + maybeFromNullableVO(_province, (value) => Province.create(value)), + "customer_province", + errors + ); + + const customerPostalCode = extractOrPushError( + maybeFromNullableVO(_postal_code, (value) => PostalCode.create(value)), + "customer_postal_code", + errors + ); + + const customerCountry = extractOrPushError( + maybeFromNullableVO(_country, (value) => Country.create(value)), + "customer_country", + errors + ); + + const createResult = InvoiceRecipient.create({ + name: customerName!, + tin: customerTin!, + street: customerStreet!, + street2: customerStreet2!, + city: customerCity!, + postalCode: customerPostalCode!, + province: customerProvince!, + country: customerCountry!, + }); + + if (createResult.isFailure) { + return Result.fail( + new ValidationErrorCollection("Invoice recipient entity creation failed", [ + { path: "recipient", message: createResult.error.message }, + ]) + ); + } + + return Result.ok(Maybe.some(createResult.data)); + } + + /** + * Mapea los datos del destinatario (recipient) de una factura de cliente + * al formato esperado por la capa de persistencia. + * + * Reglas: + * - Si la factura es proforma (`isProforma === true`), todos los campos de recipient son `null`. + * - Si la factura no es proforma (`isProforma === false`), debe existir `recipient`. + * En caso contrario, se agrega un error de validación. + */ + mapToPersistence(source: Maybe, params?: MapperParamsType) { + const { errors, parent } = params as { + parent: IssuedInvoice; + errors: ValidationErrorDetail[]; + }; + + const { hasRecipient } = parent; + + // Validación: facturas emitidas deben tener destinatario. + if (!hasRecipient) { + errors.push({ + path: "recipient", + message: "[InvoiceRecipientDomainMapper] Issued customer invoice without recipient data", + }); + } + + // Si hay errores previos, devolvemos fallo de validación inmediatamente. + if (errors.length > 0) { + return Result.fail( + new ValidationErrorCollection("Customer invoice mapping to persistence failed", errors) + ); + } + + const recipient = source.unwrap(); + + return { + customer_tin: recipient.tin.toPrimitive(), + customer_name: recipient.name.toPrimitive(), + customer_street: toNullable(recipient.street, (v) => v.toPrimitive()), + customer_street2: toNullable(recipient.street2, (v) => v.toPrimitive()), + customer_city: toNullable(recipient.city, (v) => v.toPrimitive()), + customer_province: toNullable(recipient.province, (v) => v.toPrimitive()), + customer_postal_code: toNullable(recipient.postalCode, (v) => v.toPrimitive()), + customer_country: toNullable(recipient.country, (v) => v.toPrimitive()), + }; + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/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 new file mode 100644 index 00000000..81902d27 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-taxes-domain.mapper.ts @@ -0,0 +1,94 @@ +import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api"; +import { UniqueID, type ValidationErrorDetail, toNullable } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { InvoiceTaxGroup, IssuedInvoice } from "../../../../../../domain"; +import type { + CustomerInvoiceTaxCreationAttributes, + CustomerInvoiceTaxModel, +} from "../../../../../common"; + +/** + * Mapper para customer_invoice_taxes + * + * Domina estructuras: + * { + * tax: Tax + * taxableAmount: ItemAmount + * taxesAmount: ItemAmount + * } + * + * Cada fila = un impuesto agregado en toda la factura. + */ +export class SequelizeIssuedInvoiceTaxesDomainMapper extends SequelizeDomainMapper< + CustomerInvoiceTaxModel, + CustomerInvoiceTaxCreationAttributes, + InvoiceTaxGroup +> { + public mapToDomain( + source: CustomerInvoiceTaxModel, + params?: MapperParamsType + ): Result { + throw new Error("Se calcula a partir de las líneas de detalle"); + } + + public mapToPersistence( + source: InvoiceTaxGroup, + params?: MapperParamsType + ): Result { + const { errors, parent } = params as { + parent: IssuedInvoice; + errors: ValidationErrorDetail[]; + }; + + try { + const { ivaAmount, recAmount, retentionAmount } = source; + + const totalTaxes = ivaAmount.add(recAmount).add(retentionAmount); + + const dto: CustomerInvoiceTaxCreationAttributes = { + tax_id: UniqueID.generateNewID().toPrimitive(), + invoice_id: parent.id.toPrimitive(), + + // TAXABLE AMOUNT + taxable_amount_value: source.taxableAmount.value, + taxable_amount_scale: source.taxableAmount.scale, + + // IVA + iva_code: source.iva.code, + + iva_percentage_value: source.iva.value, + iva_percentage_scale: source.iva.scale, + + iva_amount_value: ivaAmount.value, + iva_amount_scale: ivaAmount.scale, + + // REC + rec_code: toNullable(source.rec, (v) => v.code), + + rec_percentage_value: toNullable(source.rec, (v) => v.percentage.value), + rec_percentage_scale: toNullable(source.rec, (v) => v.percentage.scale) ?? 2, + + rec_amount_value: recAmount.value, + rec_amount_scale: recAmount.scale, + + // RET + retention_code: toNullable(source.retention, (v) => v.code), + + retention_percentage_value: toNullable(source.retention, (v) => v.percentage.value), + retention_percentage_scale: toNullable(source.retention, (v) => v.percentage.scale) ?? 2, + + retention_amount_value: retentionAmount.value, + retention_amount_scale: retentionAmount.scale, + + // TOTAL + taxes_amount_value: totalTaxes.value, + taxes_amount_scale: totalTaxes.scale, + }; + + return Result.ok(dto); + } catch (error: unknown) { + return Result.fail(error as Error); + } + } +} 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 new file mode 100644 index 00000000..838ba69f --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-verifactu-record-domain.mapper.ts @@ -0,0 +1,135 @@ +import type { MapperParamsType } from "@erp/core/api"; +import { SequelizeDomainMapper } from "@erp/core/api"; +import { + URLAddress, + UniqueID, + ValidationErrorCollection, + type ValidationErrorDetail, + extractOrPushError, + maybeFromNullableVO, + toEmptyString, +} from "@repo/rdx-ddd"; +import { Maybe, Result } from "@repo/rdx-utils"; + +import { + type IssuedInvoice, + type IssuedInvoiceProps, + VerifactuRecord, + VerifactuRecordEstado, +} from "../../../../../../domain"; +import type { + VerifactuRecordCreationAttributes, + VerifactuRecordModel, +} from "../../../../../common"; + +export class SequelizeIssuedInvoiceVerifactuDomainMapper extends SequelizeDomainMapper< + VerifactuRecordModel, + VerifactuRecordCreationAttributes, + Maybe +> { + public mapToDomain( + source: VerifactuRecordModel, + params?: MapperParamsType + ): Result, Error> { + const { errors, attributes } = params as { + errors: ValidationErrorDetail[]; + attributes: Partial; + }; + + if (!source) { + return Result.ok(Maybe.none()); + } + + const recordId = extractOrPushError(UniqueID.create(source.id), "id", errors); + const estado = extractOrPushError( + VerifactuRecordEstado.create(source.estado), + "estado", + errors + ); + + const qr = extractOrPushError( + maybeFromNullableVO(source.qr, (value) => Result.ok(String(value))), + "qr", + errors + ); + + const url = extractOrPushError( + maybeFromNullableVO(source.url, (value) => URLAddress.create(value)), + "url", + errors + ); + + const uuid = extractOrPushError( + maybeFromNullableVO(source.uuid, (value) => Result.ok(String(value))), + "uuid", + errors + ); + + const operacion = extractOrPushError( + maybeFromNullableVO(source.operacion, (value) => Result.ok(String(value))), + "operacion", + errors + ); + + if (errors.length > 0) { + return Result.fail( + new ValidationErrorCollection("Verifactu record mapping failed [mapToDTO]", errors) + ); + } + + const createResult = VerifactuRecord.create( + { + estado: estado!, + qrCode: qr!, + url: url!, + uuid: uuid!, + operacion: operacion!, + }, + recordId! + ); + + if (createResult.isFailure) { + return Result.fail( + new ValidationErrorCollection("Invoice verifactu entity creation failed", [ + { path: "verifactu", message: createResult.error.message }, + ]) + ); + } + + return Result.ok(Maybe.some(createResult.data)); + } + + mapToPersistence( + source: Maybe, + params?: MapperParamsType + ): Result { + const { errors, parent } = params as { + parent: IssuedInvoice; + errors: ValidationErrorDetail[]; + }; + + if (source.isNone()) { + return Result.ok({ + id: UniqueID.generateNewID().toPrimitive(), + invoice_id: parent.id.toPrimitive(), + estado: VerifactuRecordEstado.createPendiente().toPrimitive(), + qr: "", + url: "", + uuid: "", + operacion: "", + }); + } + + const verifactu = source.unwrap(); + + return Result.ok({ + id: verifactu.id.toPrimitive(), + invoice_id: parent.id.toPrimitive(), + estado: verifactu.estado.toPrimitive(), + qr: toEmptyString(verifactu.qrCode, (v) => v), + url: toEmptyString(verifactu.url, (v) => v.toPrimitive()), + uuid: toEmptyString(verifactu.uuid, (v) => v), + operacion: toEmptyString(verifactu.operacion, (v) => v), + }); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/index.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/index.ts new file mode 100644 index 00000000..9b0ff906 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/index.ts @@ -0,0 +1,2 @@ +export * from "./domain"; +export * from "./list"; diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/list/index.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/list/index.ts new file mode 100644 index 00000000..1a7689db --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/list/index.ts @@ -0,0 +1 @@ +export * from "./sequelize-issued-invoice.list.mapper"; diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/list/sequelize-issued-invoice-recipient.list.mapper.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/list/sequelize-issued-invoice-recipient.list.mapper.ts new file mode 100644 index 00000000..7ca1c651 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/list/sequelize-issued-invoice-recipient.list.mapper.ts @@ -0,0 +1,119 @@ +import { type MapperParamsType, SequelizeQueryMapper } from "@erp/core/api"; +import { + City, + Country, + Name, + PostalCode, + Province, + Street, + TINNumber, + ValidationErrorCollection, + type ValidationErrorDetail, + extractOrPushError, + maybeFromNullableVO, +} from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import { InvoiceRecipient } from "../../../../../../domain"; +import type { CustomerInvoiceModel } from "../../../../../common"; + +import type { CustomerInvoiceListDTO } from "./sequelize-issued-invoice.list.mapper"; + +export class SequelizeIssuedInvoiceRecipientListMapper extends SequelizeQueryMapper< + CustomerInvoiceModel, + InvoiceRecipient +> { + public mapToDTO( + raw: CustomerInvoiceModel, + params?: MapperParamsType + ): Result { + /** + * - Issued invoice => snapshot de los datos (campos customer_*) + */ + + const { errors, attributes } = params as { + errors: ValidationErrorDetail[]; + attributes: Partial; + }; + + const { isProforma } = attributes; + + if (isProforma && !raw.current_customer) { + errors.push({ + path: "current_customer", + message: "Current customer not included in query (InvoiceRecipientListMapper)", + }); + } + + const _name = raw.customer_name!; + const _tin = raw.customer_tin!; + const _street = raw.customer_street!; + const _street2 = raw.customer_street2!; + const _city = raw.customer_city!; + const _postal_code = raw.customer_postal_code!; + const _province = raw.customer_province!; + const _country = raw.customer_country!; + + // Customer (snapshot) + const customerName = extractOrPushError(Name.create(_name!), "customer_name", errors); + + const customerTin = extractOrPushError(TINNumber.create(_tin!), "customer_tin", errors); + + const customerStreet = extractOrPushError( + maybeFromNullableVO(_street, (value) => Street.create(value)), + "customer_street", + errors + ); + + const customerStreet2 = extractOrPushError( + maybeFromNullableVO(_street2, (value) => Street.create(value)), + "customer_street2", + errors + ); + + const customerCity = extractOrPushError( + maybeFromNullableVO(_city, (value) => City.create(value)), + "customer_city", + errors + ); + + const customerProvince = extractOrPushError( + maybeFromNullableVO(_province, (value) => Province.create(value)), + "customer_province", + errors + ); + + const customerPostalCode = extractOrPushError( + maybeFromNullableVO(_postal_code, (value) => PostalCode.create(value)), + "customer_postal_code", + errors + ); + + const customerCountry = extractOrPushError( + maybeFromNullableVO(_country, (value) => Country.create(value)), + "customer_country", + errors + ); + + const createResult = InvoiceRecipient.create({ + name: customerName!, + tin: customerTin!, + street: customerStreet!, + street2: customerStreet2!, + city: customerCity!, + postalCode: customerPostalCode!, + province: customerProvince!, + country: customerCountry!, + }); + + if (createResult.isFailure) { + return Result.fail( + new ValidationErrorCollection("Invoice recipient entity creation failed", [ + { path: "recipient", message: createResult.error.message }, + ]) + ); + } + + return Result.ok(createResult.data); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/list/sequelize-issued-invoice.list.mapper.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/list/sequelize-issued-invoice.list.mapper.ts new file mode 100644 index 00000000..8eb710bb --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/list/sequelize-issued-invoice.list.mapper.ts @@ -0,0 +1,254 @@ +import { type MapperParamsType, SequelizeQueryMapper } from "@erp/core/api"; +import { + CurrencyCode, + LanguageCode, + Percentage, + UniqueID, + UtcDate, + ValidationErrorCollection, + type ValidationErrorDetail, + extractOrPushError, + maybeFromNullableVO, +} from "@repo/rdx-ddd"; +import { Maybe, Result } from "@repo/rdx-utils"; + +import type { IIssuedInvoiceListMapper, IssuedInvoiceListDTO } from "../../../../../../application"; +import { + InvoiceAmount, + InvoiceNumber, + InvoiceSerie, + InvoiceStatus, + type VerifactuRecord, +} from "../../../../../../domain"; +import type { CustomerInvoiceModel } from "../../../../../common"; + +import { SequelizeIssuedInvoiceRecipientListMapper } from "./sequelize-issued-invoice-recipient.list.mapper"; +import { SequelizeVerifactuRecordListMapper } from "./sequelize-verifactu-record.list.mapper"; + +export class SequelizeIssuedInvoiceListMapper + extends SequelizeQueryMapper + implements IIssuedInvoiceListMapper +{ + private _recipientMapper: SequelizeIssuedInvoiceRecipientListMapper; + private _verifactuMapper: SequelizeVerifactuRecordListMapper; + + constructor() { + super(); + this._recipientMapper = new SequelizeIssuedInvoiceRecipientListMapper(); + this._verifactuMapper = new SequelizeVerifactuRecordListMapper(); + } + + public mapToDTO( + raw: CustomerInvoiceModel, + params?: MapperParamsType + ): Result { + const errors: ValidationErrorDetail[] = []; + + // 1) Valores escalares (atributos generales) + const attributes = this.mapAttributesToDTO(raw, { errors, ...params }); + + // 2) Recipient (snapshot en la factura o include) + const recipientResult = this._recipientMapper.mapToDTO(raw, { + errors, + attributes, + ...params, + }); + + if (recipientResult.isFailure) { + errors.push({ + path: "recipient", + message: recipientResult.error.message, + }); + } + + // 4) Verifactu record + let verifactu: Maybe = Maybe.none(); + if (raw.verifactu) { + const verifactuResult = this._verifactuMapper.mapToDTO(raw.verifactu, { errors, ...params }); + + if (verifactuResult.isFailure) { + errors.push({ + path: "verifactu", + message: verifactuResult.error.message, + }); + } else { + verifactu = Maybe.some(verifactuResult.data); + } + } + + // 5) Si hubo errores de mapeo, devolvemos colección de validación + if (errors.length > 0) { + return Result.fail( + new ValidationErrorCollection("Customer invoice mapping failed [mapToDTO]", errors) + ); + } + + return Result.ok({ + id: attributes.invoiceId!, + companyId: attributes.companyId!, + isProforma: attributes.isProforma, + status: attributes.status!, + series: attributes.series!, + invoiceNumber: attributes.invoiceNumber!, + invoiceDate: attributes.invoiceDate!, + operationDate: attributes.operationDate!, + + description: attributes.description!, + reference: attributes.description!, + + customerId: attributes.customerId!, + recipient: recipientResult.data, + + languageCode: attributes.languageCode!, + currencyCode: attributes.currencyCode!, + + discountPercentage: attributes.discountPercentage!, + subtotalAmount: attributes.subtotalAmount!, + discountAmount: attributes.discountAmount!, + taxableAmount: attributes.taxableAmount!, + taxesAmount: attributes.taxesAmount!, + totalAmount: attributes.totalAmount!, + + verifactu, + }); + } + + private mapAttributesToDTO(raw: CustomerInvoiceModel, params?: MapperParamsType) { + const { errors } = params as { + errors: ValidationErrorDetail[]; + }; + + const invoiceId = extractOrPushError(UniqueID.create(raw.id), "id", errors); + const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors); + + const customerId = extractOrPushError(UniqueID.create(raw.customer_id), "customer_id", errors); + + const isProforma = Boolean(raw.is_proforma); + + const status = extractOrPushError(InvoiceStatus.create(raw.status), "status", errors); + + const series = extractOrPushError( + maybeFromNullableVO(raw.series, (value) => InvoiceSerie.create(value)), + "serie", + errors + ); + + const invoiceNumber = extractOrPushError( + InvoiceNumber.create(raw.invoice_number), + "invoice_number", + errors + ); + + const invoiceDate = extractOrPushError( + UtcDate.createFromISO(raw.invoice_date), + "invoice_date", + errors + ); + + const operationDate = extractOrPushError( + maybeFromNullableVO(raw.operation_date, (value) => UtcDate.createFromISO(value)), + "operation_date", + errors + ); + + const reference = extractOrPushError( + maybeFromNullableVO(raw.reference, (value) => Result.ok(String(value))), + "description", + errors + ); + + const description = extractOrPushError( + maybeFromNullableVO(raw.description, (value) => Result.ok(String(value))), + "description", + errors + ); + + const languageCode = extractOrPushError( + LanguageCode.create(raw.language_code), + "language_code", + errors + ); + + const currencyCode = extractOrPushError( + CurrencyCode.create(raw.currency_code), + "currency_code", + errors + ); + + const discountPercentage = extractOrPushError( + Percentage.create({ + value: raw.discount_percentage_value, + scale: raw.discount_percentage_scale, + }), + "discount_percentage_value", + errors + ); + + const subtotalAmount = extractOrPushError( + InvoiceAmount.create({ + value: raw.subtotal_amount_value, + currency_code: currencyCode?.code, + }), + "subtotal_amount_value", + errors + ); + + const discountAmount = extractOrPushError( + InvoiceAmount.create({ + value: raw.discount_amount_value, + currency_code: currencyCode?.code, + }), + "discount_amount_value", + errors + ); + + const taxableAmount = extractOrPushError( + InvoiceAmount.create({ + value: raw.taxable_amount_value, + currency_code: currencyCode?.code, + }), + "taxable_amount_value", + errors + ); + + const taxesAmount = extractOrPushError( + InvoiceAmount.create({ + value: raw.taxes_amount_value, + currency_code: currencyCode?.code, + }), + "taxes_amount_value", + errors + ); + + const totalAmount = extractOrPushError( + InvoiceAmount.create({ + value: raw.total_amount_value, + currency_code: currencyCode?.code, + }), + "total_amount_value", + errors + ); + + return { + invoiceId, + companyId, + customerId, + isProforma, + status, + series, + invoiceNumber, + invoiceDate, + operationDate, + reference, + description, + languageCode, + currencyCode, + discountPercentage, + subtotalAmount, + discountAmount, + taxableAmount, + taxesAmount, + totalAmount, + }; + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/list/sequelize-verifactu-record.list.mapper.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/list/sequelize-verifactu-record.list.mapper.ts new file mode 100644 index 00000000..32a7c6c3 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/list/sequelize-verifactu-record.list.mapper.ts @@ -0,0 +1,69 @@ +import { type MapperParamsType, SequelizeQueryMapper } from "@erp/core/api"; +import { + URLAddress, + UniqueID, + ValidationErrorCollection, + type ValidationErrorDetail, + extractOrPushError, + maybeFromNullableVO, +} from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import { VerifactuRecord, VerifactuRecordEstado } from "../../../../../../domain"; +import type { VerifactuRecordModel } from "../../../../../common"; + +export class SequelizeVerifactuRecordListMapper extends SequelizeQueryMapper< + VerifactuRecordModel, + VerifactuRecord +> { + public mapToDTO( + raw: VerifactuRecordModel, + params?: MapperParamsType + ): Result { + const errors: ValidationErrorDetail[] = []; + + const recordId = extractOrPushError(UniqueID.create(raw.id), "id", errors); + const estado = extractOrPushError(VerifactuRecordEstado.create(raw.estado), "estado", errors); + + const qr = extractOrPushError( + maybeFromNullableVO(raw.qr, (value) => Result.ok(String(value))), + "qr", + errors + ); + + const url = extractOrPushError( + maybeFromNullableVO(raw.url, (value) => URLAddress.create(value)), + "url", + errors + ); + + const uuid = extractOrPushError( + maybeFromNullableVO(raw.uuid, (value) => Result.ok(String(value))), + "uuid", + errors + ); + + const operacion = extractOrPushError( + maybeFromNullableVO(raw.operacion, (value) => Result.ok(String(value))), + "operacion", + errors + ); + + if (errors.length > 0) { + return Result.fail( + new ValidationErrorCollection("Verifactu record mapping failed [mapToDTO]", errors) + ); + } + + return VerifactuRecord.create( + { + estado: estado!, + qrCode: qr!, + url: url!, + uuid: uuid!, + operacion: operacion!, + }, + recordId! + ); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/index.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/index.ts new file mode 100644 index 00000000..89081e12 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/index.ts @@ -0,0 +1 @@ +export * from "./issued-invoice.repository"; diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/issued-invoice.repository.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/issued-invoice.repository.ts new file mode 100644 index 00000000..fb6c5421 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/repositories/issued-invoice.repository.ts @@ -0,0 +1,273 @@ +import { EntityNotFoundError, SequelizeRepository, translateSequelizeError } from "@erp/core/api"; +import { type Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server"; +import type { UniqueID } from "@repo/rdx-ddd"; +import { type Collection, Result } from "@repo/rdx-utils"; +import type { FindOptions, InferAttributes, OrderItem, Sequelize, Transaction } from "sequelize"; + +import type { IIssuedInvoiceRepository, IssuedInvoiceListDTO } from "../../../../../application"; +import type { IssuedInvoice } from "../../../../../domain"; +import { + CustomerInvoiceItemModel, + CustomerInvoiceModel, + CustomerInvoiceTaxModel, + VerifactuRecordModel, +} from "../../../../common"; + +export class IssuedInvoiceRepository + extends SequelizeRepository + implements IIssuedInvoiceRepository +{ + constructor( + private readonly domainMapper: SequelizeIssuedInvoiceDomainMapper, + private readonly listMapper: SequelizeIssuedInvoiceListMapper, + database: Sequelize + ) { + super({ database }); + } + + /** + * + * Crea una nueva factura. + * + * @param invoice - El agregado a guardar. + * @param transaction - Transacción activa para la operación. + * @returns Result + */ + async create(invoice: IssuedInvoice, transaction?: Transaction): Promise> { + try { + const dtoResult = this.domainMapper.mapToPersistence(invoice); + + if (dtoResult.isFailure) { + return Result.fail(dtoResult.error); + } + + const dto = dtoResult.data; + const { id, items, taxes, verifactu, ...createPayload } = dto; + + // 1. Insertar cabecera + await CustomerInvoiceModel.create( + { + ...createPayload, + id, + }, + { transaction } + ); + + // 2. Inserta taxes de cabecera + if (Array.isArray(taxes) && taxes.length > 0) { + await CustomerInvoiceTaxModel.bulkCreate(taxes, { transaction }); + } + + // 3. Inserta items + sus taxes + if (Array.isArray(items) && items.length > 0) { + for (const item of items) { + await CustomerInvoiceItemModel.create(item, { transaction }); + } + } + + // 4. Inserta VerifactuRecord si existe + if (verifactu) { + await VerifactuRecordModel.create(verifactu, { transaction }); + } + + return Result.ok(); + } catch (err: unknown) { + return Result.fail(translateSequelizeError(err)); + } + } + + /** + * + * Busca una factura por su identificador único. + * + * @param companyId - Identificador UUID de la empresa a la que pertenece la factura. + * @param id - UUID de la factura. + * @param transaction - Transacción activa para la operación. + * @param options - Opciones adicionales para la consulta (Sequelize FindOptions) + * @returns Result + */ + async getIssuedInvoiceByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction: Transaction, + options: FindOptions> = {} + ): Promise> { + const { CustomerModel } = this.database.models; + + try { + // Normalización defensiva de order/include + const normalizedOrder = Array.isArray(options.order) + ? options.order + : options.order + ? [options.order] + : []; + + const normalizedInclude = Array.isArray(options.include) + ? options.include + : options.include + ? [options.include] + : []; + + const mergedOptions: FindOptions> = { + ...options, + where: { + ...(options.where ?? {}), + id: id.toString(), + is_proforma: false, + company_id: companyId.toString(), + }, + order: [ + ...normalizedOrder, + [{ model: CustomerInvoiceItemModel, as: "items" }, "position", "ASC"], + ], + include: [ + ...normalizedInclude, + { + model: VerifactuRecordModel, + as: "verifactu", + required: false, + attributes: ["id", "estado", "url", "uuid", "qr"], + }, + { + model: CustomerModel, + as: "current_customer", + required: false, + }, + { + model: CustomerInvoiceItemModel, + as: "items", + required: false, + }, + { + model: CustomerInvoiceTaxModel, + as: "taxes", + required: false, + }, + ], + transaction, + }; + + const row = await CustomerInvoiceModel.findOne(mergedOptions); + + if (!row) { + return Result.fail(new EntityNotFoundError("CustomerInvoice", "id", id.toString())); + } + + const invoice = this.domainMapper.mapToDomain(row); + return invoice; + } catch (err: unknown) { + return Result.fail(translateSequelizeError(err)); + } + } + + /** + * + * Consulta facturas usando un objeto Criteria (filtros, orden, paginación). + * + * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. + * @param criteria - Criterios de búsqueda. + * @param transaction - Transacción activa para la operación. + * @returns Result + * + * @see Criteria + */ + public async findByCriteriaInCompany( + companyId: UniqueID, + criteria: Criteria, + transaction: Transaction, + options: FindOptions> = {} + ): Promise, Error>> { + const { CustomerModel } = this.database.models; + + try { + const converter = new CriteriaToSequelizeConverter(); + const query = converter.convert(criteria, { + searchableFields: ["invoice_number", "reference", "description"], + mappings: { + reference: "CustomerInvoiceModel.reference", + }, + allowedFields: ["invoice_date", "id", "created_at"], + enableFullText: true, + database: this.database, + strictMode: true, // fuerza error si ORDER BY no permitido + }); + + // Normalización defensiva de order/include + const normalizedOrder = Array.isArray(options.order) + ? options.order + : options.order + ? [options.order] + : []; + + const normalizedInclude = Array.isArray(options.include) + ? options.include + : options.include + ? [options.include] + : []; + + query.where = { + ...query.where, + ...(options.where ?? {}), + is_proforma: false, + company_id: companyId.toString(), + deleted_at: null, + }; + + query.order = [...(query.order as OrderItem[]), ...normalizedOrder]; + + query.include = [ + ...normalizedInclude, + { + model: VerifactuRecordModel, + as: "verifactu", + required: false, + attributes: ["id", "estado", "url", "uuid", "qr"], + }, + { + model: CustomerModel, + as: "current_customer", + required: false, // false => LEFT JOIN + attributes: [ + "name", + "trade_name", + "tin", + "street", + "street2", + "city", + "postal_code", + "province", + "country", + ], + }, + { + model: CustomerInvoiceTaxModel, + as: "taxes", + required: false, + separate: true, // => query aparte, devuelve siempre array + }, + ]; + + // Reemplazar findAndCountAll por findAll + count (más control y mejor rendimiento) + /*const { rows, count } = await CustomerInvoiceModel.findAndCountAll({ + ...query, + transaction, + });*/ + + const [rows, count] = await Promise.all([ + CustomerInvoiceModel.findAll({ + ...query, + transaction, + }), + CustomerInvoiceModel.count({ + where: query.where, + distinct: true, // evita duplicados por LEFT JOIN + transaction, + }), + ]); + + return this.listMapper.mapToDTOCollection(rows, count); + } catch (err: unknown) { + return Result.fail(translateSequelizeError(err)); + } + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/di/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/di/index.ts new file mode 100644 index 00000000..68753d1a --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/di/index.ts @@ -0,0 +1,2 @@ +export * from "./proforma-public-services"; +export * from "./proformas.di"; diff --git a/modules/customer-invoices/src/api/infrastructure/di/documents.di.ts b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-documents.di.ts similarity index 63% rename from modules/customer-invoices/src/api/infrastructure/di/documents.di.ts rename to modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-documents.di.ts index 857f7118..03839b84 100644 --- a/modules/customer-invoices/src/api/infrastructure/di/documents.di.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-documents.di.ts @@ -1,14 +1,14 @@ import { type ModuleParams, buildCoreDocumentsDI } from "@erp/core/api"; import { - IssuedInvoiceDocumentPipelineFactory, - type IssuedInvoiceDocumentPipelineFactoryDeps, + ProformaDocumentPipelineFactory, + type ProformaDocumentPipelineFactoryDeps, } from "../documents"; -export const buildIssuedInvoiceDocumentService = (params: ModuleParams) => { +export const buildproformaDocumentService = (params: ModuleParams) => { const { documentRenderers, documentSigning, documentStorage } = buildCoreDocumentsDI(params); - const pipelineDeps: IssuedInvoiceDocumentPipelineFactoryDeps = { + const pipelineDeps: ProformaDocumentPipelineFactoryDeps = { fastReportRenderer: documentRenderers.fastReportRenderer, // @@ -21,7 +21,7 @@ export const buildIssuedInvoiceDocumentService = (params: ModuleParams) => { templateResolver: documentRenderers.fastReportTemplateResolver, }; - const documentGeneratorPipeline = IssuedInvoiceDocumentPipelineFactory.create(pipelineDeps); + const documentGeneratorPipeline = ProformaDocumentPipelineFactory.create(pipelineDeps); return documentGeneratorPipeline; }; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-number-generator.di.ts b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-number-generator.di.ts new file mode 100644 index 00000000..c97a939a --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-number-generator.di.ts @@ -0,0 +1,5 @@ +import type { IProformaNumberGenerator } from "../../../application"; +import { SequelizeProformaNumberGenerator } from "../persistence"; + +export const buildProformaNumberGenerator = (): IProformaNumberGenerator => + new SequelizeProformaNumberGenerator(); 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 new file mode 100644 index 00000000..972eb3ab --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-public-services.ts @@ -0,0 +1,24 @@ +import type { ProformasInternalDeps } from "./proformas.di"; + +export type ProformasServicesDeps = { + services: { + listIssuedInvoices: (filters: unknown, context: unknown) => null; + getIssuedInvoiceById: (id: unknown, context: unknown) => null; + generateIssuedInvoiceReport: (id: unknown, options: unknown, context: unknown) => null; + }; +}; + +export function buildProformaServices(deps: ProformasInternalDeps): ProformasServicesDeps { + return { + services: { + listIssuedInvoices: (filters, context) => null, + //internal.useCases.listIssuedInvoices().execute(filters, context), + + getIssuedInvoiceById: (id, context) => null, + //internal.useCases.getIssuedInvoiceById().execute(id, context), + + generateIssuedInvoiceReport: (id, options, context) => null, + //internal.useCases.reportIssuedInvoice().execute(id, options, context), + }, + }; +} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-repositories.di.ts b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-repositories.di.ts new file mode 100644 index 00000000..851c162a --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-repositories.di.ts @@ -0,0 +1,19 @@ +import { SpainTaxCatalogProvider } from "@erp/core"; +import type { Sequelize } from "sequelize"; + +import { + ProformaRepository, + SequelizeProformaDomainMapper, + SequelizeProformaListMapper, +} from "../persistence"; + +export const buildProformaRepository = (database: Sequelize) => { + const taxCatalog = SpainTaxCatalogProvider(); + + const domainMapper = new SequelizeProformaDomainMapper({ + taxCatalog, + }); + const listMapper = new SequelizeProformaListMapper(); + + return new ProformaRepository(domainMapper, listMapper, database); +}; 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 new file mode 100644 index 00000000..64d567c9 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts @@ -0,0 +1,181 @@ +import { + InMemoryMapperRegistry, + InMemoryPresenterRegistry, + type ModuleParams, + buildTransactionManager, +} from "@erp/core/api"; +import { buildProformaCreator } from "@erp/customer-invoices/api/application/proformas/di/proforma-creator.di"; + +import { + type GetProformaByIdUseCase, + buildGetProformaByIdUseCase, + buildListProformasUseCase, + buildProformaFinder, + buildReportProformaUseCase, +} from "../../../application"; +import { buildProformaSnapshotBuilders } from "../../../application/issued-invoices"; +import { + ChangeStatusProformaUseCase, + CreateProformaUseCase, + CustomerInvoiceApplicationService, + DeleteProformaUseCase, + GetProformaUseCase, + IssueProformaUseCase, + ListProformasUseCase, + ProformaFullPresenter, + ProformaListPresenter, + ReportProformaUseCase, + UpdateProformaUseCase, +} from "../application"; +import { + ProformaItemsReportPresenter, + ProformaReportPresenter, + ProformaTaxesReportPresenter, +} from "../application/snapshot-builders/reports"; + +import { SequelizeInvoiceNumberGenerator } from "./persistence/sequelize"; +import { + CustomerInvoiceDomainMapper, + CustomerInvoiceListMapper, +} from "./persistence/sequelize/mappers"; +import { buildProformaDocumentService } from "./proforma-documents.di"; +import { buildProformaNumberGenerator } from "./proforma-number-generator.di"; +import { buildProformaRepository } from "./proforma-repositories.di"; + +export type ProformasInternalDeps = { + useCases: { + listProformas: () => ListProformasUseCase; + getProformaById: () => GetProformaByIdUseCase; + reportProforma: () => ReportProformaUseCase; + + /*createProforma: () => CreateProformaUseCase; + updateProforma: () => UpdateProformaUseCase; + deleteProforma: () => DeleteProformaUseCase; + issueProforma: () => IssueProformaUseCase; + changeStatusProforma: () => ChangeStatusProformaUseCase;*/ + }; +}; + +export function buildProformasDependencies(params: ModuleParams): ProformasInternalDeps { + const { database, env } = params; + + // Infrastructure + const transactionManager = buildTransactionManager(database); + const repository = buildProformaRepository(database); + const numberService = buildProformaNumberGenerator(); + + // Application helpers + + const finder = buildProformaFinder(repository); + const creator = buildProformaCreator(numberService, repository); + + const snapshotBuilders = buildProformaSnapshotBuilders(); + const documentGeneratorPipeline = buildProformaDocumentService(params); + + // Internal use cases (factories) + return { + useCases: { + listProformas: () => + buildListProformasUseCase({ + finder, + itemSnapshotBuilder: snapshotBuilders.list, + transactionManager, + }), + + getProformaById: () => + buildGetProformaByIdUseCase({ + finder, + fullSnapshotBuilder: snapshotBuilders.full, + transactionManager, + }), + + reportProforma: () => + buildReportProformaUseCase({ + finder, + fullSnapshotBuilder: snapshotBuilders.full, + reportSnapshotBuilder: snapshotBuilders.report, + documentService: documentGeneratorPipeline, + transactionManager, + }), + + /*createProforma: () => + buildCreateProformaUseCase({ + creator, + fullSnapshotBuilder: snapshotBuilders.full, + transactionManager, + }),*/ + }, + }; +} + +const mapperRegistry = new InMemoryMapperRegistry(); +mapperRegistry + .registerDomainMapper( + { resource: "customer-invoice" }, + new CustomerInvoiceDomainMapper({ taxCatalog: catalogs.taxes }) + ) + .registerQueryMappers([ + { + key: { resource: "customer-invoice", query: "LIST" }, + mapper: new CustomerInvoiceListMapper(), + }, + ]); + +// Repository & Services +const numberGenerator = new SequelizeInvoiceNumberGenerator(); + +/** Aplicación */ +const appService = new CustomerInvoiceApplicationService(repository, numberGenerator); + +// Presenter Registry +const presenterRegistry = new InMemoryPresenterRegistry(); +presenterRegistry.registerPresenters([ + // FULL + { + key: { resource: "proforma-items", projection: "FULL" }, + presenter: new ProformaItemsFullPresenter(presenterRegistry), + }, + { + key: { resource: "proforma-recipient", projection: "FULL" }, + presenter: new ProformaRecipientFullPresenter(presenterRegistry), + }, + { + key: { resource: "proforma", projection: "FULL" }, + presenter: new ProformaFullPresenter(presenterRegistry), + }, + + // LIST + { + key: { resource: "proforma", projection: "LIST" }, + presenter: new ProformaListPresenter(presenterRegistry), + }, + + // REPORT + { + key: { resource: "proforma", projection: "REPORT" }, + presenter: new ProformaReportPresenter(presenterRegistry), + }, + { + key: { resource: "proforma-taxes", projection: "REPORT" }, + presenter: new ProformaTaxesReportPresenter(presenterRegistry), + }, + { + key: { resource: "proforma-items", projection: "REPORT" }, + presenter: new ProformaItemsReportPresenter(presenterRegistry), + }, +]); + +const useCases: ProformasDeps["useCases"] = { + // Proformas + list_proformas: () => new ListProformasUseCase(appService, transactionManager, presenterRegistry), + get_proforma: () => new GetProformaUseCase(appService, transactionManager, presenterRegistry), + create_proforma: () => + new CreateProformaUseCase(appService, transactionManager, presenterRegistry, catalogs.taxes), + update_proforma: () => + new UpdateProformaUseCase(appService, transactionManager, presenterRegistry), + delete_proforma: () => new DeleteProformaUseCase(appService, transactionManager), + report_proforma: () => + new ReportProformaUseCase(appService, transactionManager, presenterRegistry), + issue_proforma: () => new IssueProformaUseCase(appService, transactionManager, presenterRegistry), + changeStatus_proforma: () => new ChangeStatusProformaUseCase(appService, transactionManager), +}; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/documents/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/documents/index.ts new file mode 100644 index 00000000..5f7e3a8d --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/documents/index.ts @@ -0,0 +1,2 @@ +export * from "./pipelines"; +export * from "./renderers"; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/documents/pipelines/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/documents/pipelines/index.ts new file mode 100644 index 00000000..d368a4ef --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/documents/pipelines/index.ts @@ -0,0 +1 @@ +export * from "./proforma-document-pipeline-factory.ts"; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/documents/pipelines/proforma-document-pipeline-factory.ts b/modules/customer-invoices/src/api/infrastructure/proformas/documents/pipelines/proforma-document-pipeline-factory.ts new file mode 100644 index 00000000..5bcf6afb --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/documents/pipelines/proforma-document-pipeline-factory.ts @@ -0,0 +1,72 @@ +import { + DocumentGenerationService, + DocumentPostProcessorChain, + type FastReportRenderer, + type FastReportTemplateResolver, + type IDocumentPostProcessor, + type IDocumentSideEffect, + type IDocumentSigningService, + type IDocumentStorage, + type ISigningContextResolver, +} from "@erp/core/api"; + +import { + type ProformaDocumentGeneratorService, + ProformaDocumentMetadataFactory, + ProformaDocumentPropertiesFactory, + type ProformaReportSnapshot, +} from "../../../../application"; +import { DigitalSignaturePostProcessor } from "../post-processors"; +import { ProformaSignedDocumentCachePreProcessor } from "../pre-processors"; +import { ProformaDocumentRenderer } from "../renderers"; +import { PersistProformaDocumentSideEffect } from "../side-effects"; + +/** + * Factory de pipeline de generación de documentos + * para facturas emitidas (Issued Invoice). + */ + +export interface ProformaDocumentPipelineFactoryDeps { + // Core / Infra + fastReportRenderer: FastReportRenderer; + templateResolver: FastReportTemplateResolver; + + signingContextResolver: ISigningContextResolver; + documentSigningService: IDocumentSigningService; + + documentStorage: IDocumentStorage; +} + +export class ProformaDocumentPipelineFactory { + static create(deps: ProformaDocumentPipelineFactoryDeps): ProformaDocumentGeneratorService { + // 1. Pre-processors (cache firmado) + const preProcessors = [new ProformaSignedDocumentCachePreProcessor(deps.documentStorage)]; + + // 2. Renderer (FastReport) + const renderer = new ProformaDocumentRenderer(deps.fastReportRenderer, deps.templateResolver); + + // 3) Metadata and properties factory (Application) + const metadataFactory = new ProformaDocumentMetadataFactory(); + const propertiesFactory = new ProformaDocumentPropertiesFactory(); + + // 3) Firma real (Core / Infra) + const postProcessor: IDocumentPostProcessor = new DocumentPostProcessorChain([ + new DigitalSignaturePostProcessor(deps.signingContextResolver, deps.documentSigningService), + ]); + + // 4. Side-effects (persistencia best-effort) + const sideEffects: IDocumentSideEffect[] = [ + new PersistProformaDocumentSideEffect(deps.documentStorage), + ]; + + // 5. Pipeline final + return new DocumentGenerationService({ + renderer, + preProcessors, + postProcessor, + sideEffects, + metadataFactory, + propertiesFactory, + }); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/documents/post-processors/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/documents/post-processors/index.ts new file mode 100644 index 00000000..82543f53 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/documents/post-processors/index.ts @@ -0,0 +1 @@ +export * from "./digital-signature-post-processor"; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/documents/pre-processors/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/documents/pre-processors/index.ts new file mode 100644 index 00000000..5ed1dd1b --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/documents/pre-processors/index.ts @@ -0,0 +1 @@ +export * from "./proforma-document-cache-pre-processor"; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/documents/pre-processors/proforma-document-cache-pre-processor.ts b/modules/customer-invoices/src/api/infrastructure/proformas/documents/pre-processors/proforma-document-cache-pre-processor.ts new file mode 100644 index 00000000..ffdbc011 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/documents/pre-processors/proforma-document-cache-pre-processor.ts @@ -0,0 +1,71 @@ +import { + DocumentStorageKeyFactory, + type IDocument, + type IDocumentMetadata, + type IDocumentPreProcessor, + type IDocumentStorage, + logger, +} from "@erp/core/api"; + +/** + * Pre-processor de cache técnico para documentos firmados + * de facturas emitidas. + * + * - Best-effort + * - Nunca rompe el flujo + * - Invalida cache corrupto + */ +export class ProformaSignedDocumentCachePreProcessor implements IDocumentPreProcessor { + constructor(private readonly docStorage: IDocumentStorage) {} + + async tryResolve(metadata: IDocumentMetadata): Promise { + const metadataRecord = metadata as unknown as Record; + try { + const { paths, storageKey } = DocumentStorageKeyFactory.fromMetadataRecord(metadataRecord); + + if (!storageKey) { + return null; + } + + const exists = await this.docStorage.existsKeyStorage(storageKey, paths); + + if (!exists) { + return null; + } + + logger.info(`✅ Found Server cached document for key ${storageKey}`); + + const document = await this.docStorage.readDocument(storageKey, paths); + + if (!this.isValid(document)) { + logger.warn(`Corrupted or invalid cached document for key ${storageKey}`, { + label: "ProformaSignedDocumentCachePreProcessor", + }); + return null; + } + + return document; + } catch { + // best-effort: cualquier fallo se trata como cache miss + return null; + } + } + + /** + * Validación mínima de integridad. + * No valida firma criptográfica. + */ + private isValid(document: IDocument | null): boolean { + if (!document) return false; + + if (!document.payload || document.payload.length === 0) { + return false; + } + + if (document.mimeType !== "application/pdf") { + return false; + } + + return true; + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/documents/renderers/fastreport/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/documents/renderers/fastreport/index.ts new file mode 100644 index 00000000..72949b33 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/documents/renderers/fastreport/index.ts @@ -0,0 +1 @@ +export * from "./proforma-document-renderer.ts"; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/documents/renderers/fastreport/proforma-document-renderer.ts b/modules/customer-invoices/src/api/infrastructure/proformas/documents/renderers/fastreport/proforma-document-renderer.ts new file mode 100644 index 00000000..fd715298 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/documents/renderers/fastreport/proforma-document-renderer.ts @@ -0,0 +1,72 @@ +import type { + FastReportRenderer, + FastReportTemplateResolver, + IDocument, + IDocumentProperties, + IDocumentRenderer, +} from "@erp/core/api"; + +import type { ProformaReportSnapshot } from "../../../../../application"; + +/** + * Adaptador Application → Infra para la generación del documento + * PDF de una factura emitida (Issued Invoice). + * + * - Recibe snapshot de report + * - Invoca FastReportRenderer + * - Devuelve IDocument + * + * NO captura errores: FastReportError se propaga. + */ + +export type ProformaDocumentRenderParams = { + companySlug: string; + format: string; + languageCode: string; + filename: string; + mimeType: string; + properties: IDocumentProperties; +}; + +export class ProformaDocumentRenderer implements IDocumentRenderer { + constructor( + private readonly fastReportRenderer: FastReportRenderer, + private readonly templateResolver: FastReportTemplateResolver + ) {} + + async render( + snapshot: ProformaReportSnapshot, + params: ProformaDocumentRenderParams + ): Promise { + const { companySlug, format, languageCode, filename, mimeType, properties } = params; + + // Template + const templatePath = this.templateResolver.resolveTemplatePath({ + module: "customer-invoices", + companySlug, + languageCode, + templateFilename: "issued-invoice.frx", + }); + + const output = await this.fastReportRenderer.render({ + templatePath, + inputData: snapshot, + format, + properties, + }); + + return { + payload: this.normalizePayload(output.payload), + mimeType, + filename, + }; + } + + /** + * Normaliza la salida de FastReport a Buffer. + * FastReport puede devolver string o Buffer. + */ + private normalizePayload(payload: Buffer | string): Buffer { + return Buffer.isBuffer(payload) ? payload : Buffer.from(payload); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/documents/renderers/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/documents/renderers/index.ts new file mode 100644 index 00000000..3d635d3a --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/documents/renderers/index.ts @@ -0,0 +1 @@ +export * from "./fastreport"; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/documents/side-effects/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/documents/side-effects/index.ts new file mode 100644 index 00000000..75010823 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/documents/side-effects/index.ts @@ -0,0 +1 @@ +export * from "./persist-proforma-document-side-effect"; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/documents/side-effects/persist-proforma-document-side-effect.ts b/modules/customer-invoices/src/api/infrastructure/proformas/documents/side-effects/persist-proforma-document-side-effect.ts new file mode 100644 index 00000000..a032cdbb --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/documents/side-effects/persist-proforma-document-side-effect.ts @@ -0,0 +1,27 @@ +import type { + IDocument, + IDocumentMetadata, + IDocumentSideEffect, + IDocumentStorage, +} from "@erp/core/api"; + +/** + * Side-effect de persistencia best-effort del documento final + * de una factura emitida. + * + * - Nunca rompe el flujo + * - Usa cacheKey/metadata para decidir la clave + */ +export class PersistProformaDocumentSideEffect implements IDocumentSideEffect { + constructor(private readonly storage: IDocumentStorage) {} + + async execute(document: IDocument, metadata: IDocumentMetadata): Promise { + // Si no hay cacheKey, no se persiste + if (!metadata.storageKey) { + return; + } + + // Persistencia best-effort + await this.storage.saveDocument(document, metadata as unknown as Record); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/index.ts new file mode 100644 index 00000000..e8fe393b --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/index.ts @@ -0,0 +1,3 @@ +export * from "./di"; +export * from "./documents"; +export * from "./persistence"; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/index.ts new file mode 100644 index 00000000..62f8ac11 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/index.ts @@ -0,0 +1 @@ +export * from "./sequelize"; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/index.ts new file mode 100644 index 00000000..0c2e706a --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/index.ts @@ -0,0 +1,3 @@ +export * from "./mappers"; +export * from "./repositories"; +export * from "./services"; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/index.ts new file mode 100644 index 00000000..4900d86b --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/index.ts @@ -0,0 +1 @@ +export * from "./sequelize-proforma-domain.mapper"; 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 new file mode 100644 index 00000000..b4ffbfba --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-domain.mapper.ts @@ -0,0 +1,383 @@ +import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api"; +import { + CurrencyCode, + LanguageCode, + Percentage, + TextValue, + UniqueID, + UtcDate, + ValidationErrorCollection, + type ValidationErrorDetail, + extractOrPushError, + maybeFromNullableVO, + toNullable, +} from "@repo/rdx-ddd"; +import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils"; + +import type { IProformaDomainMapper } from "../../../../../../application"; +import { + InvoiceNumber, + InvoicePaymentMethod, + InvoiceSerie, + InvoiceStatus, + Proforma, + ProformaItems, + type ProformaProps, +} from "../../../../../../domain"; +import type { + CustomerInvoiceCreationAttributes, + CustomerInvoiceModel, +} from "../../../../../common"; + +import { SequelizeProformaItemDomainMapper } from "./sequelize-proforma-item-domain.mapper"; +import { SequelizeProformaRecipientDomainMapper } from "./sequelize-proforma-recipient-domain.mapper"; +import { SequelizeProformaTaxesDomainMapper } from "./sequelize-proforma-taxes-domain.mapper"; + +export class SequelizeProformaDomainMapper + extends SequelizeDomainMapper + implements IProformaDomainMapper +{ + private _itemsMapper: SequelizeProformaItemDomainMapper; + private _recipientMapper: SequelizeProformaRecipientDomainMapper; + private _taxesMapper: SequelizeProformaTaxesDomainMapper; + + constructor(params: MapperParamsType) { + super(); + + this._itemsMapper = new SequelizeProformaItemDomainMapper(params); + this._recipientMapper = new SequelizeProformaRecipientDomainMapper(); + this._taxesMapper = new SequelizeProformaTaxesDomainMapper(); + } + + private _mapAttributesToDomain(source: CustomerInvoiceModel, params?: MapperParamsType) { + const { errors } = params as { + errors: ValidationErrorDetail[]; + }; + + const invoiceId = extractOrPushError(UniqueID.create(source.id), "id", errors); + const companyId = extractOrPushError(UniqueID.create(source.company_id), "company_id", errors); + + const customerId = extractOrPushError( + UniqueID.create(source.customer_id), + "customer_id", + errors + ); + + const isProforma = Boolean(source.is_proforma); + + const proformaId = extractOrPushError( + maybeFromNullableVO(source.proforma_id, (v) => UniqueID.create(v)), + "proforma_id", + errors + ); + + const status = extractOrPushError(InvoiceStatus.create(source.status), "status", errors); + + const series = extractOrPushError( + maybeFromNullableVO(source.series, (v) => InvoiceSerie.create(v)), + "series", + errors + ); + + const invoiceNumber = extractOrPushError( + InvoiceNumber.create(source.invoice_number), + "invoice_number", + errors + ); + + // Fechas + const invoiceDate = extractOrPushError( + UtcDate.createFromISO(source.invoice_date), + "invoice_date", + errors + ); + + const operationDate = extractOrPushError( + maybeFromNullableVO(source.operation_date, (v) => UtcDate.createFromISO(v)), + "operation_date", + errors + ); + + // Idioma / divisa + const languageCode = extractOrPushError( + LanguageCode.create(source.language_code), + "language_code", + errors + ); + + const currencyCode = extractOrPushError( + CurrencyCode.create(source.currency_code), + "currency_code", + errors + ); + + // Textos opcionales + const reference = extractOrPushError( + maybeFromNullableVO(source.reference, (value) => Result.ok(String(value))), + "reference", + errors + ); + + const description = extractOrPushError( + maybeFromNullableVO(source.description, (value) => Result.ok(String(value))), + "description", + errors + ); + + const notes = extractOrPushError( + maybeFromNullableVO(source.notes, (value) => TextValue.create(value)), + "notes", + errors + ); + + // Método de pago (VO opcional con id + descripción) + let paymentMethod = Maybe.none(); + + if (!isNullishOrEmpty(source.payment_method_id)) { + const paymentId = extractOrPushError( + UniqueID.create(String(source.payment_method_id)), + "paymentMethod.id", + errors + ); + + const paymentVO = extractOrPushError( + InvoicePaymentMethod.create( + { paymentDescription: String(source.payment_method_description ?? "") }, + paymentId ?? undefined + ), + "payment_method_description", + errors + ); + + if (paymentVO) { + paymentMethod = Maybe.some(paymentVO); + } + } + + // % descuento (VO) + const discountPercentage = extractOrPushError( + Percentage.create({ + value: Number(source.discount_percentage_value ?? 0), + scale: Number(source.discount_percentage_scale ?? 2), + }), + "discount_percentage_value", + errors + ); + + return { + invoiceId, + companyId, + customerId, + isProforma, + proformaId, + status, + series, + invoiceNumber, + invoiceDate, + operationDate, + reference, + description, + notes, + languageCode, + currencyCode, + discountPercentage, + paymentMethod, + }; + } + + public mapToDomain( + source: CustomerInvoiceModel, + params?: MapperParamsType + ): Result { + try { + const errors: ValidationErrorDetail[] = []; + + // 1) Valores escalares (atributos generales) + const attributes = this._mapAttributesToDomain(source, { errors, ...params }); + + // 2) Recipient (snapshot en la factura o include) + const recipientResult = this._recipientMapper.mapToDomain(source, { + errors, + attributes, + ...params, + }); + + // 3) Items (colección) + const itemsResults = this._itemsMapper.mapToDomainCollection( + source.items, + source.items.length, + { + errors, + attributes, + ...params, + } + ); + + // 4) Si hubo errores de mapeo, devolvemos colección de validación + if (errors.length > 0) { + return Result.fail( + new ValidationErrorCollection("Customer invoice mapping failed [mapToDomain]", errors) + ); + } + + // 6) Construcción del agregado (Dominio) + + const items = ProformaItems.create({ + languageCode: attributes.languageCode!, + currencyCode: attributes.currencyCode!, + globalDiscountPercentage: attributes.discountPercentage!, + items: itemsResults.data.getAll(), + }); + + const invoiceProps: ProformaProps = { + companyId: attributes.companyId!, + + isProforma: attributes.isProforma, + status: attributes.status!, + series: attributes.series!, + invoiceNumber: attributes.invoiceNumber!, + invoiceDate: attributes.invoiceDate!, + operationDate: attributes.operationDate!, + + customerId: attributes.customerId!, + recipient: recipientResult.data, + + reference: attributes.reference!, + description: attributes.description!, + notes: attributes.notes!, + + languageCode: attributes.languageCode!, + currencyCode: attributes.currencyCode!, + + discountPercentage: attributes.discountPercentage!, + + paymentMethod: attributes.paymentMethod!, + + items, + }; + + const createResult = Proforma.create(invoiceProps, attributes.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); + } catch (err: unknown) { + return Result.fail(err as Error); + } + } + + public mapToPersistence( + source: Proforma, + params?: MapperParamsType + ): Result { + const errors: ValidationErrorDetail[] = []; + + // 1) Items + const itemsResult = this._itemsMapper.mapToPersistenceArray(source.items, { + errors, + parent: source, + ...params, + }); + + if (itemsResult.isFailure) { + errors.push({ + path: "items", + message: itemsResult.error.message, + }); + } + + // 2) Taxes + const taxesResult = this._taxesMapper.mapToPersistenceArray(source.getTaxes(), { + errors, + parent: source, + ...params, + }); + + if (taxesResult.isFailure) { + errors.push({ + path: "taxes", + message: taxesResult.error.message, + }); + } + + // 3) Cliente + const recipient = this._recipientMapper.mapToPersistence(source.recipient, { + errors, + parent: source, + ...params, + }); + + // 4) Si hubo errores de mapeo, devolvemos colección de validación + if (errors.length > 0) { + return Result.fail( + new ValidationErrorCollection("Customer invoice mapping to persistence failed", errors) + ); + } + + const items = itemsResult.data; + const taxes = taxesResult.data; + + const allAmounts = source.calculateAllAmounts(); // Da los totales ya calculados + + const invoiceValues: Partial = { + // Identificación + id: source.id.toPrimitive(), + company_id: source.companyId.toPrimitive(), + + // Flags / estado / serie / número + is_proforma: source.isProforma, + status: source.status.toPrimitive(), + series: toNullable(source.series, (v) => v.toPrimitive()), + invoice_number: source.invoiceNumber.toPrimitive(), + + invoice_date: source.invoiceDate.toPrimitive(), + operation_date: toNullable(source.operationDate, (v) => v.toPrimitive()), + language_code: source.languageCode.toPrimitive(), + currency_code: source.currencyCode.toPrimitive(), + + reference: toNullable(source.reference, (reference) => reference), + description: toNullable(source.description, (description) => description), + notes: toNullable(source.notes, (v) => v.toPrimitive()), + + subtotal_amount_value: allAmounts.subtotalAmount.value, + subtotal_amount_scale: allAmounts.subtotalAmount.scale, + + discount_percentage_value: source.discountPercentage.toPrimitive().value, + discount_percentage_scale: source.discountPercentage.toPrimitive().scale, + + discount_amount_value: allAmounts.globalDiscountAmount.value, + discount_amount_scale: allAmounts.globalDiscountAmount.scale, + + taxable_amount_value: allAmounts.taxableAmount.value, + taxable_amount_scale: allAmounts.taxableAmount.scale, + + taxes_amount_value: allAmounts.taxesAmount.value, + taxes_amount_scale: allAmounts.taxesAmount.scale, + + total_amount_value: allAmounts.totalAmount.value, + total_amount_scale: allAmounts.totalAmount.scale, + + payment_method_id: toNullable(source.paymentMethod, (payment) => payment.toObjectString().id), + payment_method_description: toNullable( + source.paymentMethod, + (payment) => payment.toObjectString().payment_description + ), + + customer_id: source.customerId.toPrimitive(), + ...recipient, + + taxes, + items, + }; + + return Result.ok( + invoiceValues as CustomerInvoiceCreationAttributes + ); + } +} 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 new file mode 100644 index 00000000..9bff6dc3 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-item-domain.mapper.ts @@ -0,0 +1,287 @@ +import type { JsonTaxCatalogProvider } from "@erp/core"; +import { type MapperParamsType, SequelizeDomainMapper, Tax } from "@erp/core/api"; +import { + UniqueID, + ValidationErrorCollection, + type ValidationErrorDetail, + extractOrPushError, + maybeFromNullableVO, + toNullable, +} from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import { + ItemAmount, + ItemDescription, + ItemDiscount, + ItemQuantity, + ItemTaxGroup, + type Proforma, + ProformaItem, + type ProformaItemProps, + type ProformaProps, +} from "../../../../../../domain"; +import type { + CustomerInvoiceItemCreationAttributes, + CustomerInvoiceItemModel, +} from "../../../../../common"; + +export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper< + CustomerInvoiceItemModel, + CustomerInvoiceItemCreationAttributes, + ProformaItem +> { + private _taxCatalog!: JsonTaxCatalogProvider; + + constructor(params: MapperParamsType) { + super(); + const { taxCatalog } = params as { + taxCatalog: JsonTaxCatalogProvider; + }; + + if (!taxCatalog) { + throw new Error('taxCatalog not defined ("ProformaItemDomainMapper")'); + } + + this._taxCatalog = taxCatalog; + } + + private mapAttributesToDomain( + source: CustomerInvoiceItemModel, + params?: MapperParamsType + ): Partial & { itemId?: UniqueID } { + const { errors, index, attributes } = params as { + index: number; + errors: ValidationErrorDetail[]; + attributes: Partial; + }; + + const itemId = extractOrPushError( + UniqueID.create(source.item_id), + `items[${index}].item_id`, + errors + ); + + const description = extractOrPushError( + maybeFromNullableVO(source.description, (v) => ItemDescription.create(v)), + `items[${index}].description`, + errors + ); + + const quantity = extractOrPushError( + maybeFromNullableVO(source.quantity_value, (v) => ItemQuantity.create({ value: v })), + `items[${index}].quantity`, + errors + ); + + const unitAmount = extractOrPushError( + maybeFromNullableVO(source.unit_amount_value, (value) => + ItemAmount.create({ value, currency_code: attributes.currencyCode?.code }) + ), + `items[${index}].unit_amount`, + errors + ); + + const discountPercentage = extractOrPushError( + maybeFromNullableVO(source.discount_percentage_value, (v) => + ItemDiscount.create({ value: v }) + ), + `items[${index}].discount_percentage`, + errors + ); + + const globalDiscountPercentage = extractOrPushError( + maybeFromNullableVO(source.global_discount_percentage_value, (v) => + ItemDiscount.create({ value: v }) + ), + `items[${index}].discount_percentage`, + errors + ); + + const iva = extractOrPushError( + maybeFromNullableVO(source.iva_code, (code) => Tax.createFromCode(code, this._taxCatalog)), + `items[${index}].iva_code`, + errors + ); + + const rec = extractOrPushError( + maybeFromNullableVO(source.rec_code, (code) => Tax.createFromCode(code, this._taxCatalog)), + `items[${index}].rec_code`, + errors + ); + + const retention = extractOrPushError( + maybeFromNullableVO(source.retention_code, (code) => + Tax.createFromCode(code, this._taxCatalog) + ), + `items[${index}].retention_code`, + errors + ); + + return { + itemId, + + languageCode: attributes.languageCode, + currencyCode: attributes.currencyCode, + description, + quantity, + unitAmount, + itemDiscountPercentage: discountPercentage, + globalDiscountPercentage, + + taxes: ItemTaxGroup.create({ + iva: iva!, + rec: rec!, + retention: retention!, + }).data, + }; + } + + public mapToDomain( + source: CustomerInvoiceItemModel, + params?: MapperParamsType + ): Result { + const { errors, index } = params as { + index: number; + errors: ValidationErrorDetail[]; + attributes: Partial; + }; + + // 1) Valores escalares (atributos generales) + const attributes = this.mapAttributesToDomain(source, params); + + // Si hubo errores de mapeo, devolvemos colección de validación + if (errors.length > 0) { + return Result.fail( + new ValidationErrorCollection("Customer invoice item mapping failed [mapToDomain]", errors) + ); + } + + // 2) Construcción del elemento de dominio + const createResult = ProformaItem.create( + { + languageCode: attributes.languageCode!, + currencyCode: attributes.currencyCode!, + description: attributes.description!, + quantity: attributes.quantity!, + unitAmount: attributes.unitAmount!, + itemDiscountPercentage: attributes.itemDiscountPercentage!, + globalDiscountPercentage: attributes.globalDiscountPercentage!, + taxes: attributes.taxes!, + }, + attributes.itemId + ); + + if (createResult.isFailure) { + return Result.fail( + new ValidationErrorCollection("Invoice item entity creation failed", [ + { path: `items[${index}]`, message: createResult.error.message }, + ]) + ); + } + + return createResult; + } + + public mapToPersistence( + source: ProformaItem, + params?: MapperParamsType + ): Result { + const { errors, index, parent } = params as { + index: number; + parent: Proforma; + errors: ValidationErrorDetail[]; + }; + + const allAmounts = source.calculateAllAmounts(); + const taxesAmounts = source.taxes.calculateAmounts(allAmounts.taxableAmount); + + return Result.ok({ + item_id: source.id.toPrimitive(), + invoice_id: parent.id.toPrimitive(), + position: index, + + description: toNullable(source.description, (v) => v.toPrimitive()), + + quantity_value: toNullable(source.quantity, (v) => v.toPrimitive().value), + quantity_scale: + toNullable(source.quantity, (v) => v.toPrimitive().scale) ?? ItemQuantity.DEFAULT_SCALE, + + unit_amount_value: toNullable(source.unitAmount, (v) => v.toPrimitive().value), + unit_amount_scale: + toNullable(source.unitAmount, (v) => v.toPrimitive().scale) ?? ItemAmount.DEFAULT_SCALE, + + subtotal_amount_value: allAmounts.subtotalAmount.value, + subtotal_amount_scale: allAmounts.subtotalAmount.scale, + + // + discount_percentage_value: toNullable( + source.itemDiscountPercentage, + (v) => v.toPrimitive().value + ), + discount_percentage_scale: + toNullable(source.itemDiscountPercentage, (v) => v.toPrimitive().scale) ?? + ItemDiscount.DEFAULT_SCALE, + + discount_amount_value: allAmounts.itemDiscountAmount.value, + discount_amount_scale: allAmounts.itemDiscountAmount.scale, + + // + global_discount_percentage_value: toNullable( + source.globalDiscountPercentage, + (v) => v.toPrimitive().value + ), + + global_discount_percentage_scale: + toNullable(source.globalDiscountPercentage, (v) => v.toPrimitive().scale) ?? + ItemDiscount.DEFAULT_SCALE, + + global_discount_amount_value: allAmounts.globalDiscountAmount.value, + global_discount_amount_scale: allAmounts.globalDiscountAmount.scale, + + // + total_discount_amount_value: allAmounts.totalDiscountAmount.value, + total_discount_amount_scale: allAmounts.totalDiscountAmount.scale, + + // + taxable_amount_value: allAmounts.taxableAmount.value, + taxable_amount_scale: allAmounts.taxableAmount.scale, + + // IVA + iva_code: toNullable(source.taxes.iva, (v) => v.code), + + iva_percentage_value: toNullable(source.taxes.iva, (v) => v.percentage.value), + iva_percentage_scale: toNullable(source.taxes.iva, (v) => v.percentage.scale) ?? 2, + + iva_amount_value: taxesAmounts.ivaAmount.value, + iva_amount_scale: taxesAmounts.ivaAmount.scale, + + // REC + rec_code: toNullable(source.taxes.rec, (v) => v.code), + + rec_percentage_value: toNullable(source.taxes.rec, (v) => v.percentage.value), + rec_percentage_scale: toNullable(source.taxes.rec, (v) => v.percentage.scale) ?? 2, + + rec_amount_value: taxesAmounts.recAmount.value, + rec_amount_scale: taxesAmounts.recAmount.scale, + + // RET + retention_code: toNullable(source.taxes.retention, (v) => v.code), + + retention_percentage_value: toNullable(source.taxes.retention, (v) => v.percentage.value), + retention_percentage_scale: + toNullable(source.taxes.retention, (v) => v.percentage.scale) ?? 2, + + retention_amount_value: taxesAmounts.retentionAmount.value, + retention_amount_scale: taxesAmounts.retentionAmount.scale, + + // + taxes_amount_value: allAmounts.taxesAmount.value, + taxes_amount_scale: allAmounts.taxesAmount.scale, + + // + total_amount_value: allAmounts.totalAmount.value, + total_amount_scale: allAmounts.totalAmount.scale, + }); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/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 new file mode 100644 index 00000000..877793ab --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-recipient-domain.mapper.ts @@ -0,0 +1,130 @@ +import type { MapperParamsType } from "@erp/core/api"; +import { + City, + Country, + Name, + PostalCode, + Province, + Street, + TINNumber, + ValidationErrorCollection, + type ValidationErrorDetail, + extractOrPushError, + maybeFromNullableVO, +} from "@repo/rdx-ddd"; +import { Maybe, Result } from "@repo/rdx-utils"; + +import { InvoiceRecipient, type ProformaProps } from "../../../../../../domain"; +import type { CustomerInvoiceModel } from "../../../../../common"; + +export class SequelizeProformaRecipientDomainMapper { + public mapToDomain( + source: CustomerInvoiceModel, + params?: MapperParamsType + ): Result, Error> { + /** + * En proforma -> datos de "current_customer" + */ + + const { errors, attributes } = params as { + errors: ValidationErrorDetail[]; + attributes: Partial; + }; + + const { isProforma } = attributes; + + if (isProforma && !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; + const _street2 = source.current_customer.street2; + const _city = source.current_customer.city; + const _postal_code = source.current_customer.postal_code; + const _province = source.current_customer.province; + const _country = source.current_customer.country; + + // Customer (snapshot) + const customerName = extractOrPushError(Name.create(_name!), "customer_name", errors); + + const customerTin = extractOrPushError(TINNumber.create(_tin!), "customer_tin", errors); + + const customerStreet = extractOrPushError( + maybeFromNullableVO(_street, (value) => Street.create(value)), + "customer_street", + errors + ); + + const customerStreet2 = extractOrPushError( + maybeFromNullableVO(_street2, (value) => Street.create(value)), + "customer_street2", + errors + ); + + const customerCity = extractOrPushError( + maybeFromNullableVO(_city, (value) => City.create(value)), + "customer_city", + errors + ); + + const customerProvince = extractOrPushError( + maybeFromNullableVO(_province, (value) => Province.create(value)), + "customer_province", + errors + ); + + const customerPostalCode = extractOrPushError( + maybeFromNullableVO(_postal_code, (value) => PostalCode.create(value)), + "customer_postal_code", + errors + ); + + const customerCountry = extractOrPushError( + maybeFromNullableVO(_country, (value) => Country.create(value)), + "customer_country", + errors + ); + + const createResult = InvoiceRecipient.create({ + name: customerName!, + tin: customerTin!, + street: customerStreet!, + street2: customerStreet2!, + city: customerCity!, + postalCode: customerPostalCode!, + province: customerProvince!, + country: customerCountry!, + }); + + if (createResult.isFailure) { + return Result.fail( + new ValidationErrorCollection("Invoice recipient entity creation failed", [ + { path: "recipient", message: createResult.error.message }, + ]) + ); + } + + return Result.ok(Maybe.some(createResult.data)); + } + + /** + * Para una proforma, el recipient no se persiste (todos los campos son null). + */ + mapToPersistence(source: Maybe, params?: MapperParamsType) { + return { + customer_tin: null, + customer_name: null, + customer_street: null, + customer_street2: null, + customer_city: null, + customer_province: null, + customer_postal_code: null, + customer_country: null, + }; + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-taxes-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-taxes-domain.mapper.ts new file mode 100644 index 00000000..682d1cca --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-taxes-domain.mapper.ts @@ -0,0 +1,94 @@ +import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api"; +import { UniqueID, type ValidationErrorDetail, toNullable } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { InvoiceTaxGroup, Proforma } from "../../../../../../domain"; +import type { + CustomerInvoiceTaxCreationAttributes, + CustomerInvoiceTaxModel, +} from "../../../../../common"; + +/** + * Mapper para customer_invoice_taxes + * + * Domina estructuras: + * { + * tax: Tax + * taxableAmount: ItemAmount + * taxesAmount: ItemAmount + * } + * + * Cada fila = un impuesto agregado en toda la factura. + */ +export class SequelizeProformaTaxesDomainMapper extends SequelizeDomainMapper< + CustomerInvoiceTaxModel, + CustomerInvoiceTaxCreationAttributes, + InvoiceTaxGroup +> { + public mapToDomain( + source: CustomerInvoiceTaxModel, + params?: MapperParamsType + ): Result { + throw new Error("Se calcula a partir de las líneas de detalle"); + } + + public mapToPersistence( + source: InvoiceTaxGroup, + params?: MapperParamsType + ): Result { + const { errors, parent } = params as { + parent: Proforma; + errors: ValidationErrorDetail[]; + }; + + try { + const { ivaAmount, recAmount, retentionAmount } = source.calculateAmounts(); + + const totalTaxes = ivaAmount.add(recAmount).add(retentionAmount); + + const dto: CustomerInvoiceTaxCreationAttributes = { + tax_id: UniqueID.generateNewID().toPrimitive(), + invoice_id: parent.id.toPrimitive(), + + // TAXABLE AMOUNT + taxable_amount_value: source.taxableAmount.value, + taxable_amount_scale: source.taxableAmount.scale, + + // IVA + iva_code: source.iva.code, + + iva_percentage_value: source.iva.value, + iva_percentage_scale: source.iva.scale, + + iva_amount_value: ivaAmount.value, + iva_amount_scale: ivaAmount.scale, + + // REC + rec_code: toNullable(source.rec, (v) => v.code), + + rec_percentage_value: toNullable(source.rec, (v) => v.percentage.value), + rec_percentage_scale: toNullable(source.rec, (v) => v.percentage.scale) ?? 2, + + rec_amount_value: recAmount.value, + rec_amount_scale: recAmount.scale, + + // RET + retention_code: toNullable(source.retention, (v) => v.code), + + retention_percentage_value: toNullable(source.retention, (v) => v.percentage.value), + retention_percentage_scale: toNullable(source.retention, (v) => v.percentage.scale) ?? 2, + + retention_amount_value: retentionAmount.value, + retention_amount_scale: retentionAmount.scale, + + // TOTAL + taxes_amount_value: totalTaxes.value, + taxes_amount_scale: totalTaxes.scale, + }; + + return Result.ok(dto); + } catch (error: unknown) { + return Result.fail(error as Error); + } + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/index.ts new file mode 100644 index 00000000..9b0ff906 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/index.ts @@ -0,0 +1,2 @@ +export * from "./domain"; +export * from "./list"; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/list/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/list/index.ts new file mode 100644 index 00000000..cf9c3925 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/list/index.ts @@ -0,0 +1 @@ +export * from "./sequelize-proforma.list.mapper"; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/list/sequelize-proforma-recipient.list.mapper.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/list/sequelize-proforma-recipient.list.mapper.ts new file mode 100644 index 00000000..488d90f5 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/list/sequelize-proforma-recipient.list.mapper.ts @@ -0,0 +1,124 @@ +import { + type IQueryMapperWithBulk, + type MapperParamsType, + SequelizeQueryMapper, +} from "@erp/core/api"; +import { + City, + Country, + Name, + PostalCode, + Province, + Street, + TINNumber, + ValidationErrorCollection, + type ValidationErrorDetail, + extractOrPushError, + maybeFromNullableVO, +} from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { ProformaListDTO } from "../../../../../../application"; +import { InvoiceRecipient } from "../../../../../../domain"; +import type { CustomerInvoiceModel } from "../../../../../common"; + +interface IInvoiceRecipientListMapper + extends IQueryMapperWithBulk {} + +export class SequelizeInvoiceRecipientListMapper + extends SequelizeQueryMapper + implements IInvoiceRecipientListMapper +{ + public mapToDTO( + raw: CustomerInvoiceModel, + params?: MapperParamsType + ): Result { + /** + * En proforma -> datos de "current_customer" + */ + + const { errors, attributes } = params as { + errors: ValidationErrorDetail[]; + attributes: Partial; + }; + + const { isProforma } = attributes; + + if (isProforma && !raw.current_customer) { + errors.push({ + path: "current_customer", + message: "Current customer not included in query (SequelizeInvoiceRecipientListMapper)", + }); + } + const _name = raw.current_customer.name; + const _tin = raw.current_customer.tin; + const _street = raw.current_customer.street; + const _street2 = raw.current_customer.street2; + const _city = raw.current_customer.city; + const _postal_code = raw.current_customer.postal_code; + const _province = raw.current_customer.province; + const _country = raw.current_customer.country; + + // Customer (snapshot) + const customerName = extractOrPushError(Name.create(_name!), "customer_name", errors); + + const customerTin = extractOrPushError(TINNumber.create(_tin!), "customer_tin", errors); + + const customerStreet = extractOrPushError( + maybeFromNullableVO(_street, (value) => Street.create(value)), + "customer_street", + errors + ); + + const customerStreet2 = extractOrPushError( + maybeFromNullableVO(_street2, (value) => Street.create(value)), + "customer_street2", + errors + ); + + const customerCity = extractOrPushError( + maybeFromNullableVO(_city, (value) => City.create(value)), + "customer_city", + errors + ); + + const customerProvince = extractOrPushError( + maybeFromNullableVO(_province, (value) => Province.create(value)), + "customer_province", + errors + ); + + const customerPostalCode = extractOrPushError( + maybeFromNullableVO(_postal_code, (value) => PostalCode.create(value)), + "customer_postal_code", + errors + ); + + const customerCountry = extractOrPushError( + maybeFromNullableVO(_country, (value) => Country.create(value)), + "customer_country", + errors + ); + + const createResult = InvoiceRecipient.create({ + name: customerName!, + tin: customerTin!, + street: customerStreet!, + street2: customerStreet2!, + city: customerCity!, + postalCode: customerPostalCode!, + province: customerProvince!, + country: customerCountry!, + }); + + if (createResult.isFailure) { + return Result.fail( + new ValidationErrorCollection("Invoice recipient entity creation failed", [ + { path: "recipient", message: createResult.error.message }, + ]) + ); + } + + return Result.ok(createResult.data); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/list/sequelize-proforma.list.mapper.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/list/sequelize-proforma.list.mapper.ts new file mode 100644 index 00000000..6a5d79f2 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/list/sequelize-proforma.list.mapper.ts @@ -0,0 +1,233 @@ +import { type MapperParamsType, SequelizeQueryMapper } from "@erp/core/api"; +import { + CurrencyCode, + LanguageCode, + Percentage, + UniqueID, + UtcDate, + ValidationErrorCollection, + type ValidationErrorDetail, + extractOrPushError, + maybeFromNullableVO, +} from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { IProformaListMapper, ProformaListDTO } from "../../../../../../application"; +import { + InvoiceAmount, + InvoiceNumber, + InvoiceSerie, + InvoiceStatus, +} from "../../../../../../domain"; +import type { CustomerInvoiceModel } from "../../../../../common"; + +import { SequelizeInvoiceRecipientListMapper } from "./sequelize-proforma-recipient.list.mapper"; + +export class SequelizeProformaListMapper + extends SequelizeQueryMapper + implements IProformaListMapper +{ + private _recipientMapper: SequelizeInvoiceRecipientListMapper; + + constructor() { + super(); + this._recipientMapper = new SequelizeInvoiceRecipientListMapper(); + } + + public mapToDTO( + raw: CustomerInvoiceModel, + params?: MapperParamsType + ): Result { + const errors: ValidationErrorDetail[] = []; + + // 1) Valores escalares (atributos generales) + const attributes = this.mapAttributesToDTO(raw, { errors, ...params }); + + // 2) Recipient (snapshot en la factura o include) + const recipientResult = this._recipientMapper.mapToDTO(raw, { + errors, + attributes, + ...params, + }); + + if (recipientResult.isFailure) { + errors.push({ + path: "recipient", + message: recipientResult.error.message, + }); + } + + // 4) Si hubo errores de mapeo, devolvemos colección de validación + if (errors.length > 0) { + return Result.fail( + new ValidationErrorCollection("Customer invoice mapping failed [mapToDTO]", errors) + ); + } + + return Result.ok({ + id: attributes.invoiceId!, + companyId: attributes.companyId!, + isProforma: attributes.isProforma, + status: attributes.status!, + series: attributes.series!, + invoiceNumber: attributes.invoiceNumber!, + invoiceDate: attributes.invoiceDate!, + operationDate: attributes.operationDate!, + + description: attributes.description!, + reference: attributes.description!, + + customerId: attributes.customerId!, + recipient: recipientResult.data, + + languageCode: attributes.languageCode!, + currencyCode: attributes.currencyCode!, + + discountPercentage: attributes.discountPercentage!, + subtotalAmount: attributes.subtotalAmount!, + discountAmount: attributes.discountAmount!, + taxableAmount: attributes.taxableAmount!, + taxesAmount: attributes.taxesAmount!, + totalAmount: attributes.totalAmount!, + }); + } + + private mapAttributesToDTO(raw: CustomerInvoiceModel, params?: MapperParamsType) { + const { errors } = params as { + errors: ValidationErrorDetail[]; + }; + + const invoiceId = extractOrPushError(UniqueID.create(raw.id), "id", errors); + const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors); + + const customerId = extractOrPushError(UniqueID.create(raw.customer_id), "customer_id", errors); + + const isProforma = Boolean(raw.is_proforma); + + const status = extractOrPushError(InvoiceStatus.create(raw.status), "status", errors); + + const series = extractOrPushError( + maybeFromNullableVO(raw.series, (value) => InvoiceSerie.create(value)), + "serie", + errors + ); + + const invoiceNumber = extractOrPushError( + InvoiceNumber.create(raw.invoice_number), + "invoice_number", + errors + ); + + const invoiceDate = extractOrPushError( + UtcDate.createFromISO(raw.invoice_date), + "invoice_date", + errors + ); + + const operationDate = extractOrPushError( + maybeFromNullableVO(raw.operation_date, (value) => UtcDate.createFromISO(value)), + "operation_date", + errors + ); + + const reference = extractOrPushError( + maybeFromNullableVO(raw.reference, (value) => Result.ok(String(value))), + "description", + errors + ); + + const description = extractOrPushError( + maybeFromNullableVO(raw.description, (value) => Result.ok(String(value))), + "description", + errors + ); + + const languageCode = extractOrPushError( + LanguageCode.create(raw.language_code), + "language_code", + errors + ); + + const currencyCode = extractOrPushError( + CurrencyCode.create(raw.currency_code), + "currency_code", + errors + ); + + const discountPercentage = extractOrPushError( + Percentage.create({ + value: raw.discount_percentage_value, + scale: raw.discount_percentage_scale, + }), + "discount_percentage_value", + errors + ); + + const subtotalAmount = extractOrPushError( + InvoiceAmount.create({ + value: raw.subtotal_amount_value, + currency_code: currencyCode?.code, + }), + "subtotal_amount_value", + errors + ); + + const discountAmount = extractOrPushError( + InvoiceAmount.create({ + value: raw.discount_amount_value, + currency_code: currencyCode?.code, + }), + "discount_amount_value", + errors + ); + + const taxableAmount = extractOrPushError( + InvoiceAmount.create({ + value: raw.taxable_amount_value, + currency_code: currencyCode?.code, + }), + "taxable_amount_value", + errors + ); + + const taxesAmount = extractOrPushError( + InvoiceAmount.create({ + value: raw.taxes_amount_value, + currency_code: currencyCode?.code, + }), + "taxes_amount_value", + errors + ); + + const totalAmount = extractOrPushError( + InvoiceAmount.create({ + value: raw.total_amount_value, + currency_code: currencyCode?.code, + }), + "total_amount_value", + errors + ); + + return { + invoiceId, + companyId, + customerId, + isProforma, + status, + series, + invoiceNumber, + invoiceDate, + operationDate, + reference, + description, + languageCode, + currencyCode, + discountPercentage, + subtotalAmount, + discountAmount, + taxableAmount, + taxesAmount, + totalAmount, + }; + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/index.ts new file mode 100644 index 00000000..3aae9573 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/index.ts @@ -0,0 +1 @@ +export * from "./proforma.repository"; diff --git a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/repositories/customer-invoice.repository.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts similarity index 54% rename from modules/customer-invoices/src/api/infrastructure/persistence/sequelize/repositories/customer-invoice.repository.ts rename to modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts index cab13543..c5b7552a 100644 --- a/modules/customer-invoices/src/api/infrastructure/persistence/sequelize/repositories/customer-invoice.repository.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/repositories/proforma.repository.ts @@ -7,83 +7,47 @@ import { import { type Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server"; import type { UniqueID } from "@repo/rdx-ddd"; import { type Collection, Result } from "@repo/rdx-utils"; -import type { FindOptions, InferAttributes, OrderItem, Transaction } from "sequelize"; +import type { FindOptions, InferAttributes, OrderItem, Sequelize, Transaction } from "sequelize"; -import type { - CustomerInvoice, - CustomerInvoiceStatus, - ICustomerInvoiceRepository, -} from "../../domain"; -import type { - CustomerInvoiceListDTO, - ICustomerInvoiceDomainMapper, - ICustomerInvoiceListMapper, -} from "../mappers"; +import type { IProformaRepository, ProformaListDTO } from "../../../../../application"; +import type { InvoiceStatus, Proforma } from "../../../../../domain"; import { CustomerInvoiceItemModel, CustomerInvoiceModel, CustomerInvoiceTaxModel, - VerifactuRecordModel, -} from "../models"; +} from "../../../../common"; +import type { SequelizeProformaDomainMapper, SequelizeProformaListMapper } from "../mappers"; -export class CustomerInvoiceRepository - extends SequelizeRepository - implements ICustomerInvoiceRepository +export class ProformaRepository + extends SequelizeRepository + implements IProformaRepository { - // Listado por tenant con criteria saneada - /* async searchInCompany(criteria: any, companyId: string): Promise<{ - rows: InvoiceListRow[]; - total: number; - limit: number; - offset: number; - }> { - const { where, order, limit, offset, attributes } = sanitizeListCriteria(criteria); - - // WHERE con scope de company - const scopedWhere = { ...where, company_id: companyId }; - - const options: FindAndCountOptions = { - where: scopedWhere, - order, - limit, - offset, - attributes, - raw: true, // devolvemos objetos planos -> más rápido - nest: false, - distinct: true // por si en el futuro añadimos includes no duplicar count - }; - - const { rows, count } = await CustomerInvoiceModel.findAndCountAll(options); - - return { - rows: rows as unknown as InvoiceListRow[], - total: typeof count === "number" ? count : (count as any[]).length, - limit, - offset, - }; - } */ + constructor( + private readonly domainMapper: SequelizeProformaDomainMapper, + private readonly listMapper: SequelizeProformaListMapper, + database: Sequelize + ) { + super({ database }); + } /** * * Crea una nueva factura. * - * @param invoice - El agregado a guardar. + * @param proforma - El agregado a guardar. * @param transaction - Transacción activa para la operación. * @returns Result */ - async create(invoice: CustomerInvoice, transaction?: Transaction): Promise> { + async create(proforma: Proforma, transaction?: Transaction): Promise> { try { - const mapper: ICustomerInvoiceDomainMapper = this._registry.getDomainMapper({ - resource: "customer-invoice", - }); - const dtoResult = mapper.mapToPersistence(invoice); + const dtoResult = this.domainMapper.mapToPersistence(proforma); if (dtoResult.isFailure) { return Result.fail(dtoResult.error); } const dto = dtoResult.data; - const { id, items, taxes, verifactu, ...createPayload } = dto; + const { id, items, taxes, ...createPayload } = dto; // 1. Insertar cabecera await CustomerInvoiceModel.create( @@ -106,11 +70,6 @@ export class CustomerInvoiceRepository } } - // 4. Inserta VerifactuRecord si existe - if (verifactu) { - await VerifactuRecordModel.create(verifactu, { transaction }); - } - return Result.ok(); } catch (err: unknown) { return Result.fail(translateSequelizeError(err)); @@ -124,12 +83,9 @@ export class CustomerInvoiceRepository * @param transaction - Transacción activa para la operación. * @returns Result */ - async update(invoice: CustomerInvoice, transaction: Transaction): Promise> { + async update(proforma: Proforma, transaction: Transaction): Promise> { try { - const mapper: ICustomerInvoiceDomainMapper = this._registry.getDomainMapper({ - resource: "customer-invoice", - }); - const dtoResult = mapper.mapToPersistence(invoice); + const dtoResult = this.domainMapper.mapToPersistence(proforma); if (dtoResult.isFailure) return Result.fail(dtoResult.error); @@ -207,402 +163,6 @@ export class CustomerInvoiceRepository } } - /** - * - * Busca una proforma por su identificador único. - * - * @param companyId - Identificador UUID de la empresa a la que pertenece la proforma. - * @param id - UUID de la proforma. - * @param transaction - Transacción activa para la operación. - * @param options - Opciones adicionales para la consulta (Sequelize FindOptions) - * @returns Result - */ - async getProformaByIdInCompany( - companyId: UniqueID, - id: UniqueID, - transaction: Transaction, - options: FindOptions> = {} - ): Promise> { - const { CustomerModel } = this._database.models; - - try { - const mapper: ICustomerInvoiceDomainMapper = this._registry.getDomainMapper({ - resource: "customer-invoice", - }); - - // Normalización defensiva de order/include - const normalizedOrder = Array.isArray(options.order) - ? options.order - : options.order - ? [options.order] - : []; - - const normalizedInclude = Array.isArray(options.include) - ? options.include - : options.include - ? [options.include] - : []; - - const mergedOptions: FindOptions> = { - ...options, - where: { - ...(options.where ?? {}), - id: id.toString(), - is_proforma: true, - company_id: companyId.toString(), - }, - order: [ - ...normalizedOrder, - [{ model: CustomerInvoiceItemModel, as: "items" }, "position", "ASC"], - ], - include: [ - ...normalizedInclude, - { - model: CustomerModel, - as: "current_customer", - required: false, - }, - { - model: CustomerInvoiceItemModel, - as: "items", - required: false, - }, - { - model: CustomerInvoiceTaxModel, - as: "taxes", - required: false, - }, - ], - transaction, - }; - - const row = await CustomerInvoiceModel.findOne(mergedOptions); - - if (!row) { - return Result.fail(new EntityNotFoundError("CustomerInvoice", "id", id.toString())); - } - - const invoice = mapper.mapToDomain(row); - return invoice; - } catch (err: unknown) { - return Result.fail(translateSequelizeError(err)); - } - } - - /** - * - * Busca una factura por su identificador único. - * - * @param companyId - Identificador UUID de la empresa a la que pertenece la factura. - * @param id - UUID de la factura. - * @param transaction - Transacción activa para la operación. - * @param options - Opciones adicionales para la consulta (Sequelize FindOptions) - * @returns Result - */ - async getIssuedInvoiceByIdInCompany( - companyId: UniqueID, - id: UniqueID, - transaction: Transaction, - options: FindOptions> = {} - ): Promise> { - const { CustomerModel } = this._database.models; - - try { - const mapper: ICustomerInvoiceDomainMapper = this._registry.getDomainMapper({ - resource: "customer-invoice", - }); - - // Normalización defensiva de order/include - const normalizedOrder = Array.isArray(options.order) - ? options.order - : options.order - ? [options.order] - : []; - - const normalizedInclude = Array.isArray(options.include) - ? options.include - : options.include - ? [options.include] - : []; - - const mergedOptions: FindOptions> = { - ...options, - where: { - ...(options.where ?? {}), - id: id.toString(), - is_proforma: false, - company_id: companyId.toString(), - }, - order: [ - ...normalizedOrder, - [{ model: CustomerInvoiceItemModel, as: "items" }, "position", "ASC"], - ], - include: [ - ...normalizedInclude, - { - model: VerifactuRecordModel, - as: "verifactu", - required: false, - attributes: ["id", "estado", "url", "uuid", "qr"], - }, - { - model: CustomerModel, - as: "current_customer", - required: false, - }, - { - model: CustomerInvoiceItemModel, - as: "items", - required: false, - }, - { - model: CustomerInvoiceTaxModel, - as: "taxes", - required: false, - }, - ], - transaction, - }; - - const row = await CustomerInvoiceModel.findOne(mergedOptions); - - if (!row) { - return Result.fail(new EntityNotFoundError("CustomerInvoice", "id", id.toString())); - } - - const invoice = mapper.mapToDomain(row); - return invoice; - } catch (err: unknown) { - return Result.fail(translateSequelizeError(err)); - } - } - - /** - * - * Consulta proformas usando un objeto Criteria (filtros, orden, paginación). - * - * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. - * @param criteria - Criterios de búsqueda. - * @param transaction - Transacción activa para la operación. - * @returns Result - * - * @see Criteria - */ - public async findProformasByCriteriaInCompany( - companyId: UniqueID, - criteria: Criteria, - transaction: Transaction, - options: FindOptions> = {} - ): Promise, Error>> { - const { CustomerModel } = this._database.models; - - try { - const mapper: ICustomerInvoiceListMapper = this._registry.getQueryMapper({ - resource: "customer-invoice", - query: "LIST", - }); - - const converter = new CriteriaToSequelizeConverter(); - const query = converter.convert(criteria, { - searchableFields: ["invoice_number", "reference", "description"], - mappings: { - reference: "CustomerInvoiceModel.reference", - }, - allowedFields: ["invoice_date", "id", "created_at"], - enableFullText: true, - database: this._database, - strictMode: true, // fuerza error si ORDER BY no permitido - }); - - // Normalización defensiva de order/include - const normalizedOrder = Array.isArray(options.order) - ? options.order - : options.order - ? [options.order] - : []; - - const normalizedInclude = Array.isArray(options.include) - ? options.include - : options.include - ? [options.include] - : []; - - query.where = { - ...query.where, - ...(options.where ?? {}), - is_proforma: true, - company_id: companyId.toString(), - deleted_at: null, - }; - - query.order = [...(query.order as OrderItem[]), ...normalizedOrder]; - - query.include = [ - ...normalizedInclude, - { - model: CustomerModel, - as: "current_customer", - required: false, // false => LEFT JOIN - attributes: [ - "name", - "trade_name", - "tin", - "street", - "street2", - "city", - "postal_code", - "province", - "country", - ], - }, - { - model: CustomerInvoiceTaxModel, - as: "taxes", - required: false, - separate: true, // => query aparte, devuelve siempre array - }, - ]; - - // Reemplazar findAndCountAll por findAll + count (más control y mejor rendimiento) - /*const { rows, count } = await CustomerInvoiceModel.findAndCountAll({ - ...query, - transaction, - });*/ - - const [rows, count] = await Promise.all([ - CustomerInvoiceModel.findAll({ - ...query, - transaction, - }), - CustomerInvoiceModel.count({ - where: query.where, - distinct: true, // evita duplicados por LEFT JOIN - transaction, - }), - ]); - - return mapper.mapToDTOCollection(rows, count); - } catch (err: unknown) { - return Result.fail(translateSequelizeError(err)); - } - } - - /** - * - * Consulta facturas usando un objeto Criteria (filtros, orden, paginación). - * - * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. - * @param criteria - Criterios de búsqueda. - * @param transaction - Transacción activa para la operación. - * @returns Result - * - * @see Criteria - */ - public async findIssuedInvoicesByCriteriaInCompany( - companyId: UniqueID, - criteria: Criteria, - transaction: Transaction, - options: FindOptions> = {} - ): Promise, Error>> { - const { CustomerModel } = this._database.models; - - try { - const mapper: ICustomerInvoiceListMapper = this._registry.getQueryMapper({ - resource: "customer-invoice", - query: "LIST", - }); - - const converter = new CriteriaToSequelizeConverter(); - const query = converter.convert(criteria, { - searchableFields: ["invoice_number", "reference", "description"], - mappings: { - reference: "CustomerInvoiceModel.reference", - }, - allowedFields: ["invoice_date", "id", "created_at"], - enableFullText: true, - database: this._database, - strictMode: true, // fuerza error si ORDER BY no permitido - }); - - // Normalización defensiva de order/include - const normalizedOrder = Array.isArray(options.order) - ? options.order - : options.order - ? [options.order] - : []; - - const normalizedInclude = Array.isArray(options.include) - ? options.include - : options.include - ? [options.include] - : []; - - query.where = { - ...query.where, - ...(options.where ?? {}), - is_proforma: false, - company_id: companyId.toString(), - deleted_at: null, - }; - - query.order = [...(query.order as OrderItem[]), ...normalizedOrder]; - - query.include = [ - ...normalizedInclude, - { - model: VerifactuRecordModel, - as: "verifactu", - required: false, - attributes: ["id", "estado", "url", "uuid", "qr"], - }, - { - model: CustomerModel, - as: "current_customer", - required: false, // false => LEFT JOIN - attributes: [ - "name", - "trade_name", - "tin", - "street", - "street2", - "city", - "postal_code", - "province", - "country", - ], - }, - { - model: CustomerInvoiceTaxModel, - as: "taxes", - required: false, - separate: true, // => query aparte, devuelve siempre array - }, - ]; - - // Reemplazar findAndCountAll por findAll + count (más control y mejor rendimiento) - /*const { rows, count } = await CustomerInvoiceModel.findAndCountAll({ - ...query, - transaction, - });*/ - - const [rows, count] = await Promise.all([ - CustomerInvoiceModel.findAll({ - ...query, - transaction, - }), - CustomerInvoiceModel.count({ - where: query.where, - distinct: true, // evita duplicados por LEFT JOIN - transaction, - }), - ]); - - return mapper.mapToDTOCollection(rows, count); - } catch (err: unknown) { - return Result.fail(translateSequelizeError(err)); - } - } - /** * * Elimina o marca como eliminada una proforma dentro de una empresa. @@ -612,7 +172,7 @@ export class CustomerInvoiceRepository * @param transaction - Transacción activa para la operación. * @returns Result */ - async deleteProformaByIdInCompany( + async deleteByIdInCompany( companyId: UniqueID, id: UniqueID, transaction: Transaction @@ -647,10 +207,10 @@ export class CustomerInvoiceRepository * @param transaction - Transacción activa para la operación. * @returns Result */ - async updateProformaStatusByIdInCompany( + async updateStatusByIdInCompany( companyId: UniqueID, id: UniqueID, - newStatus: CustomerInvoiceStatus, + newStatus: InvoiceStatus, transaction: Transaction ): Promise> { try { @@ -679,4 +239,188 @@ export class CustomerInvoiceRepository return Result.fail(translateSequelizeError(err)); } } + + /** + * + * Busca una factura por su identificador único. + * + * @param companyId - Identificador UUID de la empresa a la que pertenece la factura. + * @param id - UUID de la factura. + * @param transaction - Transacción activa para la operación. + * @param options - Opciones adicionales para la consulta (Sequelize FindOptions) + * @returns Result + */ + async getByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction: Transaction, + options: FindOptions> = {} + ): Promise> { + const { CustomerModel } = this.database.models; + + try { + // Normalización defensiva de order/include + const normalizedOrder = Array.isArray(options.order) + ? options.order + : options.order + ? [options.order] + : []; + + const normalizedInclude = Array.isArray(options.include) + ? options.include + : options.include + ? [options.include] + : []; + + const mergedOptions: FindOptions> = { + ...options, + where: { + ...(options.where ?? {}), + id: id.toString(), + is_proforma: false, + company_id: companyId.toString(), + }, + order: [ + ...normalizedOrder, + [{ model: CustomerInvoiceItemModel, as: "items" }, "position", "ASC"], + ], + include: [ + ...normalizedInclude, + + { + model: CustomerModel, + as: "current_customer", + required: false, + }, + { + model: CustomerInvoiceItemModel, + as: "items", + required: false, + }, + { + model: CustomerInvoiceTaxModel, + as: "taxes", + required: false, + }, + ], + transaction, + }; + + const row = await CustomerInvoiceModel.findOne(mergedOptions); + + if (!row) { + return Result.fail(new EntityNotFoundError("CustomerInvoice", "id", id.toString())); + } + + const invoice = this.domainMapper.mapToDomain(row); + return invoice; + } catch (err: unknown) { + return Result.fail(translateSequelizeError(err)); + } + } + + /** + * + * Consulta facturas usando un objeto Criteria (filtros, orden, paginación). + * + * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. + * @param criteria - Criterios de búsqueda. + * @param transaction - Transacción activa para la operación. + * @returns Result + * + * @see Criteria + */ + public async findByCriteriaInCompany( + companyId: UniqueID, + criteria: Criteria, + transaction: Transaction, + options: FindOptions> = {} + ): Promise, Error>> { + const { CustomerModel } = this.database.models; + + try { + const converter = new CriteriaToSequelizeConverter(); + const query = converter.convert(criteria, { + searchableFields: ["invoice_number", "reference", "description"], + mappings: { + reference: "CustomerInvoiceModel.reference", + }, + allowedFields: ["invoice_date", "id", "created_at"], + enableFullText: true, + database: this.database, + strictMode: true, // fuerza error si ORDER BY no permitido + }); + + // Normalización defensiva de order/include + const normalizedOrder = Array.isArray(options.order) + ? options.order + : options.order + ? [options.order] + : []; + + const normalizedInclude = Array.isArray(options.include) + ? options.include + : options.include + ? [options.include] + : []; + + query.where = { + ...query.where, + ...(options.where ?? {}), + is_proforma: false, + company_id: companyId.toString(), + deleted_at: null, + }; + + query.order = [...(query.order as OrderItem[]), ...normalizedOrder]; + + query.include = [ + ...normalizedInclude, + { + model: CustomerModel, + as: "current_customer", + required: false, // false => LEFT JOIN + attributes: [ + "name", + "trade_name", + "tin", + "street", + "street2", + "city", + "postal_code", + "province", + "country", + ], + }, + { + model: CustomerInvoiceTaxModel, + as: "taxes", + required: false, + separate: true, // => query aparte, devuelve siempre array + }, + ]; + + // Reemplazar findAndCountAll por findAll + count (más control y mejor rendimiento) + /*const { rows, count } = await CustomerInvoiceModel.findAndCountAll({ + ...query, + transaction, + });*/ + + const [rows, count] = await Promise.all([ + CustomerInvoiceModel.findAll({ + ...query, + transaction, + }), + CustomerInvoiceModel.count({ + where: query.where, + distinct: true, // evita duplicados por LEFT JOIN + transaction, + }), + ]); + + return this.listMapper.mapToDTOCollection(rows, count); + } catch (err: unknown) { + return Result.fail(translateSequelizeError(err)); + } + } } diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/services/index.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/services/index.ts new file mode 100644 index 00000000..7bc264d6 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/services/index.ts @@ -0,0 +1 @@ +export * from "./sequelize-proforma-number-generator.service"; diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/services/sequelize-proforma-number-generator.service.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/services/sequelize-proforma-number-generator.service.ts new file mode 100644 index 00000000..e081bd71 --- /dev/null +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/services/sequelize-proforma-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 { IProformaNumberGenerator } from "../../../../../application/proformas"; +import { InvoiceNumber, type InvoiceSerie } from "../../../../../domain"; +import { CustomerInvoiceModel } from "../../../../common/persistence"; + +/** + * Generador de números de factura + */ +export class SequelizeProformaNumberGenerator implements IProformaNumberGenerator { + 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 = "001"; // 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(3, "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/renderers/issued-invoice.renderer.html.ts b/modules/customer-invoices/src/api/infrastructure/renderers/issued-invoice.renderer.html.ts index ccacaa3f..ce26fa9d 100644 --- a/modules/customer-invoices/src/api/infrastructure/renderers/issued-invoice.renderer.html.ts +++ b/modules/customer-invoices/src/api/infrastructure/renderers/issued-invoice.renderer.html.ts @@ -1,11 +1,11 @@ import { TemplateRenderer } from "@erp/core/api"; -import type { CustomerInvoice } from "../../domain"; +import type { Proforma } from "../../domain"; import type { IssuedInvoiceReportJSONRenderer } from "./issued-invoice.renderer.json"; export class IssuedInvoiceReportHTMLRenderer extends TemplateRenderer { - toOutput(invoice: CustomerInvoice, params: { companySlug: string }): string { + toOutput(invoice: Proforma, params: { companySlug: string }): string { const { companySlug } = params; const jsonRenderer = this.presenterRegistry.getRenderer({ diff --git a/modules/customer-invoices/src/api/infrastructure/renderers/issued-invoice.renderer.json.ts b/modules/customer-invoices/src/api/infrastructure/renderers/issued-invoice.renderer.json.ts index e4fa0f96..5408b48e 100644 --- a/modules/customer-invoices/src/api/infrastructure/renderers/issued-invoice.renderer.json.ts +++ b/modules/customer-invoices/src/api/infrastructure/renderers/issued-invoice.renderer.json.ts @@ -4,13 +4,10 @@ import type { IssuedInvoiceFullRenderer, IssuedInvoiceReportRenderer, } from "../../application/snapshot-builders"; -import type { CustomerInvoice } from "../../domain"; +import type { Proforma } from "../../domain"; -export class IssuedInvoiceReportJSONRenderer extends Renderer< - CustomerInvoice, - Record -> { - toOutput(invoice: CustomerInvoice, params: { companySlug: string }): Record { +export class IssuedInvoiceReportJSONRenderer extends Renderer> { + toOutput(invoice: Proforma, params: { companySlug: string }): Record { const dtoRenderer = this.presenterRegistry.getRenderer({ resource: "issued-invoice", projection: "FULL", diff --git a/modules/customers/src/api/infrastructure/sequelize/repositories/customer.repository.ts b/modules/customers/src/api/infrastructure/sequelize/repositories/customer.repository.ts index 5dbc08d8..2fdf7694 100644 --- a/modules/customers/src/api/infrastructure/sequelize/repositories/customer.repository.ts +++ b/modules/customers/src/api/infrastructure/sequelize/repositories/customer.repository.ts @@ -4,12 +4,13 @@ import { SequelizeRepository, translateSequelizeError, } from "@erp/core/api"; -import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server"; -import { UniqueID } from "@repo/rdx-ddd"; -import { Collection, Result } from "@repo/rdx-utils"; -import { Transaction } from "sequelize"; -import { Customer, ICustomerRepository } from "../../../domain"; -import { CustomerListDTO, ICustomerDomainMapper, ICustomerListMapper } from "../../mappers"; +import { type Criteria, CriteriaToSequelizeConverter } 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 { Customer, ICustomerRepository } from "../../../domain"; +import type { CustomerListDTO, ICustomerDomainMapper, ICustomerListMapper } from "../../mappers"; import { CustomerModel } from "../models/customer.model"; export class CustomerRepository @@ -179,7 +180,7 @@ export class CustomerRepository "mobile_primary", ], enableFullText: true, - database: this._database, + database: this.database, strictMode: true, // fuerza error si ORDER BY no permitido });