2025-11-16 19:30:11 +00:00
|
|
|
import type { Tax } from "@erp/core/api";
|
2025-12-04 18:31:40 +00:00
|
|
|
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
|
|
|
|
2025-12-04 18:31:40 +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;
|
2025-12-04 18:31:40 +00:00
|
|
|
globalDiscountPercentage: Percentage;
|
2025-09-03 10:41:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class CustomerInvoiceItems extends Collection<CustomerInvoiceItem> {
|
|
|
|
|
private _languageCode!: LanguageCode;
|
|
|
|
|
private _currencyCode!: CurrencyCode;
|
2025-12-04 18:31:40 +00:00
|
|
|
private _globalDiscountPercentage!: Percentage;
|
2025-09-03 10:41:12 +00:00
|
|
|
|
|
|
|
|
constructor(props: CustomerInvoiceItemsProps) {
|
2025-11-23 21:44:31 +00:00
|
|
|
super(props.items ?? []);
|
|
|
|
|
this._languageCode = props.languageCode;
|
|
|
|
|
this._currencyCode = props.currencyCode;
|
2025-12-04 18:31:40 +00:00
|
|
|
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
|
|
|
|
2025-12-04 18:31:40 +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
|
|
|
!(
|
2025-12-04 18:31:40 +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
|
|
|
}
|
|
|
|
|
|
2025-12-04 18:31:40 +00:00
|
|
|
// Cálculos
|
2025-09-17 17:37:41 +00:00
|
|
|
|
2025-10-12 09:14:33 +00:00
|
|
|
/**
|
2025-12-04 18:31:40 +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
|
|
|
*/
|
2025-12-04 18:31:40 +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
|
|
|
}
|
|
|
|
|
|
2025-12-04 18:31:40 +00:00
|
|
|
public getSubtotalAmount(): ItemAmount {
|
|
|
|
|
return this.calculateAllAmounts().subtotalAmount;
|
2025-09-17 17:37:41 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-04 18:31:40 +00:00
|
|
|
public getItemDiscountAmount(): ItemAmount {
|
|
|
|
|
return this.calculateAllAmounts().itemDiscountAmount;
|
2025-09-17 17:37:41 +00:00
|
|
|
}
|
|
|
|
|
|
2025-12-04 18:31:40 +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
|
|
|
|
2025-12-04 18:31:40 +00:00
|
|
|
public getTotalDiscountAmount(): ItemAmount {
|
|
|
|
|
return this.calculateAllAmounts().totalDiscountAmount;
|
|
|
|
|
}
|
2025-09-29 07:32:13 +00:00
|
|
|
|
2025-12-04 18:31:40 +00:00
|
|
|
public getTaxableAmount(): ItemAmount {
|
|
|
|
|
return this.calculateAllAmounts().taxableAmount;
|
|
|
|
|
}
|
2025-11-23 21:44:31 +00:00
|
|
|
|
2025-12-04 18:31:40 +00:00
|
|
|
public getTaxesAmount(): ItemAmount {
|
|
|
|
|
return this.calculateAllAmounts().taxesAmount;
|
|
|
|
|
}
|
2025-09-26 15:00:11 +00:00
|
|
|
|
2025-12-04 18:31:40 +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
|
|
|
|
2025-12-04 18:31:40 +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() {
|
2025-11-23 21:44:31 +00:00
|
|
|
const map = new Map<string, { tax: Tax; taxable: ItemAmount; total: ItemAmount }>();
|
|
|
|
|
|
|
|
|
|
for (const item of this.getAll()) {
|
2025-12-04 18:31:40 +00:00
|
|
|
const amounts = item.calculateAllAmounts();
|
|
|
|
|
const taxable = amounts.taxableAmount;
|
|
|
|
|
|
|
|
|
|
const { ivaAmount, recAmount, retentionAmount } = amounts;
|
2025-11-23 21:44:31 +00:00
|
|
|
|
2025-12-04 18:31:40 +00:00
|
|
|
/* ----------------------------- IVA ----------------------------- */
|
2025-11-23 21:44:31 +00:00
|
|
|
item.taxes.iva.match(
|
|
|
|
|
(iva) => {
|
2025-12-04 18:31:40 +00:00
|
|
|
const key = iva.code;
|
|
|
|
|
const prev = map.get(key) ?? {
|
|
|
|
|
tax: iva,
|
2025-11-23 21:44:31 +00:00
|
|
|
taxable: ItemAmount.zero(taxable.currencyCode),
|
|
|
|
|
total: ItemAmount.zero(taxable.currencyCode),
|
|
|
|
|
};
|
2025-12-04 18:31:40 +00:00
|
|
|
|
|
|
|
|
map.set(key, {
|
2025-11-23 21:44:31 +00:00
|
|
|
tax: iva,
|
|
|
|
|
taxable: prev.taxable.add(taxable),
|
|
|
|
|
total: prev.total.add(ivaAmount),
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
() => {
|
|
|
|
|
//
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
2025-12-04 18:31:40 +00:00
|
|
|
/* ----------------------------- REC ----------------------------- */
|
2025-11-23 21:44:31 +00:00
|
|
|
item.taxes.rec.match(
|
|
|
|
|
(rec) => {
|
2025-12-04 18:31:40 +00:00
|
|
|
const key = rec.code;
|
|
|
|
|
const prev = map.get(key) ?? {
|
|
|
|
|
tax: rec,
|
2025-11-23 21:44:31 +00:00
|
|
|
taxable: ItemAmount.zero(taxable.currencyCode),
|
|
|
|
|
total: ItemAmount.zero(taxable.currencyCode),
|
|
|
|
|
};
|
2025-12-04 18:31:40 +00:00
|
|
|
|
|
|
|
|
map.set(key, {
|
2025-11-23 21:44:31 +00:00
|
|
|
tax: rec,
|
|
|
|
|
taxable: prev.taxable.add(taxable),
|
|
|
|
|
total: prev.total.add(recAmount),
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
() => {
|
|
|
|
|
//
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
2025-12-04 18:31:40 +00:00
|
|
|
/* -------------------------- RETENCIÓN -------------------------- */
|
2025-11-23 21:44:31 +00:00
|
|
|
item.taxes.retention.match(
|
|
|
|
|
(retention) => {
|
2025-12-04 18:31:40 +00:00
|
|
|
const key = retention.code;
|
|
|
|
|
const prev = map.get(key) ?? {
|
|
|
|
|
tax: retention,
|
2025-11-23 21:44:31 +00:00
|
|
|
taxable: ItemAmount.zero(taxable.currencyCode),
|
|
|
|
|
total: ItemAmount.zero(taxable.currencyCode),
|
|
|
|
|
};
|
2025-12-04 18:31:40 +00:00
|
|
|
|
|
|
|
|
map.set(key, {
|
2025-11-23 21:44:31 +00:00
|
|
|
tax: retention,
|
|
|
|
|
taxable: prev.taxable.add(taxable),
|
|
|
|
|
total: prev.total.add(retentionAmount),
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
() => {
|
|
|
|
|
//
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-10-12 09:14:33 +00:00
|
|
|
|
2025-11-23 21:44:31 +00:00
|
|
|
return map;
|
2025-10-12 09:14:33 +00:00
|
|
|
}
|
2025-09-03 10:41:12 +00:00
|
|
|
}
|