import type { Tax } from "@erp/core/api"; import type { CurrencyCode, LanguageCode } from "@repo/rdx-ddd"; import { Collection } from "@repo/rdx-utils"; import { ItemAmount } from "../../value-objects"; import { ItemTaxes } from "../item-taxes"; import type { CustomerInvoiceItem } from "./customer-invoice-item"; export interface CustomerInvoiceItemsProps { items?: CustomerInvoiceItem[]; languageCode: LanguageCode; currencyCode: CurrencyCode; } export class CustomerInvoiceItems extends Collection { private _languageCode!: LanguageCode; private _currencyCode!: CurrencyCode; constructor(props: CustomerInvoiceItemsProps) { const { items = [], languageCode, currencyCode } = props; super(items); this._languageCode = languageCode; this._currencyCode = currencyCode; } public static create(props: CustomerInvoiceItemsProps): CustomerInvoiceItems { return new CustomerInvoiceItems(props); } /** * @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) ) ) { return false; } return super.add(item); } /** * @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. */ public getSubtotalAmount(): ItemAmount { return this.getAll().reduce( (total, item) => total.add(item.getSubtotalAmount()), ItemAmount.zero(this._currencyCode.code) ); } /** * @summary Calcula el importe total de descuentos aplicados a todos los ítems. * @returns Un `ItemAmount` con el importe total de descuentos. */ public getDiscountAmount(): ItemAmount { return this.getAll().reduce( (total, item) => total.add(item.getDiscountAmount()), ItemAmount.zero(this._currencyCode.code) ); } /** * @summary Calcula el importe imponible total (base antes de impuestos). * @returns Un `ItemAmount` con el total imponible. */ public getTaxableAmount(): ItemAmount { return this.getAll().reduce( (total, item) => total.add(item.getTaxableAmount()), ItemAmount.zero(this._currencyCode.code) ); } /** * @summary Calcula el importe total de impuestos de todos los ítems. * @returns Un `ItemAmount` con la suma de todos los impuestos aplicados. */ public getTaxesAmount(): ItemAmount { return this.getAll().reduce( (total, item) => total.add(item.getTaxesAmount()), ItemAmount.zero(this._currencyCode.code) ); } /** * @summary Calcula el importe total final de todos los ítems (subtotal -descuentos + impuestos). * @returns Un `ItemAmount` con el total global de la colección. */ public getTotalAmount(): ItemAmount { return this.getAll().reduce( (total, item) => total.add(item.getTotalAmount()), ItemAmount.zero(this._currencyCode.code) ); } /** * @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. */ public getTaxesAmountByTaxes(): Array<{ tax: Tax; taxableAmount: ItemAmount; taxesAmount: ItemAmount; }> { const getTaxCode = (tax: Tax): string => tax.code; // clave estable para Map const currencyCode = this._currencyCode.code; // Mapeamos por clave (tax code), pero también guardamos el Tax original const resultMap = new Map< string, { tax: Tax; taxableAmount: ItemAmount; taxesAmount: ItemAmount } >(); for (const item of this.getAll()) { for (const { taxableAmount, tax, taxesAmount } of item.getTaxesAmountByTaxes()) { const key = getTaxCode(tax); const current = resultMap.get(key) ?? { tax, taxableAmount: ItemAmount.zero(currencyCode), taxesAmount: ItemAmount.zero(currencyCode), }; resultMap.set(key, { tax: current.tax, taxableAmount: current.taxableAmount.add(taxableAmount), taxesAmount: current.taxesAmount.add(taxesAmount), }); } } return Array.from(resultMap.values()); } /** * @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()); return ItemTaxes.create([...taxes.values()]); } }