import { TaxCatalogProvider, TaxItemType } from "@erp/core"; import { toDinero, toNum } from "./calculate-utils"; export interface InvoiceItemCalcInput { quantity?: string; // p.ej. "3.5" unit_amount?: string; // p.ej. "125.75" discount_percentage?: string; // p.ej. "10" (=> 10%) header_discount_percentage?: string; // p.ej. "5" (=> 5%) tax_codes: string[]; // ["iva_21", ...] } export type InvoiceItemTaxSummary = TaxItemType & { taxable_amount: number; taxes_amount: number; }; export interface InvoiceItemCalcResult { subtotal_amount: number; discount_amount: number; header_discount_amount: number; taxable_amount: number; taxes_amount: number; taxes_summary: InvoiceItemTaxSummary[]; 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 defaultScale = 4; const taxesSummary: InvoiceItemTaxSummary[] = []; const qty = Number.parseFloat(item.quantity || "0") || 0; const unit = Number.parseFloat(item.unit_amount || "0") || 0; const iten_pct = Number.parseFloat(item.discount_percentage || "0") || 0; const header_pct = Number.parseFloat(item.header_discount_percentage || "0") || 0; // Subtotal = cantidad × precio unitario const subtotal_amount = toDinero(unit, defaultScale, currency).multiply(qty); // Descuento = subtotal × (item_pct / 100) const discount_amount = subtotal_amount.percentage(iten_pct); const subtotal_w_discount_amount = subtotal_amount.subtract(discount_amount); // Descuento de la cabecera = subtotal con dto de línea × (header_pct / 100) const header_discount = subtotal_w_discount_amount.percentage(header_pct); // Base imponible const taxable_amount = subtotal_w_discount_amount.subtract(header_discount); // Impuestos acumulados con signo let taxes_amount = toDinero(0, defaultScale, currency); for (const code of item.tax_codes ?? []) { const tax = taxCatalog.findByCode(code); if (tax.isNone()) continue; tax.map((taxItem) => { const tax_pct_value = Number.parseFloat(taxItem.value) / 10 ** Number.parseInt(taxItem.scale, 10); const item_taxables_amount = taxable_amount.percentage(tax_pct_value); // Sumar o restar según grupo switch (taxItem.group.toLowerCase()) { case "retención": taxes_amount = taxes_amount.subtract(item_taxables_amount); break; default: taxes_amount = taxes_amount.add(item_taxables_amount); break; } taxesSummary.push({ ...taxItem, taxable_amount: toNum(taxable_amount), taxes_amount: toNum(item_taxables_amount), }); }); } const total = taxable_amount.add(taxes_amount); return { subtotal_amount: toNum(subtotal_amount), discount_amount: toNum(discount_amount), header_discount_amount: toNum(header_discount), taxable_amount: toNum(taxable_amount), taxes_amount: toNum(taxes_amount), taxes_summary: taxesSummary, total_amount: toNum(total), }; }