Uecko_ERP/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-items.ts
2025-12-09 11:42:13 +01:00

246 lines
7.2 KiB
TypeScript

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<CustomerInvoiceItem> {
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;
}
}