2025-11-16 19:30:11 +00:00
|
|
|
import type { Tax } from "@erp/core/api";
|
|
|
|
|
import type { CurrencyCode, LanguageCode } 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-09-10 18:14:19 +00:00
|
|
|
import { ItemAmount } from "../../value-objects";
|
2025-10-12 09:14:33 +00:00
|
|
|
import { ItemTaxes } from "../item-taxes";
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class CustomerInvoiceItems extends Collection<CustomerInvoiceItem> {
|
|
|
|
|
private _languageCode!: LanguageCode;
|
|
|
|
|
private _currencyCode!: CurrencyCode;
|
|
|
|
|
|
|
|
|
|
constructor(props: CustomerInvoiceItemsProps) {
|
2025-09-07 19:55:12 +00:00
|
|
|
const { items = [], languageCode, currencyCode } = props;
|
2025-09-03 10:41:12 +00:00
|
|
|
super(items);
|
|
|
|
|
this._languageCode = languageCode;
|
|
|
|
|
this._currencyCode = currencyCode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static create(props: CustomerInvoiceItemsProps): CustomerInvoiceItems {
|
|
|
|
|
return new CustomerInvoiceItems(props);
|
|
|
|
|
}
|
2025-09-07 19:55:12 +00:00
|
|
|
|
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)
|
|
|
|
|
)
|
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-10-12 09:14:33 +00:00
|
|
|
/**
|
|
|
|
|
* @summary Calcula el subtotal de todos los ítems sin descuentos ni impuestos.
|
|
|
|
|
* @returns Un `ItemAmount` con el subtotal total de la colección en su moneda.
|
|
|
|
|
*/
|
2025-09-17 17:37:41 +00:00
|
|
|
public getSubtotalAmount(): ItemAmount {
|
|
|
|
|
return this.getAll().reduce(
|
2025-11-06 16:16:18 +00:00
|
|
|
(total, item) => total.add(item.getSubtotalAmount()),
|
2025-09-17 17:37:41 +00:00
|
|
|
ItemAmount.zero(this._currencyCode.code)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 09:14:33 +00:00
|
|
|
/**
|
|
|
|
|
* @summary Calcula el importe total de descuentos aplicados a todos los ítems.
|
|
|
|
|
* @returns Un `ItemAmount` con el importe total de descuentos.
|
|
|
|
|
*/
|
2025-09-17 17:37:41 +00:00
|
|
|
public getDiscountAmount(): ItemAmount {
|
|
|
|
|
return this.getAll().reduce(
|
|
|
|
|
(total, item) => total.add(item.getDiscountAmount()),
|
|
|
|
|
ItemAmount.zero(this._currencyCode.code)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 09:14:33 +00:00
|
|
|
/**
|
|
|
|
|
* @summary Calcula el importe imponible total (base antes de impuestos).
|
|
|
|
|
* @returns Un `ItemAmount` con el total imponible.
|
|
|
|
|
*/
|
2025-09-17 17:37:41 +00:00
|
|
|
public getTaxableAmount(): ItemAmount {
|
|
|
|
|
return this.getAll().reduce(
|
|
|
|
|
(total, item) => total.add(item.getTaxableAmount()),
|
|
|
|
|
ItemAmount.zero(this._currencyCode.code)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 09:14:33 +00:00
|
|
|
/**
|
|
|
|
|
* @summary Calcula el importe total de impuestos de todos los ítems.
|
|
|
|
|
* @returns Un `ItemAmount` con la suma de todos los impuestos aplicados.
|
|
|
|
|
*/
|
2025-09-17 17:37:41 +00:00
|
|
|
public getTaxesAmount(): ItemAmount {
|
|
|
|
|
return this.getAll().reduce(
|
|
|
|
|
(total, item) => total.add(item.getTaxesAmount()),
|
|
|
|
|
ItemAmount.zero(this._currencyCode.code)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 09:14:33 +00:00
|
|
|
/**
|
|
|
|
|
* @summary Calcula el importe total final de todos los ítems (subtotal -descuentos + impuestos).
|
|
|
|
|
* @returns Un `ItemAmount` con el total global de la colección.
|
|
|
|
|
*/
|
2025-09-10 18:14:19 +00:00
|
|
|
public getTotalAmount(): ItemAmount {
|
|
|
|
|
return this.getAll().reduce(
|
2025-09-17 17:37:41 +00:00
|
|
|
(total, item) => total.add(item.getTotalAmount()),
|
2025-09-10 18:14:19 +00:00
|
|
|
ItemAmount.zero(this._currencyCode.code)
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-09-26 15:00:11 +00:00
|
|
|
|
2025-10-12 09:14:33 +00:00
|
|
|
/**
|
|
|
|
|
* @summary Agrupa los importes imponibles e impuestos por tipo de impuesto.
|
|
|
|
|
* @returns Un array con objetos que contienen:
|
|
|
|
|
* - `tax`: El tipo de impuesto.
|
|
|
|
|
* - `taxableAmount`: El total de base imponible asociada a ese impuesto.
|
|
|
|
|
* - `taxesAmount`: El importe total de impuestos calculado.
|
|
|
|
|
* @remarks
|
|
|
|
|
* Los impuestos se agrupan por su `tax.code` (código fiscal).
|
|
|
|
|
* Si existen varios impuestos con el mismo código, sus importes se agregan.
|
|
|
|
|
* En caso de conflicto de datos en impuestos con mismo código, prevalece
|
|
|
|
|
* el primer `Tax` encontrado.
|
|
|
|
|
*/
|
2025-09-29 07:32:13 +00:00
|
|
|
public getTaxesAmountByTaxes(): Array<{
|
|
|
|
|
tax: Tax;
|
|
|
|
|
taxableAmount: ItemAmount;
|
|
|
|
|
taxesAmount: ItemAmount;
|
|
|
|
|
}> {
|
|
|
|
|
const getTaxCode = (tax: Tax): string => tax.code; // clave estable para Map
|
2025-09-26 15:00:11 +00:00
|
|
|
const currencyCode = this._currencyCode.code;
|
|
|
|
|
|
2025-09-29 07:32:13 +00:00
|
|
|
// Mapeamos por clave (tax code), pero también guardamos el Tax original
|
|
|
|
|
const resultMap = new Map<
|
|
|
|
|
string,
|
|
|
|
|
{ tax: Tax; taxableAmount: ItemAmount; taxesAmount: ItemAmount }
|
|
|
|
|
>();
|
|
|
|
|
|
2025-09-26 15:00:11 +00:00
|
|
|
for (const item of this.getAll()) {
|
2025-09-26 18:09:14 +00:00
|
|
|
for (const { taxableAmount, tax, taxesAmount } of item.getTaxesAmountByTaxes()) {
|
2025-09-29 07:32:13 +00:00
|
|
|
const key = getTaxCode(tax);
|
|
|
|
|
const current = resultMap.get(key) ?? {
|
|
|
|
|
tax,
|
2025-09-26 18:09:14 +00:00
|
|
|
taxableAmount: ItemAmount.zero(currencyCode),
|
|
|
|
|
taxesAmount: ItemAmount.zero(currencyCode),
|
|
|
|
|
};
|
2025-09-29 07:32:13 +00:00
|
|
|
|
|
|
|
|
resultMap.set(key, {
|
|
|
|
|
tax: current.tax,
|
|
|
|
|
taxableAmount: current.taxableAmount.add(taxableAmount),
|
|
|
|
|
taxesAmount: current.taxesAmount.add(taxesAmount),
|
2025-09-26 18:09:14 +00:00
|
|
|
});
|
2025-09-26 15:00:11 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-29 07:32:13 +00:00
|
|
|
return Array.from(resultMap.values());
|
2025-09-26 15:00:11 +00:00
|
|
|
}
|
2025-10-12 09:14:33 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @summary Obtiene la lista de impuestos únicos aplicados en todos los ítems.
|
|
|
|
|
* @returns Un objeto `ItemTaxes` que contiene todos los impuestos distintos.
|
|
|
|
|
* @remarks
|
|
|
|
|
* Los impuestos se deduplican por su código (`tax.code`).
|
|
|
|
|
* Si existen varios impuestos con el mismo código, el último encontrado
|
|
|
|
|
* sobrescribirá los anteriores sin generar error.
|
|
|
|
|
*/
|
|
|
|
|
public getTaxes(): ItemTaxes {
|
|
|
|
|
const taxes = this.getAll()
|
|
|
|
|
.flatMap((item) => item.taxes.getAll())
|
|
|
|
|
.reduce((map, tax) => map.set(tax.code, tax), new Map<string, Tax>());
|
|
|
|
|
|
|
|
|
|
return ItemTaxes.create([...taxes.values()]);
|
|
|
|
|
}
|
2025-09-03 10:41:12 +00:00
|
|
|
}
|