import type { CurrencyCode, LanguageCode, Percentage } from "@repo/rdx-ddd"; import { Collection } from "@repo/rdx-utils"; import { ItemAmount, ItemDiscount, type ItemTaxGroup } from "../../value-objects"; import type { CustomerInvoiceItem } from "./customer-invoice-item"; export interface CustomerInvoiceItemsProps { items?: CustomerInvoiceItem[]; languageCode: LanguageCode; currencyCode: CurrencyCode; globalDiscountPercentage: Percentage; } export class CustomerInvoiceItems extends Collection { private _languageCode!: LanguageCode; private _currencyCode!: CurrencyCode; private _globalDiscountPercentage!: Percentage; constructor(props: CustomerInvoiceItemsProps) { super(props.items ?? []); this._languageCode = props.languageCode; this._currencyCode = props.currencyCode; this._globalDiscountPercentage = props.globalDiscountPercentage; } public static create(props: CustomerInvoiceItemsProps): CustomerInvoiceItems { return new CustomerInvoiceItems(props); } // Helpers private _sumAmounts(selector: (item: CustomerInvoiceItem) => ItemAmount): ItemAmount { return this.getAll().reduce( (acc, item) => acc.add(selector(item)), ItemAmount.zero(this._currencyCode.code) ); } /** * @summary Helper puro para sumar impuestos individuales por tipo. */ private _calculateIndividualTaxes() { let iva = ItemAmount.zero(this._currencyCode.code); let rec = ItemAmount.zero(this._currencyCode.code); let retention = ItemAmount.zero(this._currencyCode.code); for (const item of this.getAll()) { const { ivaAmount, recAmount, retentionAmount } = item.getIndividualTaxAmounts(); iva = iva.add(ivaAmount); rec = rec.add(recAmount); retention = retention.add(retentionAmount); } return { iva, rec, retention }; } // /** * @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. if ( !( this._languageCode.equals(item.languageCode) && this._currencyCode.equals(item.currencyCode) && this._globalDiscountPercentage.equals( item.globalDiscountPercentage.match( (v) => v, () => ItemDiscount.zero() ) ) ) ) { return false; } return super.add(item); } // Cálculos /** * @summary Orquestador central del cálculo agregado de la colección. * @remarks * Delega en los ítems individuales (DDD correcto) pero evita múltiples recorridos. */ public calculateAllAmounts() { let subtotalAmount = ItemAmount.zero(this._currencyCode.code); let itemDiscountAmount = ItemAmount.zero(this._currencyCode.code); let globalDiscountAmount = ItemAmount.zero(this._currencyCode.code); let totalDiscountAmount = ItemAmount.zero(this._currencyCode.code); let taxableAmount = ItemAmount.zero(this._currencyCode.code); let ivaAmount = ItemAmount.zero(this._currencyCode.code); let recAmount = ItemAmount.zero(this._currencyCode.code); let retentionAmount = ItemAmount.zero(this._currencyCode.code); let taxesAmount = ItemAmount.zero(this._currencyCode.code); let totalAmount = ItemAmount.zero(this._currencyCode.code); for (const item of this.getAll()) { const amounts = item.calculateAllAmounts(); // Subtotales subtotalAmount = subtotalAmount.add(amounts.subtotalAmount); // Descuentos itemDiscountAmount = itemDiscountAmount.add(amounts.itemDiscountAmount); globalDiscountAmount = globalDiscountAmount.add(amounts.globalDiscountAmount); totalDiscountAmount = totalDiscountAmount.add(amounts.totalDiscountAmount); // Base imponible taxableAmount = taxableAmount.add(amounts.taxableAmount); // Impuestos individuales ivaAmount = ivaAmount.add(amounts.ivaAmount); recAmount = recAmount.add(amounts.recAmount); retentionAmount = retentionAmount.add(amounts.retentionAmount); // Total impuestos del ítem taxesAmount = taxesAmount.add(amounts.taxesAmount); // Total final del ítem totalAmount = totalAmount.add(amounts.totalAmount); } return { subtotalAmount, itemDiscountAmount, globalDiscountAmount, totalDiscountAmount, taxableAmount, ivaAmount, recAmount, retentionAmount, taxesAmount, totalAmount, } as const; } public getSubtotalAmount(): ItemAmount { return this.calculateAllAmounts().subtotalAmount; } public getItemDiscountAmount(): ItemAmount { return this.calculateAllAmounts().itemDiscountAmount; } public getGlobalDiscountAmount(): ItemAmount { return this.calculateAllAmounts().globalDiscountAmount; } public getTotalDiscountAmount(): ItemAmount { return this.calculateAllAmounts().totalDiscountAmount; } public getTaxableAmount(): ItemAmount { return this.calculateAllAmounts().taxableAmount; } public getTaxesAmount(): ItemAmount { return this.calculateAllAmounts().taxesAmount; } public getTotalAmount(): ItemAmount { return this.calculateAllAmounts().totalAmount; } /** * @summary Recalcula totales agrupando por el trío iva, rec y retención. */ public groupTaxesByCode() { const map = new Map< string, { taxes: ItemTaxGroup; taxable: ItemAmount; ivaAmount: ItemAmount; recAmount: ItemAmount; retentionAmount: ItemAmount; taxesAmount: ItemAmount; } >(); for (const item of this.getAll()) { const amounts = item.calculateAllAmounts(); const taxable = amounts.taxableAmount; const { ivaAmount, recAmount, retentionAmount, taxesAmount } = amounts; const taxes = item.taxes; const ivaCode = taxes.iva.match( (t) => t.code, () => "" ); const recCode = taxes.rec.match( (t) => t.code, () => "" ); const retentionCode = taxes.retention.match( (t) => t.code, () => "" ); // Clave del grupo: combinación IVA|REC|RET const key = `${ivaCode}|${recCode}|${retentionCode}`; const prev = map.get(key) ?? { taxes, taxable: ItemAmount.zero(taxable.currencyCode), ivaAmount: ItemAmount.zero(taxable.currencyCode), recAmount: ItemAmount.zero(taxable.currencyCode), retentionAmount: ItemAmount.zero(taxable.currencyCode), taxesAmount: ItemAmount.zero(taxable.currencyCode), }; map.set(key, { taxes, taxable: prev.taxable.add(taxable), ivaAmount: prev.ivaAmount.add(ivaAmount), recAmount: prev.recAmount.add(recAmount), retentionAmount: prev.retentionAmount.add(retentionAmount), taxesAmount: prev.taxesAmount.add(taxesAmount), }); } return map; } }