import { TaxCatalogProvider } from "@erp/core"; import Dinero, { Currency } from "dinero.js"; export interface InvoiceItemCalcInput { quantity?: string; // p.ej. "3.5" unit_amount?: string; // p.ej. "125.75" discount_percentage?: string; // p.ej. "10" (=> 10%) tax_codes: string[]; // ["iva_21", ...] } export interface InvoiceItemCalcResult { subtotal_amount: number; discount_amount: number; taxable_amount: number; taxes_amount: number; total_amount: number; } /** * Cálculo financiero exacto por línea de factura. * Usa Dinero.js (base 10^2 y round half up) → resultados seguros en céntimos. */ export function calculateInvoiceItemAmounts( item: InvoiceItemCalcInput, currency: string, taxCatalog: TaxCatalogProvider ): InvoiceItemCalcResult { const scale = 4; const toDinero = (n: number) => Dinero({ amount: n === 0 ? 0 : Math.round(n * 10 ** scale), precision: scale, currency: currency as Currency, }); const qty = Number.parseFloat(item.quantity || "0") || 0; const unit = Number.parseFloat(item.unit_amount || "0") || 0; const pct = Number.parseFloat(item.discount_percentage || "0") || 0; // Subtotal = cantidad × precio unitario const subtotal = toDinero(qty * unit); // Descuento = subtotal × (pct / 100) const discount = subtotal.percentage(pct); // Base imponible const taxable = subtotal.subtract(discount); // Impuestos acumulados let taxes = toDinero(0); for (const code of item.tax_codes ?? []) { const tax = taxCatalog.findByCode(code); tax.map((taxItem) => { const pctValue = Number.parseFloat(taxItem.value) / 10 ** Number.parseInt(taxItem.scale, 10); const taxAmount = taxable.percentage(pctValue); taxes = taxes.add(taxAmount); }); } const total = taxable.add(taxes); // Devuelve valores desescalados (número con 2 decimales exactos) const toNum = (d: Dinero.Dinero) => d.toUnit(); return { subtotal_amount: toNum(subtotal), discount_amount: toNum(discount), taxable_amount: toNum(taxable), taxes_amount: toNum(taxes), total_amount: toNum(total), }; }