From acd61cf6eb673251015899639a0dc5d05792bb40 Mon Sep 17 00:00:00 2001 From: david Date: Thu, 19 Feb 2026 15:51:30 +0100 Subject: [PATCH] . --- .../di/issued-invoice-snapshot-builders.di.ts | 6 +- .../snapshot-builders/full/index.ts | 4 +- .../issued-invoice-full-snapshot-builder.ts | 80 +++---------- .../issued-invoice-full-snapshot.interface.ts | 28 ++--- ...ued-invoice-items-full-snapshot-builder.ts | 94 +++++---------- ...invoice-recipient-full-snapshot-builder.ts | 2 +- ...oice-recipient-full-snapshot.interface.ts} | 0 ...ued-invoice-tax-full-snapshot-interface.ts | 17 +++ ...ued-invoice-taxes-full-snapshot-builder.ts | 41 +++++++ .../src/api/application/proformas/index.ts | 2 +- .../mappers/create-proforma-props.mapper.ts | 2 - .../aggregates/issued-invoice.aggregate.ts | 17 +++ .../domain/issued-invoices/entities/index.ts | 3 +- .../issued-invoice-items.collection.ts | 6 +- .../issued-invoice-taxes.collection.ts | 13 -- .../entities/issued-invoice-taxes/index.ts | 2 + .../issued-invoice-tax.entity.ts | 2 +- .../issued-invoice-taxes.collection.ts | 25 ++++ .../models/customer-invoice.model.ts | 46 +++++++ .../sequelize-issued-invoice-domain.mapper.ts | 59 +++++++-- ...elize-issued-invoice-item-domain.mapper.ts | 57 +++++---- .../get-issued-invoice-by-id.response.dto.ts | 5 +- packages/rdx-ddd/src/helpers/normalizers.ts | 112 +++++++++++++++++- .../rdx-ddd/src/value-objects/money-value.ts | 22 +++- .../rdx-ddd/src/value-objects/percentage.ts | 2 + .../rdx-ddd/src/value-objects/quantity.ts | 4 + 26 files changed, 444 insertions(+), 207 deletions(-) rename modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/{issued-invoice-recipient-full-snapshot.interfce.ts => issued-invoice-recipient-full-snapshot.interface.ts} (100%) create mode 100644 modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-tax-full-snapshot-interface.ts create mode 100644 modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-taxes-full-snapshot-builder.ts delete mode 100644 modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-taxes.collection.ts create mode 100644 modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-taxes/index.ts rename modules/customer-invoices/src/api/domain/issued-invoices/entities/{ => issued-invoice-taxes}/issued-invoice-tax.entity.ts (97%) create mode 100644 modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-taxes/issued-invoice-taxes.collection.ts diff --git a/modules/customer-invoices/src/api/application/issued-invoices/di/issued-invoice-snapshot-builders.di.ts b/modules/customer-invoices/src/api/application/issued-invoices/di/issued-invoice-snapshot-builders.di.ts index d7547abe..1e290510 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/di/issued-invoice-snapshot-builders.di.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/di/issued-invoice-snapshot-builders.di.ts @@ -5,6 +5,7 @@ import { IssuedInvoiceFullSnapshotBuilder, IssuedInvoiceItemsFullSnapshotBuilder, IssuedInvoiceRecipientFullSnapshotBuilder, + IssuedInvoiceTaxesFullSnapshotBuilder, IssuedInvoiceVerifactuFullSnapshotBuilder, } from "../snapshot-builders/full"; import { @@ -16,6 +17,8 @@ import { export function buildIssuedInvoiceSnapshotBuilders() { const itemsBuilder = new IssuedInvoiceItemsFullSnapshotBuilder(); + const taxesBuilder = new IssuedInvoiceTaxesFullSnapshotBuilder(); + const recipientBuilder = new IssuedInvoiceRecipientFullSnapshotBuilder(); const verifactuBuilder = new IssuedInvoiceVerifactuFullSnapshotBuilder(); @@ -23,7 +26,8 @@ export function buildIssuedInvoiceSnapshotBuilders() { const fullSnapshotBuilder = new IssuedInvoiceFullSnapshotBuilder( itemsBuilder, recipientBuilder, - verifactuBuilder + verifactuBuilder, + taxesBuilder ); const listSnapshotBuilder = new IssuedInvoiceListItemSnapshotBuilder(); 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 009bc206..6036a5f9 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 @@ -2,7 +2,9 @@ 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.interface"; export * from "./issued-invoice-recipient-full-snapshot-builder"; +export * from "./issued-invoice-tax-full-snapshot-interface"; +export * from "./issued-invoice-taxes-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 b5547912..608bb048 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,11 +1,12 @@ import type { ISnapshotBuilder } from "@erp/core/api"; import { maybeToEmptyString } from "@repo/rdx-ddd"; -import { InvoiceAmount, type IssuedInvoice } from "../../../../domain"; +import type { IssuedInvoice } from "../../../../domain"; import type { IIssuedInvoiceFullSnapshot } 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 { IIssuedInvoiceTaxesFullSnapshotBuilder } from "./issued-invoice-taxes-full-snapshot-builder"; import type { IIssuedInvoiceVerifactuFullSnapshotBuilder } from "./issued-invoice-verifactu-full-snapshot-builder"; export interface IIssuedInvoiceFullSnapshotBuilder @@ -15,13 +16,15 @@ export class IssuedInvoiceFullSnapshotBuilder implements IIssuedInvoiceFullSnaps constructor( private readonly itemsBuilder: IIssuedInvoiceItemsFullSnapshotBuilder, private readonly recipientBuilder: IIssuedInvoiceRecipientFullSnapshotBuilder, - private readonly verifactuBuilder: IIssuedInvoiceVerifactuFullSnapshotBuilder + private readonly verifactuBuilder: IIssuedInvoiceVerifactuFullSnapshotBuilder, + private readonly taxesBuilder: IIssuedInvoiceTaxesFullSnapshotBuilder ) {} toOutput(invoice: IssuedInvoice): IIssuedInvoiceFullSnapshot { const items = this.itemsBuilder.toOutput(invoice.items); const recipient = this.recipientBuilder.toOutput(invoice); const verifactu = this.verifactuBuilder.toOutput(invoice); + const taxes = this.taxesBuilder.toOutput(invoice.taxes); const payment = invoice.paymentMethod.match( (payment) => { @@ -34,57 +37,11 @@ export class IssuedInvoiceFullSnapshotBuilder implements IIssuedInvoiceFullSnaps () => 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.taxes().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", + is_proforma: "false", invoice_number: invoice.invoiceNumber.toString(), status: invoice.status.toPrimitive(), series: maybeToEmptyString(invoice.series, (value) => value.toString()), @@ -104,25 +61,26 @@ export class IssuedInvoiceFullSnapshotBuilder implements IIssuedInvoiceFullSnaps payment_method: payment, - subtotal_amount: allAmounts.subtotalAmount.toObjectString(), - items_discount_amount: allAmounts.itemDiscountAmount.toObjectString(), + subtotal_amount: invoice.subtotalAmount.toObjectString(), + items_discount_amount: invoice.itemsDiscountAmount.toObjectString(), - discount_percentage: invoice.globalDiscountPercentage.toObjectString(), - discount_amount: allAmounts.globalDiscountAmount.toObjectString(), + global_discount_percentage: invoice.globalDiscountPercentage.toObjectString(), + global_discount_amount: invoice.globalDiscountAmount.toObjectString(), - taxable_amount: allAmounts.taxableAmount.toObjectString(), + total_discount_amount: invoice.totalDiscountAmount.toObjectString(), - iva_amount: totalIvaAmount.toObjectString(), - rec_amount: totalRecAmount.toObjectString(), - retention_amount: totalRetentionAmount.toObjectString(), + taxable_amount: invoice.taxableAmount.toObjectString(), - taxes_amount: allAmounts.taxesAmount.toObjectString(), - total_amount: allAmounts.totalAmount.toObjectString(), + iva_amount: invoice.ivaAmount.toObjectString(), + rec_amount: invoice.recAmount.toObjectString(), + retention_amount: invoice.retentionAmount.toObjectString(), - taxes: invoiceTaxes, + taxes_amount: invoice.taxesAmount.toObjectString(), + total_amount: invoice.totalAmount.toObjectString(), + + taxes, verifactu, - items, metadata: { diff --git a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-full-snapshot.interface.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-full-snapshot.interface.ts index 29e5ce99..4be17677 100644 --- a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-full-snapshot.interface.ts +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-full-snapshot.interface.ts @@ -1,5 +1,6 @@ import type { IIssuedInvoiceItemFullSnapshot } from "./issued-invoice-item-full-snapshot.interface"; -import type { IIssuedInvoiceRecipientFullSnapshot } from "./issued-invoice-recipient-full-snapshot.interfce"; +import type { IIssuedInvoiceRecipientFullSnapshot } from "./issued-invoice-recipient-full-snapshot.interface"; +import type { IIssuedInvoiceTaxFullSnapshot } from "./issued-invoice-tax-full-snapshot-interface"; import type { IIssuedInvoiceVerifactuFullSnapshot } from "./issued-invoice-verifactu-full-snapshot.interface"; export interface IIssuedInvoiceFullSnapshot { @@ -30,10 +31,11 @@ export interface IIssuedInvoiceFullSnapshot { }; 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 }; + items_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 }; + total_discount_amount: { value: string; scale: string; currency_code: string }; taxable_amount: { value: string; scale: string; currency_code: string }; @@ -44,23 +46,7 @@ export interface IIssuedInvoiceFullSnapshot { 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 }; - }>; + taxes: IIssuedInvoiceTaxFullSnapshot[]; verifactu: IIssuedInvoiceVerifactuFullSnapshot; items: IIssuedInvoiceItemFullSnapshot[]; 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 0a8d0f6a..48396fb3 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,5 +1,10 @@ import type { ISnapshotBuilder } from "@erp/core/api"; -import { maybeToEmptyString } from "@repo/rdx-ddd"; +import { + maybeToEmptyMoneyObjectString, + maybeToEmptyPercentageObjectString, + maybeToEmptyQuantityObjectString, + maybeToEmptyString, +} from "@repo/rdx-ddd"; import type { IssuedInvoiceItem, IssuedInvoiceItems } from "../../../../domain"; @@ -16,75 +21,38 @@ export class IssuedInvoiceItemsFullSnapshotBuilder id: invoiceItem.id.toPrimitive(), is_valued: String(invoiceItem.isValued), position: String(index), - description: maybeToEmptyString(invoiceItem.description, (value) => value.toPrimitive()), - quantity: invoiceItem.quantity.match( - (quantity) => quantity.toObjectString(), - () => ({ value: "", scale: "" }) + description: maybeToEmptyString(invoiceItem.description, (value) => value.toString()), + + quantity: maybeToEmptyQuantityObjectString(invoiceItem.quantity), + unit_amount: maybeToEmptyMoneyObjectString(invoiceItem.unitAmount), + + subtotal_amount: maybeToEmptyMoneyObjectString(invoiceItem.subtotalAmount), + + discount_percentage: maybeToEmptyPercentageObjectString(invoiceItem.itemDiscountPercentage), + discount_amount: maybeToEmptyMoneyObjectString(invoiceItem.itemDiscountAmount), + + global_discount_percentage: maybeToEmptyPercentageObjectString( + invoiceItem.globalDiscountPercentage ), + global_discount_amount: maybeToEmptyMoneyObjectString(invoiceItem.globalDiscountAmount), - unit_amount: invoiceItem.unitAmount.match( - (unitAmount) => unitAmount.toObjectString(), - () => ({ value: "", scale: "", currency_code: "" }) - ), + taxable_amount: maybeToEmptyMoneyObjectString(invoiceItem.taxableAmount), - subtotal_amount: invoiceItem.subtotalAmount.toObjectString(), + iva_code: maybeToEmptyString(invoiceItem.ivaCode), + iva_percentage: maybeToEmptyPercentageObjectString(invoiceItem.ivaPercentage), + iva_amount: maybeToEmptyMoneyObjectString(invoiceItem.ivaAmount), - discount_percentage: invoiceItem.itemDiscountPercentage.match( - (discountPercentage) => discountPercentage.toObjectString(), - () => ({ value: "", scale: "" }) - ), + rec_code: maybeToEmptyString(invoiceItem.recCode), + rec_percentage: maybeToEmptyPercentageObjectString(invoiceItem.recPercentage), + rec_amount: maybeToEmptyMoneyObjectString(invoiceItem.recAmount), - discount_amount: invoiceItem.itemDiscountAmount.toObjectString(), + retention_code: maybeToEmptyString(invoiceItem.retentionCode), + retention_percentage: maybeToEmptyPercentageObjectString(invoiceItem.retentionPercentage), + retention_amount: maybeToEmptyMoneyObjectString(invoiceItem.retentionAmount), - global_discount_percentage: invoiceItem.globalDiscountPercentage.match( - (discountPercentage) => discountPercentage.toObjectString(), - () => ({ value: "", scale: "" }) - ), - - global_discount_amount: invoiceItem.globalDiscountAmount.toObjectString(), - - taxable_amount: invoiceItem.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: invoiceItem.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: invoiceItem.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: invoiceItem.retentionAmount.toObjectString(), - - taxes_amount: invoiceItem.taxesAmount.toObjectString(), - - total_amount: invoiceItem.totalAmount.toObjectString(), + taxes_amount: maybeToEmptyMoneyObjectString(invoiceItem.taxesAmount), + total_amount: maybeToEmptyMoneyObjectString(invoiceItem.totalAmount), }; } 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 ee2d09dc..d77f2583 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 @@ -3,7 +3,7 @@ import { DomainValidationError, maybeToEmptyString } from "@repo/rdx-ddd"; import type { InvoiceRecipient, IssuedInvoice } from "../../../../domain"; -import type { IIssuedInvoiceRecipientFullSnapshot } from "./issued-invoice-recipient-full-snapshot.interfce"; +import type { IIssuedInvoiceRecipientFullSnapshot } from "./issued-invoice-recipient-full-snapshot.interface"; export interface IIssuedInvoiceRecipientFullSnapshotBuilder extends ISnapshotBuilder {} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-recipient-full-snapshot.interfce.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-recipient-full-snapshot.interface.ts similarity index 100% rename from modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-recipient-full-snapshot.interfce.ts rename to modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-recipient-full-snapshot.interface.ts diff --git a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-tax-full-snapshot-interface.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-tax-full-snapshot-interface.ts new file mode 100644 index 00000000..b93c5ed3 --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-tax-full-snapshot-interface.ts @@ -0,0 +1,17 @@ +export interface IIssuedInvoiceTaxFullSnapshot { + 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 }; +} diff --git a/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-taxes-full-snapshot-builder.ts b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-taxes-full-snapshot-builder.ts new file mode 100644 index 00000000..2b6b5dfe --- /dev/null +++ b/modules/customer-invoices/src/api/application/issued-invoices/snapshot-builders/full/issued-invoice-taxes-full-snapshot-builder.ts @@ -0,0 +1,41 @@ +import type { ISnapshotBuilder } from "@erp/core/api"; +import { + maybeToEmptyMoneyObjectString, + maybeToEmptyPercentageObjectString, + maybeToEmptyString, +} from "@repo/rdx-ddd"; + +import type { IssuedInvoiceTax, IssuedInvoiceTaxes } from "../../../../domain"; + +import type { IIssuedInvoiceTaxFullSnapshot } from "./issued-invoice-tax-full-snapshot-interface"; + +export interface IIssuedInvoiceTaxesFullSnapshotBuilder + extends ISnapshotBuilder {} + +export class IssuedInvoiceTaxesFullSnapshotBuilder + implements IIssuedInvoiceTaxesFullSnapshotBuilder +{ + private mapItem(invoiceTax: IssuedInvoiceTax, index: number): IIssuedInvoiceTaxFullSnapshot { + return { + taxable_amount: invoiceTax.taxableAmount.toObjectString(), + + iva_code: invoiceTax.ivaCode.toString(), + iva_percentage: invoiceTax.ivaPercentage.toObjectString(), + iva_amount: invoiceTax.ivaAmount.toObjectString(), + + rec_code: maybeToEmptyString(invoiceTax.recCode), + rec_percentage: maybeToEmptyPercentageObjectString(invoiceTax.recPercentage), + rec_amount: maybeToEmptyMoneyObjectString(invoiceTax.recAmount), + + retention_code: maybeToEmptyString(invoiceTax.retentionCode), + retention_percentage: maybeToEmptyPercentageObjectString(invoiceTax.retentionPercentage), + retention_amount: maybeToEmptyMoneyObjectString(invoiceTax.retentionAmount), + + taxes_amount: invoiceTax.taxesAmount.toObjectString(), + }; + } + + toOutput(invoiceTaxes: IssuedInvoiceTaxes): IIssuedInvoiceTaxFullSnapshot[] { + return invoiceTaxes.map((item, index) => this.mapItem(item, index)); + } +} diff --git a/modules/customer-invoices/src/api/application/proformas/index.ts b/modules/customer-invoices/src/api/application/proformas/index.ts index 680f4d36..57d301f9 100644 --- a/modules/customer-invoices/src/api/application/proformas/index.ts +++ b/modules/customer-invoices/src/api/application/proformas/index.ts @@ -1,7 +1,7 @@ export * from "./application-models"; export * from "./di"; export * from "./dtos"; -export * from "./mappers"; +//export * from "./mappers"; export * from "./repositories"; export * from "./services"; export * from "./snapshot-builders"; diff --git a/modules/customer-invoices/src/api/application/proformas/mappers/create-proforma-props.mapper.ts b/modules/customer-invoices/src/api/application/proformas/mappers/create-proforma-props.mapper.ts index af296d79..1baf4c5f 100644 --- a/modules/customer-invoices/src/api/application/proformas/mappers/create-proforma-props.mapper.ts +++ b/modules/customer-invoices/src/api/application/proformas/mappers/create-proforma-props.mapper.ts @@ -17,8 +17,6 @@ import { Maybe, Result } from "@repo/rdx-utils"; import type { CreateProformaItemRequestDTO, CreateProformaRequestDTO } from "../../../../common"; import { - CustomerInvoiceItems, - type IProformaProps, InvoiceNumber, InvoicePaymentMethod, type InvoiceRecipient, diff --git a/modules/customer-invoices/src/api/domain/issued-invoices/aggregates/issued-invoice.aggregate.ts b/modules/customer-invoices/src/api/domain/issued-invoices/aggregates/issued-invoice.aggregate.ts index e41e79e2..212fe755 100644 --- a/modules/customer-invoices/src/api/domain/issued-invoices/aggregates/issued-invoice.aggregate.ts +++ b/modules/customer-invoices/src/api/domain/issued-invoices/aggregates/issued-invoice.aggregate.ts @@ -55,6 +55,11 @@ export type IssuedInvoiceProps = { totalDiscountAmount: InvoiceAmount; taxableAmount: InvoiceAmount; + + ivaAmount: InvoiceAmount; + recAmount: InvoiceAmount; + retentionAmount: InvoiceAmount; + taxesAmount: InvoiceAmount; totalAmount: InvoiceAmount; @@ -188,6 +193,18 @@ export class IssuedInvoice extends AggregateRoot { 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(): InvoiceAmount { return this.props.taxesAmount; } 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 index cfbc7c8b..8abeeec0 100644 --- a/modules/customer-invoices/src/api/domain/issued-invoices/entities/index.ts +++ b/modules/customer-invoices/src/api/domain/issued-invoices/entities/index.ts @@ -1,4 +1,3 @@ export * from "./issued-invoice-items"; -export * from "./issued-invoice-tax.entity"; -export * from "./issued-invoice-taxes.collection"; +export * from "./issued-invoice-taxes"; export * from "./verifactu-record.entity"; 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 index 6308bcfb..f59c85ea 100644 --- 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 @@ -1,7 +1,7 @@ import type { CurrencyCode, LanguageCode, Percentage } from "@repo/rdx-ddd"; import { Collection } from "@repo/rdx-utils"; -import { InvoiceAmount, ItemDiscountPercentage } from "../../../common"; +import { ItemDiscountPercentage } from "../../../common"; import type { IssuedInvoiceItem } from "./issued-invoice-item.entity"; @@ -57,10 +57,10 @@ export class IssuedInvoiceItems extends Collection { return super.add(item); } - public getTotalAmount(): InvoiceAmount { + /*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-taxes.collection.ts b/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-taxes.collection.ts deleted file mode 100644 index ade4ce6d..00000000 --- a/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-taxes.collection.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Collection } from "@repo/rdx-utils"; - -import type { IssuedInvoiceTax } from "./issued-invoice-tax.entity"; - -export class IssuedInvoiceTaxes extends Collection { - constructor(items: IssuedInvoiceTax[] = []) { - super(items); - } - - public static create(items: IssuedInvoiceTax[] = []): IssuedInvoiceTaxes { - return new IssuedInvoiceTaxes(items); - } -} diff --git a/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-taxes/index.ts b/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-taxes/index.ts new file mode 100644 index 00000000..6e80f7f8 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-taxes/index.ts @@ -0,0 +1,2 @@ +export * from "./issued-invoice-tax.entity"; +export * from "./issued-invoice-taxes.collection"; diff --git a/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-tax.entity.ts b/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-taxes/issued-invoice-tax.entity.ts similarity index 97% rename from modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-tax.entity.ts rename to modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-taxes/issued-invoice-tax.entity.ts index 2a3152c6..c626c06d 100644 --- a/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-tax.entity.ts +++ b/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-taxes/issued-invoice-tax.entity.ts @@ -1,7 +1,7 @@ import { DomainEntity, type Percentage, type UniqueID } from "@repo/rdx-ddd"; import { type Maybe, Result } from "@repo/rdx-utils"; -import type { InvoiceAmount } from "../../common"; +import type { InvoiceAmount } from "../../../common"; export type IssuedInvoiceTaxProps = { taxableAmount: InvoiceAmount; diff --git a/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-taxes/issued-invoice-taxes.collection.ts b/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-taxes/issued-invoice-taxes.collection.ts new file mode 100644 index 00000000..e7086d2e --- /dev/null +++ b/modules/customer-invoices/src/api/domain/issued-invoices/entities/issued-invoice-taxes/issued-invoice-taxes.collection.ts @@ -0,0 +1,25 @@ +import type { CurrencyCode, LanguageCode } from "@repo/rdx-ddd"; +import { Collection } from "@repo/rdx-utils"; + +import type { IssuedInvoiceTax } from "./issued-invoice-tax.entity"; + +export type IssuedInvoiceTaxesProps = { + taxes?: IssuedInvoiceTax[]; + languageCode: LanguageCode; + currencyCode: CurrencyCode; +}; + +export class IssuedInvoiceTaxes extends Collection { + private _languageCode!: LanguageCode; + private _currencyCode!: CurrencyCode; + + constructor(props: IssuedInvoiceTaxesProps) { + super(props.taxes ?? []); + this._languageCode = props.languageCode; + this._currencyCode = props.currencyCode; + } + + public static create(props: IssuedInvoiceTaxesProps): IssuedInvoiceTaxes { + return new IssuedInvoiceTaxes(props); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice.model.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice.model.ts index 9f5c9a47..62bfaf56 100644 --- a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice.model.ts +++ b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/models/customer-invoice.model.ts @@ -88,6 +88,18 @@ export class CustomerInvoiceModel extends Model< declare taxable_amount_value: number; declare taxable_amount_scale: number; + // IVA amount + declare iva_amount_value: number; + declare iva_amount_scale: number; + + // Recargo de equivalencia amount + declare rec_amount_value: number; + declare rec_amount_scale: number; + + // Retention amount + declare retention_amount_value: number; + declare retention_amount_scale: number; + // Total taxes amount / taxes total declare taxes_amount_value: number; declare taxes_amount_scale: number; @@ -350,6 +362,40 @@ export default (database: Sequelize) => { defaultValue: 2, }, + iva_amount_value: { + type: DataTypes.BIGINT, + allowNull: true, + defaultValue: null, + }, + + iva_amount_scale: { + type: DataTypes.SMALLINT, + allowNull: false, + defaultValue: 4, + }, + + rec_amount_value: { + type: DataTypes.BIGINT, + allowNull: true, + defaultValue: null, + }, + rec_amount_scale: { + type: DataTypes.SMALLINT, + allowNull: false, + defaultValue: 4, + }, + + retention_amount_value: { + type: DataTypes.BIGINT, + allowNull: true, + defaultValue: null, + }, + retention_amount_scale: { + type: DataTypes.SMALLINT, + allowNull: false, + defaultValue: 4, + }, + taxes_amount_value: { type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes allowNull: false, diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-domain.mapper.ts index 909c623a..3b7886ae 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-domain.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-domain.mapper.ts @@ -216,6 +216,33 @@ export class SequelizeIssuedInvoiceDomainMapper errors ); + const ivaAmount = extractOrPushError( + InvoiceAmount.create({ + value: raw.iva_amount_value, + currency_code: currencyCode?.code, + }), + "iva_amount_value", + errors + ); + + const recAmount = extractOrPushError( + InvoiceAmount.create({ + value: raw.rec_amount_value, + currency_code: currencyCode?.code, + }), + "rec_amount_value", + errors + ); + + const retentionAmount = extractOrPushError( + InvoiceAmount.create({ + value: raw.retention_amount_value, + currency_code: currencyCode?.code, + }), + "retention_amount_value", + errors + ); + const taxesAmount = extractOrPushError( InvoiceAmount.create({ value: raw.taxes_amount_value, @@ -258,6 +285,9 @@ export class SequelizeIssuedInvoiceDomainMapper globalDiscountAmount, totalDiscountAmount, taxableAmount, + ivaAmount, + recAmount, + retentionAmount, taxesAmount, totalAmount, }; @@ -318,18 +348,19 @@ export class SequelizeIssuedInvoiceDomainMapper // 6) Construcción del agregado (Dominio) + const verifactu = verifactuResult.data; + const items = IssuedInvoiceItems.create({ + items: itemsResults.data.getAll(), languageCode: attributes.languageCode!, currencyCode: attributes.currencyCode!, globalDiscountPercentage: attributes.globalDiscountPercentage!, - items: itemsResults.data.getAll(), }); const taxes = IssuedInvoiceTaxes.create({ + taxes: taxesResults.data.getAll(), languageCode: attributes.languageCode!, currencyCode: attributes.currencyCode!, - globalDiscountPercentage: attributes.globalDiscountPercentage!, - taxes: taxesResults.data.getAll(), }); const invoiceProps: IssuedInvoiceProps = { @@ -360,14 +391,18 @@ export class SequelizeIssuedInvoiceDomainMapper totalDiscountAmount: attributes.totalDiscountAmount!, taxableAmount: attributes.taxableAmount!, + ivaAmount: attributes.ivaAmount!, + recAmount: attributes.recAmount!, + retentionAmount: attributes.retentionAmount!, + taxesAmount: attributes.taxesAmount!, totalAmount: attributes.totalAmount!, paymentMethod: attributes.paymentMethod!, items, - taxes: taxesResults.data, - verifactu: verifactuResult.data, + taxes, + verifactu, }; const createResult = IssuedInvoice.create(invoiceProps, attributes.invoiceId); @@ -479,11 +514,17 @@ export class SequelizeIssuedInvoiceDomainMapper subtotal_amount_value: source.subtotalAmount.value, subtotal_amount_scale: source.subtotalAmount.scale, - discount_percentage_value: source.globalDiscountPercentage.toPrimitive().value, - discount_percentage_scale: source.globalDiscountPercentage.toPrimitive().scale, + items_discount_amount_value: source.itemsDiscountAmount.value, + items_discount_amount_scale: source.itemsDiscountAmount.scale, - discount_amount_value: source.globalDiscountAmount.value, - discount_amount_scale: source.globalDiscountAmount.scale, + global_discount_percentage_value: source.globalDiscountPercentage.toPrimitive().value, + global_discount_percentage_scale: source.globalDiscountPercentage.toPrimitive().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, diff --git a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-item-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-item-domain.mapper.ts index 8caa52ad..762e23de 100644 --- a/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-item-domain.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/issued-invoices/persistence/sequelize/mappers/domain/sequelize-issued-invoice-item-domain.mapper.ts @@ -339,10 +339,6 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe maybeToNullable(source.subtotalAmount, (v) => v.toPrimitive().scale) ?? ItemAmount.DEFAULT_SCALE, - // Te has quedado aquí --- IGNORE --- - // !!!!!!!!!!!!!!!!!!! - - // discount_percentage_value: maybeToNullable( source.itemDiscountPercentage, (v) => v.toPrimitive().value @@ -351,29 +347,46 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe maybeToNullable(source.itemDiscountPercentage, (v) => v.toPrimitive().scale) ?? ItemDiscountPercentage.DEFAULT_SCALE, - discount_amount_value: source.itemDiscountAmount.value, - discount_amount_scale: source.itemDiscountAmount.scale, + discount_amount_value: maybeToNullable( + source.itemDiscountAmount, + (v) => v.toPrimitive().value + ), + discount_amount_scale: + maybeToNullable(source.itemDiscountPercentage, (v) => v.toPrimitive().scale) ?? + ItemDiscountPercentage.DEFAULT_SCALE, - // global_discount_percentage_value: maybeToNullable( source.globalDiscountPercentage, (v) => v.toPrimitive().value ), - global_discount_percentage_scale: maybeToNullable(source.globalDiscountPercentage, (v) => v.toPrimitive().scale) ?? ItemDiscountPercentage.DEFAULT_SCALE, - global_discount_amount_value: source.globalDiscountAmount.value, - global_discount_amount_scale: source.globalDiscountAmount.scale, + global_discount_amount_value: maybeToNullable( + source.globalDiscountAmount, + (v) => v.toPrimitive().value + ), + global_discount_amount_scale: + maybeToNullable(source.globalDiscountAmount, (v) => v.toPrimitive().scale) ?? + ItemAmount.DEFAULT_SCALE, + + total_discount_amount_value: maybeToNullable( + source.totalDiscountAmount, + (v) => v.toPrimitive().value + ), + total_discount_amount_scale: + maybeToNullable(source.totalDiscountAmount, (v) => v.toPrimitive().scale) ?? + ItemAmount.DEFAULT_SCALE, + + // Te has quedado aquí --- IGNORE --- + // !!!!!!!!!!!!!!!!!!! // - 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, + taxable_amount_value: maybeToNullable(source.taxableAmount, (v) => v.toPrimitive().value), + taxable_amount_scale: + maybeToNullable(source.taxableAmount, (v) => v.toPrimitive().scale) ?? + ItemAmount.DEFAULT_SCALE, // IVA iva_code: maybeToNullableString(source.ivaCode), @@ -410,12 +423,16 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe maybeToNullable(source.retentionAmount, (v) => v.toPrimitive().scale) ?? 4, // - taxes_amount_value: source.taxesAmount.value, - taxes_amount_scale: source.taxesAmount.scale, + taxes_amount_value: maybeToNullable(source.taxesAmount, (v) => v.toPrimitive().value), + taxes_amount_scale: + maybeToNullable(source.taxesAmount, (v) => v.toPrimitive().scale) ?? + ItemAmount.DEFAULT_SCALE, // - total_amount_value: source.totalAmount.value, - total_amount_scale: source.totalAmount.scale, + total_amount_value: maybeToNullable(source.totalAmount, (v) => v.toPrimitive().value), + total_amount_scale: + maybeToNullable(source.totalAmount, (v) => v.toPrimitive().scale) ?? + ItemAmount.DEFAULT_SCALE, }); } } diff --git a/modules/customer-invoices/src/common/dto/response/issued-invoices/get-issued-invoice-by-id.response.dto.ts b/modules/customer-invoices/src/common/dto/response/issued-invoices/get-issued-invoice-by-id.response.dto.ts index dcbd790e..bf699365 100644 --- a/modules/customer-invoices/src/common/dto/response/issued-invoices/get-issued-invoice-by-id.response.dto.ts +++ b/modules/customer-invoices/src/common/dto/response/issued-invoices/get-issued-invoice-by-id.response.dto.ts @@ -62,8 +62,9 @@ export const GetIssuedInvoiceByIdResponseSchema = z.object({ subtotal_amount: MoneySchema, items_discount_amount: MoneySchema, - discount_percentage: PercentageSchema, - discount_amount: MoneySchema, + global_discount_percentage: PercentageSchema, + global_discount_amount: MoneySchema, + total_discount_amount: MoneySchema, taxable_amount: MoneySchema, iva_amount: MoneySchema, rec_amount: MoneySchema, diff --git a/packages/rdx-ddd/src/helpers/normalizers.ts b/packages/rdx-ddd/src/helpers/normalizers.ts index 762460a5..d49d7b9c 100644 --- a/packages/rdx-ddd/src/helpers/normalizers.ts +++ b/packages/rdx-ddd/src/helpers/normalizers.ts @@ -1,8 +1,9 @@ -// application/shared/normalizers.ts // Normalizadores y adaptadores DTO -> Maybe/VO import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils"; +import { MoneyValue, Percentage, Quantity } from "../value-objects"; + /** any | null | undefined -> Maybe usando validación */ export function maybeFromNullableResult( input: any, @@ -13,6 +14,115 @@ export function maybeFromNullableResult( return value.isSuccess ? Result.ok(Maybe.some(value.data)) : Result.fail(value.error); } +/** + * Serializa un Maybe aplicando una política de objeto vacío. + * + * @internal + */ +function maybeToEmptyObjectString( + maybe: Maybe, + emptyObject: R, + defaultSerializer: (value: T) => R, + map?: (value: T) => R +): R { + if (!maybe || maybe.isNone()) { + return emptyObject; + } + + const value = maybe.unwrap(); + return map ? map(value) : defaultSerializer(value); +} + +/** + * Serializa un `Maybe` a un objeto de transporte normalizado. + * + * - Si el `Maybe` es `None`, devuelve un objeto vacío: + * `{ value: "", scale: "", currency_code: "" }` + * - Si es `Some`, aplica la función `map` si está declarada o + * aplica el método `toObjectString()` del `MoneyValue`. + * + * Motivación: + * - Evita devolver `null` en la capa de transporte. + * - Garantiza una estructura estable para consumidores (API / frontend). + * - Centraliza la política de normalización de importes opcionales. + * + * @typeParam T - Tipo interno del MoneyValue/Amount en dominio. + * @param maybe - Instancia `Maybe` que envuelve el Amount de dominio. + * @param map - Función que transforma el Amount de dominio al objeto de transporte. + * @returns Objeto normalizado de importe listo para transporte. + */ +export function maybeToEmptyMoneyObjectString( + maybe: Maybe, + map?: (value: MoneyValue) => { value: string; scale: string; currency_code: string } +): { value: string; scale: string; currency_code: string } { + return maybeToEmptyObjectString( + maybe, + MoneyValue.EMPTY_MONEY_OBJECT, + (value) => value.toObjectString(), + map + ); +} + +/** + * Serializa un `Maybe` a un objeto de transporte normalizado. + * + * - Si el `Maybe` es `None`, devuelve un objeto vacío: + * `{ value: "", scale: "" }` + * - Si es `Some`, aplica la función `map` si está declarada o + * aplica el método `toObjectString()` del `Percentage`. + * + * Motivación: + * - Evita devolver `null` en la capa de transporte. + * - Garantiza una estructura estable para consumidores (API / frontend). + * - Centraliza la política de normalización de porcentajes opcionales. + * + * @typeParam T - Tipo interno del Percentage en dominio. + * @param maybe - Instancia `Maybe` que envuelve el Percentage de dominio. + * @param map - Función que transforma el Percentage de dominio al objeto de transporte. + * @returns Objeto normalizado de porcentaje listo para transporte. + */ +export function maybeToEmptyPercentageObjectString( + maybe: Maybe, + map?: (value: Percentage) => { value: string; scale: string } +): { value: string; scale: string } { + return maybeToEmptyObjectString( + maybe, + Percentage.EMPTY_PERCENTAGE_OBJECT, + (value) => value.toObjectString(), + map + ); +} + +/** + * Serializa un `Maybe` a un objeto de transporte normalizado. + * + * - Si el `Maybe` es `None`, devuelve un objeto vacío: + * `{ value: "", scale: "" }` + * - Si es `Some`, aplica la función `map` si está declarada o + * aplica el método `toObjectString()` del `Quantity`. + * + * Motivación: + * - Evita devolver `null` en la capa de transporte. + * - Garantiza una estructura estable para consumidores (API / frontend). + * - Centraliza la política de normalización de cantidades opcionales. + * + * @typeParam T - Tipo interno del Quantity en dominio. + * @param maybe - Instancia `Maybe` que envuelve el Quantity de dominio. + * @param map - Función que transforma el Quantity de dominio al objeto de transporte. + * @returns Objeto normalizado de cantidad listo para transporte. + */ +export function maybeToEmptyQuantityObjectString( + maybe: Maybe, + map?: (value: Quantity) => { value: string; scale: string } +): { value: string; scale: string } { + return maybeToEmptyObjectString( + maybe, + Quantity.EMPTY_QUANTITY_OBJECT, + (value) => value.toObjectString(), + map + ); +} + /** string | null | undefined -> Maybe (trim, vacío => None) */ export function maybeFromNullableOrEmptyString(input?: string | null): Maybe { if (isNullishOrEmpty(input)) return Maybe.none(); diff --git a/packages/rdx-ddd/src/value-objects/money-value.ts b/packages/rdx-ddd/src/value-objects/money-value.ts index b34c4b1e..339ae14b 100644 --- a/packages/rdx-ddd/src/value-objects/money-value.ts +++ b/packages/rdx-ddd/src/value-objects/money-value.ts @@ -1,7 +1,8 @@ import { Result } from "@repo/rdx-utils"; -import DineroFactory, { Currency, Dinero } from "dinero.js"; -import { Percentage } from "./percentage"; -import { Quantity } from "./quantity"; +import DineroFactory, { type Currency, type Dinero } from "dinero.js"; + +import type { Percentage } from "./percentage"; +import type { Quantity } from "./quantity"; import { ValueObject } from "./value-object"; const DEFAULT_SCALE = 2; @@ -45,6 +46,8 @@ export interface IMoneyValue { hasSameCurrency(comparator: MoneyValue): boolean; hasSameAmount(comparator: MoneyValue): boolean; format(locale: string): string; + + toObjectString(): { value: string; scale: string; currency_code: string }; } export class MoneyValue extends ValueObject implements IMoneyValue { @@ -52,6 +55,7 @@ export class MoneyValue extends ValueObject implements IMoneyVa static DEFAULT_SCALE = DEFAULT_SCALE; static DEFAULT_CURRENCY_CODE = DEFAULT_CURRENCY_CODE; + static EMPTY_MONEY_OBJECT = { value: "", scale: "", currency_code: "" }; static create({ value, currency_code, scale }: MoneyValueProps) { const props = { @@ -95,13 +99,13 @@ export class MoneyValue extends ValueObject implements IMoneyVa } /** Reconstruye el VO desde la cadena persistida */ - static fromPersistence(value: string): MoneyValue { + /*static fromPersistence(value: string): MoneyValue { const [currencyCode, amountStr, scaleStr] = value.split(":"); const amount = Number.parseInt(amountStr, 10); const scale = Number.parseInt(scaleStr, 10); const currency = currencyCode; return new MoneyValue({ value: amount, scale, currency_code: currency }); - } + }*/ toPrimitive() { return { @@ -111,6 +115,14 @@ export class MoneyValue extends ValueObject implements IMoneyVa }; } + toObjectString() { + return { + value: String(this.value), + scale: String(this.scale), + currency_code: this.currencyCode, + }; + } + convertScale(newScale: number, roundingMode: RoundingMode = "HALF_UP"): MoneyValue { const _newDinero = this.dinero.convertPrecision(newScale, roundingMode); return new MoneyValue({ diff --git a/packages/rdx-ddd/src/value-objects/percentage.ts b/packages/rdx-ddd/src/value-objects/percentage.ts index 7695a752..23508754 100644 --- a/packages/rdx-ddd/src/value-objects/percentage.ts +++ b/packages/rdx-ddd/src/value-objects/percentage.ts @@ -26,6 +26,8 @@ export class Percentage extends ValueObject { static MIN_SCALE = DEFAULT_MIN_SCALE; static MAX_SCALE = DEFAULT_MAX_SCALE; + static EMPTY_PERCENTAGE_OBJECT = { value: "", scale: "" }; + protected static validate(values: PercentageProps) { const schema = z.object({ value: z.number().int().min(Percentage.MIN_VALUE, "La cantidad no puede ser negativa."), diff --git a/packages/rdx-ddd/src/value-objects/quantity.ts b/packages/rdx-ddd/src/value-objects/quantity.ts index 33b04b9c..957e6a31 100644 --- a/packages/rdx-ddd/src/value-objects/quantity.ts +++ b/packages/rdx-ddd/src/value-objects/quantity.ts @@ -1,6 +1,8 @@ import { Result } from "@repo/rdx-utils"; import { z } from "zod/v4"; + import { translateZodValidationError } from "../helpers"; + import { ValueObject } from "./value-object"; const DEFAULT_SCALE = 2; @@ -13,6 +15,8 @@ export interface QuantityProps { } export class Quantity extends ValueObject { + static EMPTY_QUANTITY_OBJECT = { value: "", scale: "" }; + protected static validate(values: QuantityProps) { const schema = z.object({ value: z.number().int(),