import { MoneyDTO, PercentageDTO, TaxCatalogProvider } from "@erp/core"; import { useMoney, usePercentage, useQuantity } from "@erp/core/hooks"; import * as React from "react"; import { CustomerInvoiceItem } from "../schemas"; /** * Recalcula todos los importes de una línea usando los hooks de escala. */ type UseCalculateItemAmountsParams = { locale: string; currencyCode: string; keepNullWhenEmpty?: boolean; // Mantener todos los importes a null cuando la línea está “vacía” (qty+unit vacíos) taxCatalog: TaxCatalogProvider; // Catálogo de impuestos (inyectable para test) }; export function useCalculateItemAmounts(params: UseCalculateItemAmountsParams) { const { locale, currencyCode, taxCatalog, keepNullWhenEmpty } = params; const { add, sub, multiply, percentage: moneyPct, fromNumber, toNumber, isEmptyMoneyDTO, fallbackCurrency, } = useMoney(); const { toNumber: qtyToNumber } = useQuantity(); const { toNumber: pctToNumber } = usePercentage(); // Crea un MoneyDTO "cero" con la misma divisa/escala que unit_amount const zeroOf = React.useCallback( (unit?: MoneyDTO | null): MoneyDTO => { const cur = unit?.currency_code ?? fallbackCurrency; const sc = Number(unit?.scale ?? 2); return fromNumber(0, cur as any, sc); }, [fromNumber, fallbackCurrency] ); const emptyAmountDTO = React.useMemo( () => ({ value: "", scale: "4", currency_code: currencyCode, }), [] ); return React.useCallback( (item: CustomerInvoiceItem): CustomerInvoiceItem => { const qty = qtyToNumber(item.quantity); // 0 si vacío const unit = item.unit_amount && !isEmptyMoneyDTO(item.unit_amount) ? item.unit_amount : null; const zero = zeroOf(unit ?? undefined); // Línea “vacía”: mantener null si se pide y no hay datos const isEmptyLine = qty === 0 && (!unit || toNumber(unit) === 0); if (isEmptyLine && keepNullWhenEmpty) { return { ...item, subtotal_amount: emptyAmountDTO, discount_amount: emptyAmountDTO, taxable_amount: emptyAmountDTO, taxes_amount: emptyAmountDTO, total_amount: emptyAmountDTO, }; } // 1) Subtotal = qty × unit const subtotal = unit ? multiply(unit, qty) : zero; // 2) Descuento = subtotal × (discount_percentage / 100) const pctDTO = item.discount_percentage ?? ({ value: "", scale: "" } as PercentageDTO); const pct = pctToNumber(pctDTO); // 0 si vacío const discountAmount = pct !== 0 ? moneyPct(subtotal, Math.abs(pct)) : zero; // 3) Base imponible = subtotal - descuento const baseAmount = sub(subtotal, discountAmount); // 4) Impuestos const taxesBreakdown = (item.tax_codes ?.map((code) => { const maybe = taxCatalog.findByCode(code); if (maybe.isNone()) { console.warn(`[useCalculateItemAmounts] Tax code not found: "${code}"`); return null; } const tax = maybe.unwrap()!; // { name, value, scale } const p = pctToNumber({ value: tax.value, scale: tax.scale }); // ej. 21 return moneyPct(baseAmount, p); }) .filter(Boolean) as MoneyDTO[]) ?? []; const taxesTotal = taxesBreakdown.length ? taxesBreakdown.reduce((acc, m) => add(acc, m), zero) : zero; // 5) Total = base + impuestos const total = add(baseAmount, taxesTotal); return { ...item, subtotal_amount: subtotal, discount_amount: discountAmount, taxable_amount: baseAmount, taxes_amount: taxesTotal, total_amount: total, }; }, [ qtyToNumber, pctToNumber, multiply, moneyPct, add, sub, isEmptyMoneyDTO, zeroOf, toNumber, keepNullWhenEmpty, taxCatalog, ] ); }