// Exact money arithmetic. Amounts are integers of 10^-6 USDC, never floats. export const UNIT = 1_000_000n; /** "1,234.5" -> 1234500000n. Refuses anything that is not a plain amount. */ export function parseAmount(text) { const s = String(text).trim().replace(/,/g, ''); if (!/^\d{1,12}(\.\d{1,6})?$/.test(s)) throw new Error('Enter an amount like 120.50'); const [whole, frac = ''] = s.split('.'); return BigInt(whole) * UNIT + BigInt(frac.padEnd(6, '0')); } export function formatAmount(units, { min = 2 } = {}) { const n = BigInt(units); const neg = n < 0n; const abs = neg ? -n : n; const whole = (abs / UNIT).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); let frac = (abs % UNIT).toString().padStart(6, '0').replace(/0+$/, ''); frac = frac.padEnd(min, '0'); return `${neg ? '-' : ''}${whole}${frac ? `.${frac}` : ''}`; } /** Quantity times unit price, rounded half up to a unit. Quantity allows 3 decimals. */ export function lineTotal(qtyText, priceUnits) { const s = String(qtyText).trim(); if (!/^\d{1,7}(\.\d{1,3})?$/.test(s)) throw new Error('Enter a quantity like 2 or 1.5'); const [w, f = ''] = s.split('.'); const milli = BigInt(w) * 1000n + BigInt(f.padEnd(3, '0')); return (milli * BigInt(priceUnits) + 500n) / 1000n; } /** Tax as basis points of the subtotal, rounded half up. */ export function taxOn(subtotal, taxBps) { return (BigInt(subtotal) * BigInt(taxBps) + 5000n) / 10000n; } export function percentToBps(text) { const s = String(text).trim(); if (s === '') return 0; if (!/^\d{1,3}(\.\d{1,2})?$/.test(s)) throw new Error('Enter a percentage like 18 or 7.5'); const [w, f = ''] = s.split('.'); const bps = Number(w) * 100 + Number(f.padEnd(2, '0')); if (bps > 10000) throw new Error('A percentage cannot be above 100'); return bps; } export function invoiceTotals(doc) { const lines = doc.items.map((it) => lineTotal(it.q, parseAmount(it.p))); const subtotal = lines.reduce((a, b) => a + b, 0n); const tax = taxOn(subtotal, doc.taxBps || 0); return { lines, subtotal, tax, total: subtotal + tax }; }