Uecko_ERP/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-items.ts

267 lines
7.8 KiB
TypeScript
Raw Normal View History

2025-11-16 19:30:11 +00:00
import type { Tax } from "@erp/core/api";
import type { CurrencyCode, LanguageCode, Percentage } from "@repo/rdx-ddd";
2025-09-03 10:41:12 +00:00
import { Collection } from "@repo/rdx-utils";
2025-11-16 19:30:11 +00:00
import { ItemAmount, ItemDiscount } from "../../value-objects";
2025-11-16 19:30:11 +00:00
import type { CustomerInvoiceItem } from "./customer-invoice-item";
2025-09-03 10:41:12 +00:00
export interface CustomerInvoiceItemsProps {
items?: CustomerInvoiceItem[];
languageCode: LanguageCode;
currencyCode: CurrencyCode;
globalDiscountPercentage: Percentage;
2025-09-03 10:41:12 +00:00
}
export class CustomerInvoiceItems extends Collection<CustomerInvoiceItem> {
private _languageCode!: LanguageCode;
private _currencyCode!: CurrencyCode;
private _globalDiscountPercentage!: Percentage;
2025-09-03 10:41:12 +00:00
constructor(props: CustomerInvoiceItemsProps) {
super(props.items ?? []);
this._languageCode = props.languageCode;
this._currencyCode = props.currencyCode;
this._globalDiscountPercentage = props.globalDiscountPercentage;
2025-09-03 10:41:12 +00:00
}
public static create(props: CustomerInvoiceItemsProps): CustomerInvoiceItems {
return new CustomerInvoiceItems(props);
}
2025-09-07 19:55:12 +00:00
// 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 };
}
//
2025-10-12 09:14:33 +00:00
/**
* @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.
*/
2025-09-07 19:55:12 +00:00
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.
2025-09-10 18:14:19 +00:00
if (
2025-11-16 19:30:11 +00:00
!(
this._languageCode.equals(item.languageCode) &&
this._currencyCode.equals(item.currencyCode) &&
this._globalDiscountPercentage.equals(
item.globalDiscountPercentage.match(
(v) => v,
() => ItemDiscount.zero()
)
)
2025-11-16 19:30:11 +00:00
)
2025-09-10 18:14:19 +00:00
) {
2025-09-07 19:55:12 +00:00
return false;
}
2025-09-10 18:14:19 +00:00
return super.add(item);
2025-09-07 19:55:12 +00:00
}
// Cálculos
2025-09-17 17:37:41 +00:00
2025-10-12 09:14:33 +00:00
/**
* @summary Orquestador central del cálculo agregado de la colección.
* @remarks
* Delega en los ítems individuales (DDD correcto) pero evita múltiples recorridos.
2025-10-12 09:14:33 +00:00
*/
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;
2025-09-17 17:37:41 +00:00
}
public getSubtotalAmount(): ItemAmount {
return this.calculateAllAmounts().subtotalAmount;
2025-09-17 17:37:41 +00:00
}
public getItemDiscountAmount(): ItemAmount {
return this.calculateAllAmounts().itemDiscountAmount;
2025-09-17 17:37:41 +00:00
}
public getGlobalDiscountAmount(): ItemAmount {
return this.calculateAllAmounts().globalDiscountAmount;
2025-09-10 18:14:19 +00:00
}
2025-09-26 15:00:11 +00:00
public getTotalDiscountAmount(): ItemAmount {
return this.calculateAllAmounts().totalDiscountAmount;
}
2025-09-29 07:32:13 +00:00
public getTaxableAmount(): ItemAmount {
return this.calculateAllAmounts().taxableAmount;
}
public getTaxesAmount(): ItemAmount {
return this.calculateAllAmounts().taxesAmount;
}
2025-09-26 15:00:11 +00:00
public getTotalAmount(): ItemAmount {
return this.calculateAllAmounts().totalAmount;
2025-09-26 15:00:11 +00:00
}
2025-10-12 09:14:33 +00:00
/**
* @summary Agrupa bases e importes por código fiscal.
* @remarks
* Este método se usa para poblar la tabla `customer_invoice_taxes`.
*/
public groupTaxesByCode() {
const map = new Map<string, { tax: Tax; taxable: ItemAmount; total: ItemAmount }>();
for (const item of this.getAll()) {
const amounts = item.calculateAllAmounts();
const taxable = amounts.taxableAmount;
const { ivaAmount, recAmount, retentionAmount } = amounts;
/* ----------------------------- IVA ----------------------------- */
item.taxes.iva.match(
(iva) => {
const key = iva.code;
const prev = map.get(key) ?? {
tax: iva,
taxable: ItemAmount.zero(taxable.currencyCode),
total: ItemAmount.zero(taxable.currencyCode),
};
map.set(key, {
tax: iva,
taxable: prev.taxable.add(taxable),
total: prev.total.add(ivaAmount),
});
},
() => {
//
}
);
/* ----------------------------- REC ----------------------------- */
item.taxes.rec.match(
(rec) => {
const key = rec.code;
const prev = map.get(key) ?? {
tax: rec,
taxable: ItemAmount.zero(taxable.currencyCode),
total: ItemAmount.zero(taxable.currencyCode),
};
map.set(key, {
tax: rec,
taxable: prev.taxable.add(taxable),
total: prev.total.add(recAmount),
});
},
() => {
//
}
);
/* -------------------------- RETENCIÓN -------------------------- */
item.taxes.retention.match(
(retention) => {
const key = retention.code;
const prev = map.get(key) ?? {
tax: retention,
taxable: ItemAmount.zero(taxable.currencyCode),
total: ItemAmount.zero(taxable.currencyCode),
};
map.set(key, {
tax: retention,
taxable: prev.taxable.add(taxable),
total: prev.total.add(retentionAmount),
});
},
() => {
//
}
);
}
2025-10-12 09:14:33 +00:00
return map;
2025-10-12 09:14:33 +00:00
}
2025-09-03 10:41:12 +00:00
}