import test from 'node:test';import assert from 'node:assert/strict';import { formatAmount, invoiceTotals, lineTotal, parseAmount, percentToBps, taxOn } from '../web/lib/money.js';import { invoiceRow, toCsv } from '../web/lib/csv.js';test('amounts parse exactly, with no float rounding', () => { assert.equal(parseAmount('0.1'), 100_000n); assert.equal(parseAmount('1,234.56'), 1_234_560_000n); assert.equal(parseAmount('0.000001'), 1n); for (const bad of ['', 'abc', '1.2345678', '-1', '1e3', '0x10', '1.2.3']) assert.throws(() => parseAmount(bad));});test('amounts format for people', () => { assert.equal(formatAmount(1_234_560_000n), '1,234.56'); assert.equal(formatAmount(1_000_000n), '1.00'); assert.equal(formatAmount(1n), '0.000001'); assert.equal(formatAmount(0n), '0.00');});test('invoice totals use integer maths', () => { const doc = { taxBps: 1800, items: [{ q: '3', p: '19.99' }, { q: '1.5', p: '100' }] }; const t = invoiceTotals(doc); assert.equal(t.subtotal, 209_970_000n); assert.equal(t.tax, 37_794_600n); assert.equal(t.total, 247_764_600n); assert.equal(lineTotal('0.001', 500n), 1n); assert.equal(taxOn(3n, 5000), 2n);});test('percentages', () => { assert.equal(percentToBps('7.5'), 750); assert.equal(percentToBps(''), 0); assert.throws(() => percentToBps('100.01')); assert.throws(() => percentToBps('abc'));});const inv = { number: 7, amount: '100000000', applied: '100000000', paid: '130000000', refunded: '0', excess: '30000000', set_aside: '30000000', status: 'paid', issued_at_ms: '1800000000000', due_at_ms: '1801000000000', closed_at_ms: '1800500000000', payments: [{ at_ms: '1800500000000', refund_to: 'base:USDC:0xabc' }],};test('a paid invoice becomes one readable row that adds up', () => { const row = invoiceRow(inv, { to: { name: 'Acme, Inc' }, ccy: 'EUR', fx: 0.9, taxAmount: '10.00' }, 'https://x/y'); assert.equal(row[0], 'INV-0007'); assert.equal(row[4], 'Paid'); assert.equal(row[9], '130.00'); assert.equal(row[11], '30.00'); assert.equal(row[12], '100.00'); assert.equal(row[14], '30.00'); assert.equal(row[17], '90.00');});test('a discount shows as a discount', () => { const row = invoiceRow({ ...inv, applied: '98000000', paid: '98000000', excess: '0', set_aside: '0' }, null, ''); assert.equal(row[8], '2.00');});test('csv quotes commas and neutralises spreadsheet formulas', () => { const csv = toCsv([['=HYPERLINK("http://evil")', '+1', '-1', '@x', 'a,b', 'say "hi"', 'plain']]); assert.ok(csv.startsWith('Invoice number,')); const line = csv.split('\r\n')[1]; assert.ok(line.includes(`"'=HYPERLINK(""http://evil"")"`)); assert.ok(line.includes(`'+1,'-1,'@x,"a,b","say ""hi""",plain`));});test('a client name cannot inject a row', () => { const lines = toCsv([['line1\nline2']]).split('\r\n'); assert.equal(lines.length, 3);});