Uecko_ERP/modules/customer-invoices/src/web/domain/calculate-invoice-item-amounts.ts

73 lines
2.1 KiB
TypeScript
Raw Normal View History

2025-10-12 18:36:33 +00:00
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),
};
}