From efcae31500ec27825f7721633fb5d6e9e3efe8b2 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 12 Oct 2025 11:14:33 +0200 Subject: [PATCH] Facturas de cliente --- .../customer-invoice-items.full.presenter.ts | 2 +- .../domain/customer-invoice.full.presenter.ts | 19 ++++-- .../api/domain/aggregates/customer-invoice.ts | 30 ++++++-- .../customer-invoice-item.ts | 68 ++++++++++++++++++- .../customer-invoice-items.ts | 58 ++++++++++++++++ .../entities/invoice-taxes/invoice-taxes.ts | 44 +++++++----- .../domain/entities/item-taxes/item-taxes.ts | 8 ++- 7 files changed, 196 insertions(+), 33 deletions(-) diff --git a/modules/customer-invoices/src/api/application/presenters/domain/customer-invoice-items.full.presenter.ts b/modules/customer-invoices/src/api/application/presenters/domain/customer-invoice-items.full.presenter.ts index 63799652..dc7592cb 100644 --- a/modules/customer-invoices/src/api/application/presenters/domain/customer-invoice-items.full.presenter.ts +++ b/modules/customer-invoices/src/api/application/presenters/domain/customer-invoice-items.full.presenter.ts @@ -17,7 +17,7 @@ export class CustomerInvoiceItemsFullPresenter extends Presenter { return { id: invoiceItem.id.toPrimitive(), - isNonValued: String(invoiceItem.isNonValued), + is_non_valued: String(invoiceItem.isNonValued), position: String(index), description: toEmptyString(invoiceItem.description, (value) => value.toPrimitive()), diff --git a/modules/customer-invoices/src/api/application/presenters/domain/customer-invoice.full.presenter.ts b/modules/customer-invoices/src/api/application/presenters/domain/customer-invoice.full.presenter.ts index f5c520cd..3bc1498a 100644 --- a/modules/customer-invoices/src/api/application/presenters/domain/customer-invoice.full.presenter.ts +++ b/modules/customer-invoices/src/api/application/presenters/domain/customer-invoice.full.presenter.ts @@ -35,6 +35,17 @@ export class CustomerInvoiceFullPresenter extends Presenter< () => undefined ); + const invoiceTaxes = invoice.getTaxes().map((taxItem) => { + console.log(taxItem); + return { + tax_code: taxItem.tax.code, + taxable_amount: taxItem.taxableAmount.toObjectString(), + taxes_amount: taxItem.taxesAmount.toObjectString(), + }; + }); + + console.log(invoiceTaxes); + return { id: invoice.id.toString(), company_id: invoice.companyId.toString(), @@ -56,13 +67,7 @@ export class CustomerInvoiceFullPresenter extends Presenter< customer_id: invoice.customerId.toString(), recipient, - taxes: invoice.taxes.map((taxItem) => { - return { - tax_code: taxItem.tax.code, - taxable_amount: taxItem.taxableAmount.convertScale(2).toObjectString(), - taxes_amount: taxItem.taxesAmount.convertScale(2).toObjectString(), - }; - }), + taxes: invoiceTaxes, payment_method: payment, 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 cf05c287..6a6b573b 100644 --- a/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts +++ b/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts @@ -9,7 +9,7 @@ import { UtcDate, } from "@repo/rdx-ddd"; import { Maybe, Result } from "@repo/rdx-utils"; -import { CustomerInvoiceItems, InvoicePaymentMethod } from "../entities"; +import { CustomerInvoiceItems, InvoicePaymentMethod, InvoiceTaxTotal } from "../entities"; import { CustomerInvoiceNumber, CustomerInvoiceSerie, @@ -68,6 +68,8 @@ export interface ICustomerInvoice { getTaxesAmount(): InvoiceAmount; getTotalAmount(): InvoiceAmount; + getTaxes(): InvoiceTaxTotal[]; + issueInvoice(newInvoiceNumber: CustomerInvoiceNumber): Result; } @@ -198,10 +200,6 @@ export class CustomerInvoice return this._items; } - public get taxes() { - return this.items.getTaxesAmountByTaxes(); - } - public get hasRecipient() { return this.recipient.isSome(); } @@ -224,7 +222,9 @@ export class CustomerInvoice private _getTaxesAmount(taxableAmount: InvoiceAmount): InvoiceAmount { let amount = InvoiceAmount.zero(this.currencyCode.code); - for (const taxItem of this.taxes) { + const itemTaxes = this.items.getTaxesAmountByTaxes(); + + for (const taxItem of itemTaxes) { amount = amount.add(taxItem.taxesAmount); } return amount; @@ -262,6 +262,24 @@ export class CustomerInvoice return this._getTotalAmount(taxableAmount, taxesAmount); } + public getTaxes(): InvoiceTaxTotal[] { + const itemTaxes = this.items.getTaxesAmountByTaxes(); + + return itemTaxes.map((item) => { + return { + tax: item.tax, + taxableAmount: InvoiceAmount.create({ + value: item.taxableAmount.convertScale(2).value, + currency_code: this.currencyCode.code, + }).data, + taxesAmount: InvoiceAmount.create({ + value: item.taxesAmount.convertScale(2).value, + currency_code: this.currencyCode.code, + }).data, + }; + }); + } + public getAllAmounts() { const subtotalAmount = this.getSubtotalAmount(); const discountAmount = this._getDiscountAmount(subtotalAmount); 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 62a80778..57a48703 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 @@ -6,7 +6,7 @@ import { ItemDiscount, ItemQuantity, } from "../../value-objects"; -import { ItemTaxes } from "../item-taxes"; +import { ItemTaxTotal, ItemTaxes } from "../item-taxes"; export interface CustomerInvoiceItemProps { description: Maybe; @@ -109,6 +109,12 @@ export class CustomerInvoiceItem return this.getProps(); } + /** + * @private + * @summary Calcula el importe de descuento a partir del subtotal y el porcentaje. + * @param subtotalAmount - Importe subtotal. + * @returns El importe de descuento calculado. + */ private _getDiscountAmount(subtotalAmount: ItemAmount): ItemAmount { const discount = this.discountPercentage.match( (percentage) => percentage, @@ -117,18 +123,44 @@ export class CustomerInvoiceItem return subtotalAmount.percentage(discount); } + /** + * @private + * @summary Calcula el importe imponible restando el descuento al subtotal. + * @param subtotalAmount - Importe subtotal. + * @param discountAmount - Importe de descuento. + * @returns El importe imponible resultante. + */ private _getTaxableAmount(subtotalAmount: ItemAmount, discountAmount: ItemAmount): ItemAmount { return subtotalAmount.subtract(discountAmount); } + /** + * @private + * @summary Calcula el importe total de impuestos sobre la base imponible. + * @param taxableAmount - Importe imponible. + * @returns El importe de impuestos calculado. + */ private _getTaxesAmount(taxableAmount: ItemAmount): ItemAmount { return this.props.taxes.getTaxesAmount(taxableAmount); } + /** + * @private + * @summary Calcula el importe total sumando base imponible e impuestos. + * @param taxableAmount - Importe imponible. + * @param taxesAmount - Importe de impuestos. + * @returns El importe total del ítem. + */ private _getTotalAmount(taxableAmount: ItemAmount, taxesAmount: ItemAmount): ItemAmount { return taxableAmount.add(taxesAmount); } + /** + * @summary Calcula el subtotal del ítem (cantidad × importe unitario). + * @returns Un `ItemAmount` con el subtotal del ítem. + * @remarks + * Si la cantidad o el importe unitario no están definidos, se asumen valores cero. + */ public getSubtotalAmount(): ItemAmount { const curCode = this.currencyCode.code; const quantity = this.quantity.match( @@ -143,18 +175,34 @@ export class CustomerInvoiceItem return unitAmount.multiply(quantity); } + /** + * @summary Calcula el importe total de descuento del ítem. + * @returns Un `ItemAmount` con el importe descontado. + */ public getDiscountAmount(): ItemAmount { return this._getDiscountAmount(this.getSubtotalAmount()); } + /** + * @summary Calcula el importe imponible (subtotal − descuento). + * @returns Un `ItemAmount` con la base imponible del ítem. + */ public getTaxableAmount(): ItemAmount { return this._getTaxableAmount(this.getSubtotalAmount(), this.getDiscountAmount()); } + /** + * @summary Calcula el importe total de impuestos aplicados al ítem. + * @returns Un `ItemAmount` con el total de impuestos. + */ public getTaxesAmount(): ItemAmount { return this._getTaxesAmount(this.getTaxableAmount()); } + /** + * @summary Calcula el importe total final del ítem (base imponible + impuestos). + * @returns Un `ItemAmount` con el importe total. + */ public getTotalAmount(): ItemAmount { const taxableAmount = this.getTaxableAmount(); const taxesAmount = this._getTaxesAmount(taxableAmount); @@ -162,10 +210,26 @@ export class CustomerInvoiceItem return this._getTotalAmount(taxableAmount, taxesAmount); } - public getTaxesAmountByTaxes() { + /** + * @summary Obtiene los importes de impuestos agrupados por tipo de impuesto. + * @returns Una colección con la base imponible y el importe de impuestos por cada tipo. + */ + public getTaxesAmountByTaxes(): ItemTaxTotal[] { return this.taxes.getTaxesAmountByTaxes(this.getTaxableAmount()); } + /** + * @summary Devuelve todos los importes calculados del ítem en un único objeto. + * @returns Un objeto con las propiedades: + * - `subtotalAmount` + * - `discountAmount` + * - `taxableAmount` + * - `taxesAmount` + * - `totalAmount` + * @remarks + * Este método es útil para mostrar todos los cálculos en la interfaz de usuario + * o serializar el ítem con sus valores calculados. + */ public getAllAmounts() { const subtotalAmount = this.getSubtotalAmount(); const discountAmount = this._getDiscountAmount(subtotalAmount); diff --git a/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-items.ts b/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-items.ts index 8cdb5e13..8ee0689d 100644 --- a/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-items.ts +++ b/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-items.ts @@ -2,6 +2,7 @@ import { Tax } from "@erp/core/api"; import { CurrencyCode, LanguageCode } from "@repo/rdx-ddd"; import { Collection } from "@repo/rdx-utils"; import { ItemAmount } from "../../value-objects"; +import { ItemTaxes } from "../item-taxes"; import { CustomerInvoiceItem } from "./customer-invoice-item"; export interface CustomerInvoiceItemsProps { @@ -25,6 +26,15 @@ export class CustomerInvoiceItems extends Collection { return new CustomerInvoiceItems(props); } + /** + * @summary Añade un nuevo ítem a la colección. + * @param item - El ítem de factura a añadir. + * @returns `true` si el ítem fue añadido correctamente; `false` si fue rechazado. + * @remarks + * Sólo se aceptan ítems cuyo `LanguageCode` y `CurrencyCode` coincidan con + * los de la colección. Si no coinciden, el método devuelve `false` sin modificar + * la colección. + */ add(item: CustomerInvoiceItem): boolean { // Antes de añadir un nuevo item, debo comprobar que el item a añadir // tiene el mismo "currencyCode" y "languageCode" que la colección de items. @@ -37,6 +47,10 @@ export class CustomerInvoiceItems extends Collection { return super.add(item); } + /** + * @summary Calcula el subtotal de todos los ítems sin descuentos ni impuestos. + * @returns Un `ItemAmount` con el subtotal total de la colección en su moneda. + */ public getSubtotalAmount(): ItemAmount { return this.getAll().reduce( (total, tax) => total.add(tax.getSubtotalAmount()), @@ -44,6 +58,10 @@ export class CustomerInvoiceItems extends Collection { ); } + /** + * @summary Calcula el importe total de descuentos aplicados a todos los ítems. + * @returns Un `ItemAmount` con el importe total de descuentos. + */ public getDiscountAmount(): ItemAmount { return this.getAll().reduce( (total, item) => total.add(item.getDiscountAmount()), @@ -51,6 +69,10 @@ export class CustomerInvoiceItems extends Collection { ); } + /** + * @summary Calcula el importe imponible total (base antes de impuestos). + * @returns Un `ItemAmount` con el total imponible. + */ public getTaxableAmount(): ItemAmount { return this.getAll().reduce( (total, item) => total.add(item.getTaxableAmount()), @@ -58,6 +80,10 @@ export class CustomerInvoiceItems extends Collection { ); } + /** + * @summary Calcula el importe total de impuestos de todos los ítems. + * @returns Un `ItemAmount` con la suma de todos los impuestos aplicados. + */ public getTaxesAmount(): ItemAmount { return this.getAll().reduce( (total, item) => total.add(item.getTaxesAmount()), @@ -65,6 +91,10 @@ export class CustomerInvoiceItems extends Collection { ); } + /** + * @summary Calcula el importe total final de todos los ítems (subtotal -descuentos + impuestos). + * @returns Un `ItemAmount` con el total global de la colección. + */ public getTotalAmount(): ItemAmount { return this.getAll().reduce( (total, item) => total.add(item.getTotalAmount()), @@ -72,6 +102,18 @@ export class CustomerInvoiceItems extends Collection { ); } + /** + * @summary Agrupa los importes imponibles e impuestos por tipo de impuesto. + * @returns Un array con objetos que contienen: + * - `tax`: El tipo de impuesto. + * - `taxableAmount`: El total de base imponible asociada a ese impuesto. + * - `taxesAmount`: El importe total de impuestos calculado. + * @remarks + * Los impuestos se agrupan por su `tax.code` (código fiscal). + * Si existen varios impuestos con el mismo código, sus importes se agregan. + * En caso de conflicto de datos en impuestos con mismo código, prevalece + * el primer `Tax` encontrado. + */ public getTaxesAmountByTaxes(): Array<{ tax: Tax; taxableAmount: ItemAmount; @@ -105,4 +147,20 @@ export class CustomerInvoiceItems extends Collection { return Array.from(resultMap.values()); } + + /** + * @summary Obtiene la lista de impuestos únicos aplicados en todos los ítems. + * @returns Un objeto `ItemTaxes` que contiene todos los impuestos distintos. + * @remarks + * Los impuestos se deduplican por su código (`tax.code`). + * Si existen varios impuestos con el mismo código, el último encontrado + * sobrescribirá los anteriores sin generar error. + */ + public getTaxes(): ItemTaxes { + const taxes = this.getAll() + .flatMap((item) => item.taxes.getAll()) + .reduce((map, tax) => map.set(tax.code, tax), new Map()); + + return ItemTaxes.create([...taxes.values()]); + } } 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 index 27ac32e5..d1ce77da 100644 --- 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 @@ -1,31 +1,43 @@ -import { Collection } from "@repo/rdx-utils"; +import { Tax, Taxes } from "@erp/core/api"; import { InvoiceAmount } from "../../value-objects"; -import { InvoiceTax } from "./invoice-tax"; -export interface InvoiceTaxesProps { - items?: InvoiceTax[]; -} +export type InvoiceTaxTotal = { + tax: Tax; + taxableAmount: InvoiceAmount; + taxesAmount: InvoiceAmount; +}; -export class InvoiceTaxes extends Collection { - constructor(props: InvoiceTaxesProps) { - const { items = [] } = props; - super(items); - } - - public static create(props: InvoiceTaxesProps): InvoiceTaxes { - return new InvoiceTaxes(props); +export class InvoiceTaxes extends Taxes { + constructor(items: Tax[] = [], totalItems: number | null = null) { + super(items, totalItems); } public getTaxesAmount(taxableAmount: InvoiceAmount): InvoiceAmount { return this.getAll().reduce( - (total, tax) => total.add(tax.getTaxAmount(taxableAmount)), + (total, tax) => total.add(taxableAmount.percentage(tax.percentage)), InvoiceAmount.zero(taxableAmount.currencyCode) - ) as InvoiceAmount; + ); + } + + public getTaxesAmountByTaxCode(taxCode: string, taxableAmount: InvoiceAmount): InvoiceAmount { + const currencyCode = taxableAmount.currencyCode; + + return this.filter((itemTax) => itemTax.code === taxCode).reduce((totalAmount, itemTax) => { + return taxableAmount.percentage(itemTax.percentage).add(totalAmount); + }, InvoiceAmount.zero(currencyCode)); + } + + public getTaxesAmountByTaxes(taxableAmount: InvoiceAmount): InvoiceTaxTotal[] { + return this.getAll().map((taxItem) => ({ + taxableAmount, + tax: taxItem, + taxesAmount: this.getTaxesAmountByTaxCode(taxItem.code, taxableAmount), + })); } public getCodesToString(): string { return this.getAll() - .map((taxItem) => taxItem.tax.code) + .map((taxItem) => taxItem.code) .join(", "); } } 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 index 7051c455..640ed5eb 100644 --- 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 @@ -1,6 +1,12 @@ import { Tax, Taxes } from "@erp/core/api"; import { ItemAmount } from "../../value-objects"; +export type ItemTaxTotal = { + tax: Tax; + taxableAmount: ItemAmount; + taxesAmount: ItemAmount; +}; + export class ItemTaxes extends Taxes { constructor(items: Tax[] = [], totalItems: number | null = null) { super(items, totalItems); @@ -21,7 +27,7 @@ export class ItemTaxes extends Taxes { }, ItemAmount.zero(currencyCode)); } - public getTaxesAmountByTaxes(taxableAmount: ItemAmount) { + public getTaxesAmountByTaxes(taxableAmount: ItemAmount): ItemTaxTotal[] { return this.getAll().map((taxItem) => ({ taxableAmount, tax: taxItem,