From 8c867eb7f3a157b81b42050b81ebee928fa7b37c Mon Sep 17 00:00:00 2001 From: david Date: Wed, 10 Sep 2025 13:57:15 +0200 Subject: [PATCH] Facturas de cliente --- .../core/src/api/domain/value-objects/tax.ts | 16 ++- .../customer-invoice-item.ts | 116 +++++++++--------- .../src/api/domain/entities/index.ts | 2 +- .../entities/invoice-taxes/invoice-tax.ts | 29 ++--- .../api/domain/entities/item-taxes/index.ts | 2 + .../domain/entities/item-taxes/item-tax.ts | 36 ++++++ .../domain/entities/item-taxes/item-taxes.ts | 25 ++++ .../invoice-recipient/index.ts | 0 .../invoice-recipient/invoice-recipient.ts | 0 .../api/domain/value-objects/item-amount.ts | 2 +- .../api/domain/value-objects/item-discount.ts | 2 +- .../api/domain/value-objects/item-quantity.ts | 2 +- .../rdx-ddd/src/value-objects/money-value.ts | 16 +-- packages/rdx-utils/src/helpers/collection.ts | 9 ++ 14 files changed, 167 insertions(+), 90 deletions(-) create mode 100644 modules/customer-invoices/src/api/domain/entities/item-taxes/index.ts create mode 100644 modules/customer-invoices/src/api/domain/entities/item-taxes/item-tax.ts create mode 100644 modules/customer-invoices/src/api/domain/entities/item-taxes/item-taxes.ts rename modules/customer-invoices/src/api/domain/{entities => value-objects}/invoice-recipient/index.ts (100%) rename modules/customer-invoices/src/api/domain/{entities => value-objects}/invoice-recipient/invoice-recipient.ts (100%) diff --git a/modules/core/src/api/domain/value-objects/tax.ts b/modules/core/src/api/domain/value-objects/tax.ts index 83f1d304..1a43a463 100644 --- a/modules/core/src/api/domain/value-objects/tax.ts +++ b/modules/core/src/api/domain/value-objects/tax.ts @@ -1,5 +1,5 @@ import { TaxCatalogProvider } from "@erp/core"; -import { ValueObject } from "@repo/rdx-ddd"; +import { Percentage, ValueObject } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import * as z from "zod/v4"; @@ -26,6 +26,8 @@ export class Tax extends ValueObject { private static CODE_REGEX = /^[a-z0-9_:-]+$/; + private _percentage!: Percentage; + protected static validate(values: TaxProps) { const schema = z.object({ value: z @@ -96,6 +98,14 @@ export class Tax extends ValueObject { }); } + protected constructor(props: TaxProps) { + super(props); + this._percentage = Percentage.create({ + value: this.props.value, + scale: this.props.scale, + }).data; + } + get value(): number { return this.props.value; } @@ -109,6 +119,10 @@ export class Tax extends ValueObject { return this.props.code; } + get percentage(): Percentage { + return this._percentage; + } + getProps(): TaxProps { return this.props; } diff --git a/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.ts b/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.ts index d7eef312..544d1e81 100644 --- a/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.ts +++ b/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.ts @@ -1,12 +1,4 @@ -import { - CurrencyCode, - DomainEntity, - LanguageCode, - MoneyValue, - Percentage, - Taxes, - UniqueID, -} from "@repo/rdx-ddd"; +import { CurrencyCode, DomainEntity, LanguageCode, Percentage, UniqueID } from "@repo/rdx-ddd"; import { Maybe, Result } from "@repo/rdx-utils"; import { CustomerInvoiceItemDescription, @@ -14,14 +6,16 @@ import { ItemDiscount, ItemQuantity, } from "../../value-objects"; +import { ItemTaxes } from "../item-taxes"; export interface CustomerInvoiceItemProps { description: Maybe; quantity: Maybe; // Cantidad de unidades unitAmount: Maybe; // Precio unitario en la moneda de la factura + discountPercentage: Maybe; // % descuento - taxes: Taxes; + taxes: ItemTaxes; languageCode: LanguageCode; currencyCode: CurrencyCode; @@ -32,31 +26,26 @@ export interface ICustomerInvoiceItem { quantity: Maybe; // Cantidad de unidades unitAmount: Maybe; // Precio unitario en la moneda de la factura - subtotalAmount: ItemAmount; discountPercentage: Maybe; // % descuento - discountAmount: Maybe; - taxableAmount: ItemAmount; - taxesAmount: ItemAmount; - - totalAmount: ItemAmount; + taxes: ItemTaxes; languageCode: LanguageCode; currencyCode: CurrencyCode; - calculateSubtotal(): ItemAmount; + getSubtotalAmount(): ItemAmount; + getDiscountAmount(): ItemAmount; - calculateTotal(): ItemAmount; + getTaxableAmount(): ItemAmount; + getTaxesAmount(): ItemAmount; + getTotalAmount(): ItemAmount; } export class CustomerInvoiceItem extends DomainEntity implements ICustomerInvoiceItem { - private _subtotalAmount!: ItemAmount; - private _totalAmount!: ItemAmount; - public static create( props: CustomerInvoiceItemProps, id?: UniqueID @@ -67,10 +56,6 @@ export class CustomerInvoiceItem // ... // ... - // 🔹 Disparar evento de dominio "CustomerInvoiceItemCreatedEvent" - //const { customerInvoice } = props; - //user.addDomainEvent(new CustomerInvoiceAuthenticatedEvent(id, customerInvoice.toString())); - return Result.ok(item); } @@ -86,38 +71,22 @@ export class CustomerInvoiceItem return this.props.unitAmount; } - get subtotalAmount(): ItemAmount { - throw new Error("Not implemented"); - } - get discountPercentage(): Maybe { return this.props.discountPercentage; } - get discountAmount(): Maybe { - throw new Error("Not implemented"); - } - - get taxableAmount(): ItemAmount { - throw new Error("Not implemented"); - } - - get taxesAmount(): ItemAmount { - throw new Error("Not implemented"); - } - - get totalAmount(): ItemAmount { - throw new Error("Not implemented"); - } - - public get languageCode(): LanguageCode { + get languageCode(): LanguageCode { return this.props.languageCode; } - public get currencyCode(): CurrencyCode { + get currencyCode(): CurrencyCode { return this.props.currencyCode; } + get taxes(): ItemTaxes { + return this.props.taxes; + } + getProps(): CustomerInvoiceItemProps { return this.props; } @@ -126,17 +95,52 @@ export class CustomerInvoiceItem return this.getProps(); } - calculateSubtotal(): ItemAmount { - throw new Error("Not implemented"); + public getSubtotalAmount(): ItemAmount { + const curCode = this.currencyCode.code; + const quantity = this.quantity.match( + (quantity) => quantity, + () => ItemQuantity.zero() + ); + const unitAmount = this.unitAmount.match( + (unitAmount) => unitAmount, + () => ItemAmount.zero(curCode) + ); - /*const unitPrice = this.unitPrice.isSome() - ? this.unitPrice.unwrap() - : CustomerInvoiceItemUnitPrice.zero(); - return this.unitPrice.multiply(this.quantity.toNumber()); // Precio unitario * Cantidad*/ + return unitAmount.multiply(quantity); } - calculateTotal(): ItemAmount { - throw new Error("Not implemented"); - //return this.subtotalPrice.subtract(this.subtotalPrice.percentage(this.discount.toNumber())); + public getDiscountAmount(): ItemAmount { + const discount = this.discountPercentage.match( + (percentage) => percentage, + () => ItemDiscount.zero() + ); + return this.getSubtotalAmount().percentage(discount); + } + + public getTaxableAmount(): ItemAmount { + return this.getSubtotalAmount().subtract(this.getDiscountAmount()); + } + + public getTaxesAmount(): ItemAmount { + return this._getTaxesAmount(this.getTaxableAmount()); + } + + public getTotalAmount(): ItemAmount { + const taxableAmount = this.getTaxableAmount(); + return taxableAmount.add(this._getTaxesAmount(taxableAmount)); + } + + public getAllAmounts() { + return { + subtotalAmount: this.getSubtotalAmount(), + discountAmount: this.getDiscountAmount(), + taxableAmount: this.getTaxableAmount(), + taxesAmount: this.getTaxesAmount(), + totalAmount: this.getTotalAmount(), + }; + } + + private _getTaxesAmount(_taxableAmount: ItemAmount): ItemAmount { + return this.props.taxes.getTaxesAmount(_taxableAmount); } } diff --git a/modules/customer-invoices/src/api/domain/entities/index.ts b/modules/customer-invoices/src/api/domain/entities/index.ts index 03dcb0d1..165a0998 100644 --- a/modules/customer-invoices/src/api/domain/entities/index.ts +++ b/modules/customer-invoices/src/api/domain/entities/index.ts @@ -1,3 +1,3 @@ export * from "./customer-invoice-items"; -export * from "./invoice-recipient"; export * from "./invoice-taxes"; +export * from "./item-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 index 6cc42ce5..665b471c 100644 --- 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 @@ -1,5 +1,5 @@ import { Tax } from "@erp/core/api"; -import { ValueObject } from "@repo/rdx-ddd"; +import { DomainEntity, UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { InvoiceAmount } from "../../value-objects/invoice-amount"; @@ -9,28 +9,15 @@ export interface InvoiceTaxProps { taxesAmount: InvoiceAmount; } -export class InvoiceTax extends ValueObject { - protected static validate(values: InvoiceTaxProps) { - return Result.ok(values); - } +export class InvoiceTax extends DomainEntity { + static create(props: InvoiceTaxProps, id?: UniqueID): Result { + const invoiceTax = new InvoiceTax(props, id); - static create(values: InvoiceTaxProps): Result { - const valueIsValid = InvoiceTax.validate(values); + // Reglas de negocio / validaciones + // ... + // ... - 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); + return Result.ok(invoiceTax); } public get tax(): Tax { diff --git a/modules/customer-invoices/src/api/domain/entities/item-taxes/index.ts b/modules/customer-invoices/src/api/domain/entities/item-taxes/index.ts new file mode 100644 index 00000000..f5bef2b9 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/entities/item-taxes/index.ts @@ -0,0 +1,2 @@ +export * from "./item-tax"; +export * from "./item-taxes"; diff --git a/modules/customer-invoices/src/api/domain/entities/item-taxes/item-tax.ts b/modules/customer-invoices/src/api/domain/entities/item-taxes/item-tax.ts new file mode 100644 index 00000000..7ece3ac2 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/entities/item-taxes/item-tax.ts @@ -0,0 +1,36 @@ +import { Tax } from "@erp/core/api"; +import { DomainEntity, UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; +import { ItemAmount } from "../../value-objects"; + +export interface ItemTaxProps { + tax: Tax; +} + +export class ItemTax extends DomainEntity { + static create(props: ItemTaxProps, id?: UniqueID): Result { + const invoiceTax = new ItemTax(props, id); + + // Reglas de negocio / validaciones + // ... + // ... + + return Result.ok(invoiceTax); + } + + public get tax(): Tax { + return this.props.tax; + } + + getProps(): ItemTaxProps { + return this.props; + } + + toPrimitive() { + return this.getProps(); + } + + public getTaxAmount(taxableAmount: ItemAmount): ItemAmount { + return taxableAmount.percentage(this.tax.percentage); + } +} diff --git a/modules/customer-invoices/src/api/domain/entities/item-taxes/item-taxes.ts b/modules/customer-invoices/src/api/domain/entities/item-taxes/item-taxes.ts new file mode 100644 index 00000000..b44dd0c1 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/entities/item-taxes/item-taxes.ts @@ -0,0 +1,25 @@ +import { Collection } from "@repo/rdx-utils"; +import { ItemAmount } from "../../value-objects"; +import { ItemTax } from "./item-tax"; + +export interface ItemTaxesProps { + items?: ItemTax[]; +} + +export class ItemTaxes extends Collection { + constructor(props: ItemTaxesProps) { + const { items = [] } = props; + super(items); + } + + public static create(props: ItemTaxesProps): ItemTaxes { + return new ItemTaxes(props); + } + + public getTaxesAmount(taxableAmount: ItemAmount): ItemAmount { + return this.getAll().reduce( + (total, tax) => total.add(tax.getTaxAmount(taxableAmount)), + ItemAmount.zero(taxableAmount.currencyCode) + ); + } +} diff --git a/modules/customer-invoices/src/api/domain/entities/invoice-recipient/index.ts b/modules/customer-invoices/src/api/domain/value-objects/invoice-recipient/index.ts similarity index 100% rename from modules/customer-invoices/src/api/domain/entities/invoice-recipient/index.ts rename to modules/customer-invoices/src/api/domain/value-objects/invoice-recipient/index.ts diff --git a/modules/customer-invoices/src/api/domain/entities/invoice-recipient/invoice-recipient.ts b/modules/customer-invoices/src/api/domain/value-objects/invoice-recipient/invoice-recipient.ts similarity index 100% rename from modules/customer-invoices/src/api/domain/entities/invoice-recipient/invoice-recipient.ts rename to modules/customer-invoices/src/api/domain/value-objects/invoice-recipient/invoice-recipient.ts diff --git a/modules/customer-invoices/src/api/domain/value-objects/item-amount.ts b/modules/customer-invoices/src/api/domain/value-objects/item-amount.ts index e07d4f03..bd0ab95d 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/item-amount.ts +++ b/modules/customer-invoices/src/api/domain/value-objects/item-amount.ts @@ -19,6 +19,6 @@ export class ItemAmount extends MoneyValue { value: 0, currency_code, }; - return ItemAmount.create(props); + return ItemAmount.create(props).data; } } diff --git a/modules/customer-invoices/src/api/domain/value-objects/item-discount.ts b/modules/customer-invoices/src/api/domain/value-objects/item-discount.ts index 268220df..a3f53686 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/item-discount.ts +++ b/modules/customer-invoices/src/api/domain/value-objects/item-discount.ts @@ -14,6 +14,6 @@ export class ItemDiscount extends Percentage { } static zero() { - return ItemDiscount.create({ value: 0 }); + return ItemDiscount.create({ value: 0 }).data; } } diff --git a/modules/customer-invoices/src/api/domain/value-objects/item-quantity.ts b/modules/customer-invoices/src/api/domain/value-objects/item-quantity.ts index 35ab91e5..27a064ac 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/item-quantity.ts +++ b/modules/customer-invoices/src/api/domain/value-objects/item-quantity.ts @@ -13,6 +13,6 @@ export class ItemQuantity extends Quantity { } static zero() { - return ItemQuantity.create({ value: 0 }); + return ItemQuantity.create({ value: 0 }).data; } } diff --git a/packages/rdx-ddd/src/value-objects/money-value.ts b/packages/rdx-ddd/src/value-objects/money-value.ts index dd716bef..19bce3ef 100644 --- a/packages/rdx-ddd/src/value-objects/money-value.ts +++ b/packages/rdx-ddd/src/value-objects/money-value.ts @@ -27,7 +27,7 @@ export interface MoneyValueProps { export interface IMoneyValue { value: number; scale: number; - currency: Dinero.Currency; + currencyCode: string; getProps(): MoneyValueProps; convertScale(newScale: number): MoneyValue; @@ -78,8 +78,8 @@ export class MoneyValue extends ValueObject implements IMoneyVa return this.dinero.getAmount() / 10 ** this.dinero.getPrecision(); // ** => Math.pow } - get currency(): CurrencyData { - return this.dinero.getCurrency(); + get currencyCode(): string { + return this.dinero.getCurrency().toString(); } get scale(): number { @@ -103,7 +103,7 @@ export class MoneyValue extends ValueObject implements IMoneyVa return { value: this.value, scale: this.scale, - currency_code: this.currency, + currency_code: this.currencyCode, }; } @@ -120,7 +120,7 @@ export class MoneyValue extends ValueObject implements IMoneyVa return new MoneyValue({ value: this.dinero.add(addend.dinero).getAmount(), scale: this.scale, - currency_code: this.currency, + currency_code: this.currencyCode, }); } @@ -128,7 +128,7 @@ export class MoneyValue extends ValueObject implements IMoneyVa return new MoneyValue({ value: this.dinero.subtract(subtrahend.dinero).getAmount(), scale: this.scale, - currency_code: this.currency, + currency_code: this.currencyCode, }); } @@ -205,12 +205,12 @@ export class MoneyValue extends ValueObject implements IMoneyVa */ format(locale: string): string { const value = this.value; - const currency = this.currency; + const currencyCode = this.currencyCode; const scale = this.scale; return new Intl.NumberFormat(locale, { style: "currency", - currency: currency, + currency: currencyCode, minimumFractionDigits: scale, maximumFractionDigits: scale, useGrouping: true, diff --git a/packages/rdx-utils/src/helpers/collection.ts b/packages/rdx-utils/src/helpers/collection.ts index c6e81c90..aa352bc0 100644 --- a/packages/rdx-utils/src/helpers/collection.ts +++ b/packages/rdx-utils/src/helpers/collection.ts @@ -82,6 +82,15 @@ export class Collection { return this.totalItems; } + /** + * Performs the specified action for each element in an array. + * @param callbackfn A function that accepts up to three arguments. forEach calls the callbackfn function one time for each element in the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value. + */ + forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void { + this.items.forEach(callbackfn); + } + /** * Aplica una función a cada elemento y devuelve un nuevo array con los resultados. * @param callback - Función transformadora.