diff --git a/modules/core/src/api/domain/value-objects/tax.ts b/modules/core/src/api/domain/value-objects/tax.ts index 938e95f8..83f1d304 100644 --- a/modules/core/src/api/domain/value-objects/tax.ts +++ b/modules/core/src/api/domain/value-objects/tax.ts @@ -11,10 +11,10 @@ const DEFAULT_MIN_SCALE = 0; const DEFAULT_MAX_SCALE = 4; export interface TaxProps { - value: number; - scale: number; - name: string; - code: string; + code: string; // iva_21 + name: string; // 21% IVA + value: number; // 2100 + scale: number; // 2 } export class Tax extends ValueObject { diff --git a/modules/customer-invoices/src/api/application/list-customer-invoices/list-customer-invoices.use-case.ts b/modules/customer-invoices/src/api/application/list-customer-invoices/list-customer-invoices.use-case.ts index d9f7fd24..2cc73afa 100644 --- a/modules/customer-invoices/src/api/application/list-customer-invoices/list-customer-invoices.use-case.ts +++ b/modules/customer-invoices/src/api/application/list-customer-invoices/list-customer-invoices.use-case.ts @@ -1,3 +1,4 @@ +import { JsonTaxCatalogProvider } from "@erp/core"; import { ITransactionManager } from "@erp/core/api"; import { Criteria } from "@repo/rdx-criteria/server"; import { UniqueID } from "@repo/rdx-ddd"; @@ -16,7 +17,8 @@ export class ListCustomerInvoicesUseCase { constructor( private readonly service: CustomerInvoiceService, private readonly transactionManager: ITransactionManager, - private readonly assembler: ListCustomerInvoicesAssembler + private readonly assembler: ListCustomerInvoicesAssembler, + private readonly taxCatalog: JsonTaxCatalogProvider ) {} public execute( diff --git a/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts b/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts index dc0e075c..091f1285 100644 --- a/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts +++ b/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts @@ -1,4 +1,4 @@ -import { DomainValidationError, Taxes } from "@erp/core/api"; +import { DomainValidationError } from "@erp/core/api"; import { AggregateRoot, CurrencyCode, @@ -11,6 +11,7 @@ import { } from "@repo/rdx-ddd"; import { Maybe, Result } from "@repo/rdx-utils"; import { CustomerInvoiceItems, InvoiceRecipient } from "../entities"; +import { InvoiceTaxes } from "../entities/invoice-taxes"; import { CustomerInvoiceNumber, CustomerInvoiceSerie, @@ -53,7 +54,7 @@ export interface CustomerInvoiceProps { discountPercentage: Percentage; //discountAmount: MoneyValue; - taxes: Taxes; + taxes: InvoiceTaxes; //totalAmount: MoneyValue; diff --git a/modules/customer-invoices/src/api/domain/entities/index.ts b/modules/customer-invoices/src/api/domain/entities/index.ts index ea23a845..03dcb0d1 100644 --- a/modules/customer-invoices/src/api/domain/entities/index.ts +++ b/modules/customer-invoices/src/api/domain/entities/index.ts @@ -1,4 +1,3 @@ export * from "./customer-invoice-items"; -export * from "./invoice-customer"; export * from "./invoice-recipient"; - +export * from "./invoice-taxes"; diff --git a/modules/customer-invoices/src/api/domain/entities/invoice-customer/index.ts b/modules/customer-invoices/src/api/domain/entities/invoice-customer/index.ts deleted file mode 100644 index 6d725810..00000000 --- a/modules/customer-invoices/src/api/domain/entities/invoice-customer/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./invoice-customer"; diff --git a/modules/customer-invoices/src/api/domain/entities/invoice-customer/invoice-address.ts b/modules/customer-invoices/src/api/domain/entities/invoice-customer/invoice-address.ts deleted file mode 100644 index ea67b13d..00000000 --- a/modules/customer-invoices/src/api/domain/entities/invoice-customer/invoice-address.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { EmailAddress, Name, PostalAddress, ValueObject } from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; -import { PhoneNumber } from "libphonenumber-js"; -import { CustomerInvoiceAddressType } from "../../value-objects"; - -export interface ICustomerInvoiceAddressProps { - type: CustomerInvoiceAddressType; - title: Name; - address: PostalAddress; - email: EmailAddress; - phone: PhoneNumber; -} - -export interface ICustomerInvoiceAddress { - type: CustomerInvoiceAddressType; - title: Name; - address: PostalAddress; - email: EmailAddress; - phone: PhoneNumber; -} - -export class CustomerInvoiceAddress - extends ValueObject - implements ICustomerInvoiceAddress -{ - public static create(props: ICustomerInvoiceAddressProps) { - return Result.ok(new CustomerInvoiceAddress(props)); - } - - public static createShippingAddress(props: ICustomerInvoiceAddressProps) { - return Result.ok( - new CustomerInvoiceAddress({ - ...props, - type: CustomerInvoiceAddressType.create("shipping").data, - }) - ); - } - - public static createBillingAddress(props: ICustomerInvoiceAddressProps) { - return Result.ok( - new CustomerInvoiceAddress({ - ...props, - type: CustomerInvoiceAddressType.create("billing").data, - }) - ); - } - - get title(): Name { - return this.props.title; - } - - get address(): PostalAddress { - return this.props.address; - } - - get email(): EmailAddress { - return this.props.email; - } - - get phone(): PhoneNumber { - return this.props.phone; - } - - get type(): CustomerInvoiceAddressType { - return this.props.type; - } - - getProps(): ICustomerInvoiceAddressProps { - return this.props; - } - - toPrimitive() { - return { - type: this.type.toString(), - title: this.title.toString(), - address: this.address.toString(), - email: this.email.toString(), - phone: this.phone.toString(), - }; - } -} diff --git a/modules/customer-invoices/src/api/domain/entities/invoice-customer/invoice-customer.ts b/modules/customer-invoices/src/api/domain/entities/invoice-customer/invoice-customer.ts deleted file mode 100644 index ea9571a3..00000000 --- a/modules/customer-invoices/src/api/domain/entities/invoice-customer/invoice-customer.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { DomainEntity, Name, TINNumber, UniqueID } from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; -import { CustomerInvoiceAddress } from "./customer-invoice-address"; - -export interface ICustomerInvoiceCustomerProps { - tin: TINNumber; - companyName: Name; - firstName: Name; - lastName: Name; - - billingAddress?: CustomerInvoiceAddress; - shippingAddress?: CustomerInvoiceAddress; -} - -export interface ICustomerInvoiceCustomer { - id: UniqueID; - tin: TINNumber; - companyName: Name; - firstName: Name; - lastName: Name; - - billingAddress?: CustomerInvoiceAddress; - shippingAddress?: CustomerInvoiceAddress; -} - -export class CustomerInvoiceCustomer - extends DomainEntity - implements ICustomerInvoiceCustomer -{ - public static create( - props: ICustomerInvoiceCustomerProps, - id?: UniqueID - ): Result { - const participant = new CustomerInvoiceCustomer(props, id); - return Result.ok(participant); - } - - get tin(): TINNumber { - return this.props.tin; - } - - get companyName(): Name { - return this.props.companyName; - } - - get firstName(): Name { - return this.props.firstName; - } - - get lastName(): Name { - return this.props.lastName; - } - - get billingAddress() { - return this.props.billingAddress; - } - - get shippingAddress() { - return this.props.shippingAddress; - } -} diff --git a/modules/customer-invoices/src/api/domain/entities/invoice-taxes/index.ts b/modules/customer-invoices/src/api/domain/entities/invoice-taxes/index.ts new file mode 100644 index 00000000..dcc779f4 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/entities/invoice-taxes/index.ts @@ -0,0 +1,2 @@ +export * from "./invoice-tax"; +export * from "./invoice-taxes"; diff --git a/modules/customer-invoices/src/api/domain/entities/invoice-taxes/invoice-tax.ts b/modules/customer-invoices/src/api/domain/entities/invoice-taxes/invoice-tax.ts new file mode 100644 index 00000000..6cc42ce5 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/entities/invoice-taxes/invoice-tax.ts @@ -0,0 +1,55 @@ +import { Tax } from "@erp/core/api"; +import { ValueObject } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; +import { InvoiceAmount } from "../../value-objects/invoice-amount"; + +export interface InvoiceTaxProps { + tax: Tax; + taxableAmount: InvoiceAmount; + taxesAmount: InvoiceAmount; +} + +export class InvoiceTax extends ValueObject { + protected static validate(values: InvoiceTaxProps) { + return Result.ok(values); + } + + static create(values: InvoiceTaxProps): Result { + const valueIsValid = InvoiceTax.validate(values); + + if (valueIsValid.isFailure) { + return Result.fail(valueIsValid.error); + } + + return Result.ok(new InvoiceTax(values)); + } + + public update(partial: Partial): Result { + const updatedProps = { + ...this.props, + ...partial, + } as InvoiceTaxProps; + + return InvoiceTax.create(updatedProps); + } + + public get tax(): Tax { + return this.props.tax; + } + + public get taxableAmount(): InvoiceAmount { + return this.props.taxableAmount; + } + + public get taxesAmount(): InvoiceAmount { + return this.props.taxesAmount; + } + + getProps(): InvoiceTaxProps { + return this.props; + } + + toPrimitive() { + return this.getProps(); + } +} diff --git a/modules/customer-invoices/src/api/domain/entities/invoice-taxes/invoice-taxes.ts b/modules/customer-invoices/src/api/domain/entities/invoice-taxes/invoice-taxes.ts new file mode 100644 index 00000000..d7bd9c90 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/entities/invoice-taxes/invoice-taxes.ts @@ -0,0 +1,17 @@ +import { Collection } from "@repo/rdx-utils"; +import { InvoiceTax } from "./invoice-tax"; + +export interface InvoiceTaxesProps { + items?: InvoiceTax[]; +} + +export class InvoiceTaxes extends Collection { + constructor(props: InvoiceTaxesProps) { + const { items = [] } = props; + super(items); + } + + public static create(props: InvoiceTaxesProps): InvoiceTaxes { + return new InvoiceTaxes(props); + } +} diff --git a/modules/customer-invoices/src/api/domain/value-objects/index.ts b/modules/customer-invoices/src/api/domain/value-objects/index.ts index 41658010..973f195e 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/index.ts +++ b/modules/customer-invoices/src/api/domain/value-objects/index.ts @@ -3,6 +3,7 @@ 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 "./item-amount"; export * from "./item-discount"; export * from "./item-quantity"; diff --git a/modules/customer-invoices/src/api/domain/value-objects/invoice-amount.ts b/modules/customer-invoices/src/api/domain/value-objects/invoice-amount.ts new file mode 100644 index 00000000..33f278a5 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/value-objects/invoice-amount.ts @@ -0,0 +1,24 @@ +import { MoneyValue, MoneyValueProps } from "@repo/rdx-ddd"; + +type InvoiceAmountProps = Pick; + +export class InvoiceAmount extends MoneyValue { + public static DEFAULT_SCALE = 2; + + static create({ value, currency_code }: InvoiceAmountProps) { + const props = { + value: Number(value), + scale: InvoiceAmount.DEFAULT_SCALE, + currency_code, + }; + return MoneyValue.create(props); + } + + static zero(currency_code: string) { + const props = { + value: 0, + currency_code, + }; + return InvoiceAmount.create(props); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/dependencies.ts b/modules/customer-invoices/src/api/infrastructure/dependencies.ts index 2b5a0094..27499e1f 100644 --- a/modules/customer-invoices/src/api/infrastructure/dependencies.ts +++ b/modules/customer-invoices/src/api/infrastructure/dependencies.ts @@ -52,10 +52,13 @@ export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps { const { database } = params; const transactionManager = new SequelizeTransactionManager(database); - if (!_mapper) _mapper = new CustomerInvoiceMapper(); + if (!_catalogs) _catalogs = { taxes: spainTaxCatalogProvider }; + if (!_mapper) + _mapper = new CustomerInvoiceMapper({ + taxCatalog: _catalogs!.taxes, + }); if (!_repo) _repo = new CustomerInvoiceRepository({ mapper: _mapper, database }); if (!_service) _service = new CustomerInvoiceService(_repo); - if (!_catalogs) _catalogs = { taxes: spainTaxCatalogProvider }; if (!_assemblers) { _assemblers = { @@ -75,8 +78,19 @@ export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps { catalogs: _catalogs, build: { list: () => - new ListCustomerInvoicesUseCase(_service!, transactionManager!, _assemblers!.list), - get: () => new GetCustomerInvoiceUseCase(_service!, transactionManager!, _assemblers!.get), + new ListCustomerInvoicesUseCase( + _service!, + transactionManager!, + _assemblers!.list, + _catalogs!.taxes + ), + get: () => + new GetCustomerInvoiceUseCase( + _service!, + transactionManager!, + _assemblers!.get, + _catalogs!.taxes + ), create: () => new CreateCustomerInvoiceUseCase( _service!, @@ -85,7 +99,12 @@ export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps { _catalogs!.taxes ), update: () => - new UpdateCustomerInvoiceUseCase(_service!, transactionManager!, _assemblers!.update), + new UpdateCustomerInvoiceUseCase( + _service!, + transactionManager!, + _assemblers!.update, + _catalogs!.taxes + ), delete: () => new DeleteCustomerInvoiceUseCase(_service!, transactionManager!), }, presenters: { diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice-item.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice-item.mapper.ts index e66cdf7c..63d88f14 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice-item.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice-item.mapper.ts @@ -2,31 +2,23 @@ import { ISequelizeMapper, MapperParamsType, SequelizeMapper, + ValidationErrorCollection, ValidationErrorDetail, extractOrPushError, } from "@erp/core/api"; -import { - CurrencyCode, - LanguageCode, - UniqueID, - maybeFromNullableVO, - toNullable, -} from "@repo/rdx-ddd"; +import { UniqueID, maybeFromNullableVO, toNullable } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { InferCreationAttributes } from "sequelize"; import { CustomerInvoice, CustomerInvoiceItem, CustomerInvoiceItemDescription, + CustomerInvoiceProps, ItemAmount, ItemDiscount, ItemQuantity, } from "../../domain"; -import { - CustomerInvoiceItemCreationAttributes, - CustomerInvoiceItemModel, - CustomerInvoiceModel, -} from "../sequelize"; +import { CustomerInvoiceItemCreationAttributes, CustomerInvoiceItemModel } from "../sequelize"; export interface ICustomerInvoiceItemMapper extends ISequelizeMapper< @@ -43,26 +35,16 @@ export class CustomerInvoiceItemMapper > implements ICustomerInvoiceItemMapper { - public mapToDomain( - source: CustomerInvoiceItemModel, - params?: MapperParamsType - ): Result { - const { sourceParent, errors } = params as { - sourceParent: CustomerInvoiceModel; + private mapAttributesToDomain(source: CustomerInvoiceItemModel, params?: MapperParamsType) { + const { errors, index, attributes } = params as { + index: number; errors: ValidationErrorDetail[]; + attributes: Partial; }; - const itemId = extractOrPushError(UniqueID.create(source.item_id), "item_id", errors); - - const languageCode = extractOrPushError( - LanguageCode.create(sourceParent.language_code), - "language_code", - errors - ); - - const currencyCode = extractOrPushError( - CurrencyCode.create(sourceParent.currency_code), - "currency_code", + const itemId = extractOrPushError( + UniqueID.create(source.item_id), + `items[${index}].item_id`, errors ); @@ -70,51 +52,87 @@ export class CustomerInvoiceItemMapper maybeFromNullableVO(source.description, (value) => CustomerInvoiceItemDescription.create(value) ), - "description", + `items[${index}].description`, errors ); const quantity = extractOrPushError( maybeFromNullableVO(source.quantity_value, (value) => ItemQuantity.create({ value })), - "discount_percentage", + `items[${index}].discount_percentage`, errors ); const unitAmount = extractOrPushError( maybeFromNullableVO(source.unit_amount_value, (value) => - ItemAmount.create({ value, currency_code: currencyCode!.code }) + ItemAmount.create({ value, currency_code: attributes.currencyCode!.code }) ), - "unit_amount", + `items[${index}].unit_amount`, errors ); + return { + itemId, + languageCode: attributes.languageCode, + currencyCode: attributes.currencyCode, + description, + quantity, + + unitAmount, + }; + } + + public mapToDomain( + source: CustomerInvoiceItemModel, + params?: MapperParamsType + ): Result { + const { errors, index, requireIncludes } = params as { + index: number; + requireIncludes: boolean; + errors: ValidationErrorDetail[]; + attributes: Partial; + }; + + if (requireIncludes) { + if (!source.taxes) { + errors.push({ + path: `items[${index}].taxes`, + message: "Taxes not included in query (requireIncludes=true)", + }); + } + } + + const attributes = this.mapAttributesToDomain(source, params); + const discountPercentage = extractOrPushError( maybeFromNullableVO(source.discount_percentage_value, (value) => ItemDiscount.create({ value }) ), - "discount_percentage", + `items[${index}].discount_percentage`, errors ); // Creación del objeto de dominio - const itemOrError = CustomerInvoiceItem.create( + const createResult = CustomerInvoiceItem.create( { - languageCode: languageCode!, - currencyCode: currencyCode!, - description: description!, - quantity: quantity!, - unitAmount: unitAmount!, + languageCode: attributes.languageCode!, + currencyCode: attributes.currencyCode!, + description: attributes.description!, + quantity: attributes.quantity!, + unitAmount: attributes.unitAmount!, discountPercentage: discountPercentage!, - taxes: "", }, - itemId + attributes.itemId ); - if (itemOrError.isFailure) { - errors.push({ path: "item", message: itemOrError.error.message }); + if (createResult.isFailure) { + return Result.fail( + new ValidationErrorCollection("Invoice item entity creation failed", [ + { path: `items[${index}]`, message: createResult.error.message }, + ]) + ); } - return itemOrError; + return createResult; } public mapToPersistence( diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice.mapper.ts index cee41f18..d286070e 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice.mapper.ts @@ -1,3 +1,4 @@ +import { JsonTaxCatalogProvider } from "@erp/core"; import { ISequelizeMapper, MapperParamsType, @@ -25,6 +26,7 @@ import { CustomerInvoiceSerie, CustomerInvoiceStatus, } from "../../domain"; +import { InvoiceTaxes } from "../../domain/entities/invoice-taxes"; import { CustomerInvoiceCreationAttributes, CustomerInvoiceModel } from "../sequelize"; import { CustomerInvoiceItemMapper } from "./customer-invoice-item.mapper"; import { InvoiceRecipientMapper } from "./invoice-recipient.mapper"; @@ -45,11 +47,103 @@ export class CustomerInvoiceMapper private _recipientMapper: InvoiceRecipientMapper; private _taxesMapper: TaxesMapper; - constructor() { + constructor(params: { + taxCatalog: JsonTaxCatalogProvider; + }) { super(); this._itemsMapper = new CustomerInvoiceItemMapper(); // Instanciar el mapper de items this._recipientMapper = new InvoiceRecipientMapper(); - this._taxesMapper = new TaxesMapper(); + this._taxesMapper = new TaxesMapper(params); + } + + 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 status = extractOrPushError( + CustomerInvoiceStatus.create(source.status), + "status", + errors + ); + + const series = extractOrPushError( + maybeFromNullableVO(source.series, (value) => CustomerInvoiceSerie.create(value)), + "serie", + errors + ); + + const invoiceNumber = extractOrPushError( + CustomerInvoiceNumber.create(source.invoice_number), + "invoice_number", + errors + ); + + const invoiceDate = extractOrPushError( + UtcDate.createFromISO(source.invoice_date), + "invoice_date", + errors + ); + + const operationDate = extractOrPushError( + maybeFromNullableVO(source.operation_date, (value) => UtcDate.createFromISO(value)), + "operation_date", + errors + ); + + const notes = extractOrPushError( + maybeFromNullableVO(source.notes, (value) => TextValue.create(value)), + "notes", + errors + ); + + const languageCode = extractOrPushError( + LanguageCode.create(source.language_code), + "language_code", + errors + ); + + const currencyCode = extractOrPushError( + CurrencyCode.create(source.currency_code), + "currency_code", + errors + ); + + const discountPercentage = extractOrPushError( + Percentage.create({ + value: source.discount_percentage_value, + scale: source.discount_percentage_scale, + }), + "discount_percentage", + errors + ); + + return { + invoiceId, + companyId, + customerId, + isProforma, + status, + series, + invoiceNumber, + invoiceDate, + operationDate, + notes, + languageCode, + currencyCode, + discountPercentage, + }; } public mapToDomain( @@ -59,135 +153,130 @@ export class CustomerInvoiceMapper try { const errors: ValidationErrorDetail[] = []; - const invoiceId = extractOrPushError(UniqueID.create(source.id), "id", errors); - const companyId = extractOrPushError( - UniqueID.create(source.company_id), - "company_id", - errors - ); + const attributes = this.mapAttributesToDomain(source, { errors, ...params }); - const isProforma = Boolean(source.is_proforma); + const requireIncludes = Boolean(params?.requireIncludes); + if (requireIncludes) { + if (!source.items) { + errors.push({ + path: "items", + message: "Items not included in query (requireIncludes=true)", + }); + } + if (!source.taxes) { + errors.push({ + path: "taxes", + message: "Taxes not included in query (requireIncludes=true)", + }); + } - const status = extractOrPushError( - CustomerInvoiceStatus.create(source.status), - "status", - errors - ); + if (attributes.isProforma && !source.current_customer) { + errors.push({ + path: "current_customer", + message: "Current customer not included in query (requireIncludes=true)", + }); + } + } - const series = extractOrPushError( - maybeFromNullableVO(source.series, (value) => CustomerInvoiceSerie.create(value)), - "serie", - errors - ); - - const invoiceNumber = extractOrPushError( - CustomerInvoiceNumber.create(source.invoice_number), - "invoice_number", - errors - ); - - const invoiceDate = extractOrPushError( - UtcDate.createFromISO(source.invoice_date), - "invoice_date", - errors - ); - - const operationDate = extractOrPushError( - maybeFromNullableVO(source.operation_date, (value) => UtcDate.createFromISO(value)), - "operation_date", - errors - ); - - const notes = extractOrPushError( - maybeFromNullableVO(source.notes, (value) => TextValue.create(value)), - "notes", - errors - ); - - const languageCode = extractOrPushError( - LanguageCode.create(source.language_code), - "language_code", - errors - ); - - const currencyCode = extractOrPushError( - CurrencyCode.create(source.currency_code), - "currency_code", - errors - ); - - const discountPercentage = extractOrPushError( - Percentage.create({ - value: source.discount_percentage_value, - scale: source.discount_percentage_scale, - }), - "discount_percentage", - errors - ); - - // Customer - const customerId = extractOrPushError( - UniqueID.create(source.customer_id), - "customer_id", - errors - ); - - // Recipient (customer data) (snapshot) - const recipient = this._recipientMapper.mapToDomain(source, { + // 3) Recipient (snapshot en la factura o include) + const recipientResult = this._recipientMapper.mapToDomain(source, { errors, + attributes, ...params, }); - // Mapear los items de la factura - const itemsOrResult = this._itemsMapper.mapArrayToDomain(source.items, { - parent: source, + if (recipientResult.isFailure) { + errors.push({ + path: "recipient", + message: recipientResult.error.message, + }); + } + + // 4) Items (colección) + const itemsResults = this._itemsMapper.mapArrayToDomain(source.items, { + requireIncludes, errors, + attributes, ...params, }); - // Mapear los impuestos - const taxesOrResult = this._taxesMapper.mapArrayToDomain(source.taxes, { - parent: source, + if (itemsResults.isFailure) { + errors.push({ + path: "items", + message: recipientResult.error.message, + }); + } + + // 5) Taxes (colección a nivel factura) + const taxesResults = this._taxesMapper.mapArrayToDomain(source.taxes, { errors, + attributes, ...params, }); + if (taxesResults.isFailure) { + errors.push({ + path: "taxes", + message: recipientResult.error.message, + }); + } + + // 6) Si hubo errores de mapeo, devolvemos colección de validación if (errors.length > 0) { return Result.fail( - new ValidationErrorCollection("Customer invoice item props mapping failed", errors) + new ValidationErrorCollection("Customer invoice mapping failed", errors) ); } + // 7) Construcción del agregado (Dominio) + + const recipient = recipientResult.data; + + const taxes = InvoiceTaxes.create({ + items: taxesResults.data.getAll(), + }); + + const items = CustomerInvoiceItems.create({ + languageCode: attributes.languageCode!, + currencyCode: attributes.currencyCode!, + items: itemsResults.data.getAll(), + }); + const invoiceProps: CustomerInvoiceProps = { - companyId: companyId!, + companyId: attributes.companyId!, - isProforma: isProforma, - status: status!, - series: series!, - invoiceNumber: invoiceNumber!, - invoiceDate: invoiceDate!, - operationDate: operationDate!, + isProforma: attributes.isProforma, + status: attributes.status!, + series: attributes.series!, + invoiceNumber: attributes.invoiceNumber!, + invoiceDate: attributes.invoiceDate!, + operationDate: attributes.operationDate!, - customerId: customerId!, + customerId: attributes.customerId!, recipient: recipient, - notes: notes!, + notes: attributes.notes!, - languageCode: languageCode!, - currencyCode: currencyCode!, + languageCode: attributes.languageCode!, + currencyCode: attributes.currencyCode!, - discountPercentage: discountPercentage!, + discountPercentage: attributes.discountPercentage!, - taxes: taxesOrResult, - - items: CustomerInvoiceItems.create({ - languageCode: languageCode!, - currencyCode: currencyCode!, - items: itemsOrResult.isSuccess ? itemsOrResult.data.getAll() : [], - }), + taxes, + items, }; - return CustomerInvoice.create(invoiceProps, invoiceId); + const createResult = CustomerInvoice.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); } @@ -262,9 +351,10 @@ export class CustomerInvoiceMapper discount_amount_scale: source.discountAmount.scale, taxable_amount_value: source.taxableAmount.value, - taxable_amount_scale: source.taxableAmount.value, - tax_amount_value: source.taxAmount.value, - tax_amount_scale: source.taxAmount.value, + taxable_amount_scale: source.taxableAmount.scale, + + taxes_amount_value: source.taxAmount.value, + taxes_amount_scale: source.taxAmount.scale, total_amount_value: 0, //total.amount, total_amount_scale: 2, //total.scale, diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/invoice-recipient.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/invoice-recipient.mapper.ts index 5d3f1127..09d2879f 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/invoice-recipient.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/invoice-recipient.mapper.ts @@ -9,69 +9,82 @@ import { maybeFromNullableVO, } from "@repo/rdx-ddd"; -import { MapperParamsType, ValidationErrorDetail, extractOrPushError } from "@erp/core/api"; -import { Maybe, isNullishOrEmpty } from "@repo/rdx-utils"; +import { + MapperParamsType, + ValidationErrorCollection, + ValidationErrorDetail, + extractOrPushError, +} from "@erp/core/api"; +import { Maybe, Result } from "@repo/rdx-utils"; import { InferCreationAttributes } from "sequelize"; -import { CustomerInvoice, InvoiceRecipient } from "../../domain"; +import { CustomerInvoice, CustomerInvoiceProps, InvoiceRecipient } from "../../domain"; import { CustomerInvoiceModel } from "../sequelize"; export class InvoiceRecipientMapper { - public mapToDomain(source: CustomerInvoiceModel, params?: MapperParamsType) { - const { errors } = params as { + public mapToDomain( + source: CustomerInvoiceModel, + params?: MapperParamsType + ): Result, Error> { + const { errors, attributes } = params as { errors: ValidationErrorDetail[]; + attributes: Partial; }; + const { isProforma } = attributes; + + const _name = isProforma ? source.current_customer.name : source.customer_name; + const _tin = isProforma ? source.current_customer.tin : source.customer_tin; + const _street = isProforma ? source.current_customer.street : source.customer_street; + const _street2 = isProforma ? source.current_customer.street2 : source.customer_street2; + const _city = isProforma ? source.current_customer.city : source.customer_city; + const _postal_code = isProforma + ? source.current_customer.postal_code + : source.customer_postal_code; + const _province = isProforma ? source.current_customer.province : source.customer_province; + const _country = isProforma ? source.current_customer.country : source.customer_country; + // Customer (snapshot) + const customerName = extractOrPushError(Name.create(_name), "customer_name", errors); - const customerName = extractOrPushError( - Name.create(source.customer_name), - "customer_name", - errors - ); - - const customerTin = extractOrPushError( - TINNumber.create(source.customer_tin), - "customer_tin", - errors - ); + const customerTin = extractOrPushError(TINNumber.create(_tin), "customer_tin", errors); const customerStreet = extractOrPushError( - maybeFromNullableVO(source.customer_street, (value) => Street.create(value)), + maybeFromNullableVO(_street, (value) => Street.create(value)), "customer_street", errors ); const customerStreet2 = extractOrPushError( - maybeFromNullableVO(source.customer_street2, (value) => Street.create(value)), + maybeFromNullableVO(_street2, (value) => Street.create(value)), "customer_street2", errors ); const customerCity = extractOrPushError( - maybeFromNullableVO(source.customer_city, (value) => City.create(value)), + maybeFromNullableVO(_city, (value) => City.create(value)), "customer_city", errors ); const customerProvince = extractOrPushError( - maybeFromNullableVO(source.customer_province, (value) => Province.create(value)), + maybeFromNullableVO(_province, (value) => Province.create(value)), "customer_province", errors ); const customerPostalCode = extractOrPushError( - maybeFromNullableVO(source.customer_postal_code, (value) => PostalCode.create(value)), + maybeFromNullableVO(_postal_code, (value) => PostalCode.create(value)), "customer_postal_code", errors ); const customerCountry = extractOrPushError( - maybeFromNullableVO(source.customer_country, (value) => Country.create(value)), + maybeFromNullableVO(_country, (value) => Country.create(value)), "customer_country", errors ); - const recipientOrError = InvoiceRecipient.create({ + const createResult = InvoiceRecipient.create({ name: customerName!, tin: customerTin!, street: customerStreet!, @@ -82,16 +95,21 @@ export class InvoiceRecipientMapper { country: customerCountry!, }); - return isNullishOrEmpty(recipientOrError) - ? Maybe.none() - : Maybe.some(recipientOrError.data); + 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)); } public mapToPersistence( source: InvoiceRecipient, params?: MapperParamsType ): Partial> { - 1; const { index, sourceParent } = params as { index: number; sourceParent: CustomerInvoice; diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/taxes.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/taxes.mapper.ts index 9e2dbd68..1098afd0 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/taxes.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/taxes.mapper.ts @@ -1,9 +1,104 @@ -import { MapperParamsType, Taxes } from "@erp/core/api"; +import { JsonTaxCatalogProvider } from "@erp/core"; +import { + MapperParamsType, + SequelizeMapper, + Tax, + Taxes, + ValidationErrorCollection, + ValidationErrorDetail, + extractOrPushError, +} from "@erp/core/api"; +import { Result } from "@repo/rdx-utils"; import { InferCreationAttributes } from "sequelize"; -import { CustomerInvoiceItemTaxModel, CustomerInvoiceTaxModel } from "../sequelize"; +import { CustomerInvoiceProps, InvoiceAmount } from "../../domain"; +import { InvoiceTax } from "../../domain/entities/invoice-taxes"; +import { + CustomerInvoiceItemTaxModel, + CustomerInvoiceTaxCreationAttributes, + CustomerInvoiceTaxModel, +} from "../sequelize"; -export class TaxesMapper { - public mapArrayToDomain(taxes: CustomerInvoiceTaxModel[], params?: MapperParamsType) {} +export class TaxesMapper extends SequelizeMapper< + CustomerInvoiceTaxModel, + CustomerInvoiceTaxCreationAttributes, + InvoiceTax +> { + private _taxCatalog: JsonTaxCatalogProvider; + + constructor(params: { + taxCatalog: JsonTaxCatalogProvider; + }) { + super(); + const { taxCatalog } = params; + this._taxCatalog = taxCatalog; + } + + public mapToDomain( + source: CustomerInvoiceTaxModel, + params?: MapperParamsType + ): Result { + const { errors, index, attributes } = params as { + index: number; + requireIncludes: boolean; + errors: ValidationErrorDetail[]; + attributes: Partial; + }; + + const tax = extractOrPushError( + Tax.createFromCode(source.tax_code, this._taxCatalog), + `taxes[${index}].tax_code`, + errors + ); + + const taxableAmount = extractOrPushError( + InvoiceAmount.create({ + value: source.taxable_amount_value, + currency_code: attributes.currencyCode?.code, + }), + `taxes[${index}].taxable_amount_value`, + errors + ); + + if (source.taxable_amount_scale !== InvoiceAmount.DEFAULT_SCALE) { + errors.push({ + path: `taxes[${index}].taxable_amount_scale`, + message: "Invalid taxable amount scale", + }); + } + + const taxesAmount = extractOrPushError( + InvoiceAmount.create({ + value: source.taxes_amount_value, + currency_code: attributes.currencyCode?.code, + }), + `taxes[${index}].taxes_amount_value`, + errors + ); + + if (source.taxes_amount_scale !== InvoiceAmount.DEFAULT_SCALE) { + errors.push({ + path: `taxes[${index}].taxes_amount_scale`, + message: "Invalid taxes amount scale", + }); + } + + // Creación del objeto de dominio + const createResult = InvoiceTax.create({ + tax: tax!, + taxableAmount: taxableAmount!, + taxesAmount: taxesAmount!, + }); + + if (createResult.isFailure) { + return Result.fail( + new ValidationErrorCollection("Invoice taxes creation failed", [ + { path: `taxes[${index}]`, message: createResult.error.message }, + ]) + ); + } + + return createResult; + } public mapToPersistence( source: Taxes, diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice-item-tax.model.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice-item-tax.model.ts index 0c1efa11..bc699142 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice-item-tax.model.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice-item-tax.model.ts @@ -27,8 +27,8 @@ export class CustomerInvoiceItemTaxModel extends Model< declare taxable_amount_scale: number; // Total tax amount / taxes total // 21,00 € - declare tax_amount_value: number; - declare tax_amount_scale: number; + declare taxes_amount_value: number; + declare taxes_amount_scale: number; // Relaciones declare item: NonAttribute; @@ -71,18 +71,20 @@ export default (database: Sequelize) => { allowNull: true, defaultValue: null, }, + taxable_amount_scale: { type: new DataTypes.SMALLINT(), allowNull: false, defaultValue: 2, }, - tax_amount_value: { + taxes_amount_value: { type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes allowNull: true, defaultValue: null, }, - tax_amount_scale: { + + taxes_amount_scale: { type: new DataTypes.SMALLINT(), allowNull: false, defaultValue: 2, diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice-tax.model.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice-tax.model.ts index a8846fe3..21fd2de4 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice-tax.model.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice-tax.model.ts @@ -27,8 +27,8 @@ export class CustomerInvoiceTaxModel extends Model< declare taxable_amount_scale: number; // Total tax amount / taxes total // 21,00 € - declare tax_amount_value: number; - declare tax_amount_scale: number; + declare taxes_amount_value: number; + declare taxes_amount_scale: number; // Relaciones declare invoice: NonAttribute; @@ -70,18 +70,20 @@ export default (database: Sequelize) => { allowNull: true, defaultValue: null, }, + taxable_amount_scale: { type: new DataTypes.SMALLINT(), allowNull: false, defaultValue: 2, }, - tax_amount_value: { + taxes_amount_value: { type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes allowNull: true, defaultValue: null, }, - tax_amount_scale: { + + taxes_amount_scale: { type: new DataTypes.SMALLINT(), allowNull: false, defaultValue: 2, diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.model.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.model.ts index f44b252f..00547f0a 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.model.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.model.ts @@ -19,7 +19,7 @@ import { export type CustomerInvoiceCreationAttributes = InferCreationAttributes< CustomerInvoiceModel, - { omit: "items" | "taxes" | "currentCustomer" } + { omit: "items" | "taxes" | "current_customer" } > & { items?: CustomerInvoiceItemCreationAttributes[]; taxes?: CustomerInvoiceTaxCreationAttributes[]; @@ -81,7 +81,7 @@ export class CustomerInvoiceModel extends Model< // Relaciones declare items: NonAttribute; declare taxes: NonAttribute; - declare currentCustomer: NonAttribute; + declare current_customer: NonAttribute; static associate(database: Sequelize) { const { @@ -92,7 +92,7 @@ export class CustomerInvoiceModel extends Model< } = database.models; CustomerInvoiceModel.belongsTo(CustomerModel, { - as: "currentCustomer", + as: "current_customer", foreignKey: "customer_id", constraints: false, }); diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts index 70e1d437..687b0e8f 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts @@ -159,7 +159,7 @@ export class CustomerInvoiceRepository query.include = [ { model: CustomerModel, - as: "currentCustomer", + as: "current_customer", required: false, // false => LEFT JOIN }, ];