246 lines
7.2 KiB
TypeScript
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;
|
|
}
|
|
}
|