import assert from 'node:assert/strict'; import test from 'node:test'; import { buildLoanSchedule, calculateBreakEven, calculateBudget, calculateFinancialTools, calculateFundingGap, calculateLoanPayment, exportFinancialToolsCsv, restoreFinancialToolsSnapshot, serializeFinancialToolsSnapshot, } from '../../resources/js/lib/financial-calculators.ts'; function cents(value) { const [whole, fraction = ''] = value.split('.'); return BigInt(whole) * 100n + BigInt(fraction.padEnd(2, '0')); } void test('financial tools snapshot persists editable calculations and rejects malformed data', () => { const data = { budgetPeriod: 'Mensal', loan: { principal: '100000', rate: '18', months: '24', rateType: 'nominal', periodMonths: '1', gracePeriods: '0', upfrontFee: '0', recurringFee: '0', }, breakEven: { fixed: '50000', price: '1000', variable: '600' }, funding: { total: '1000000', secured: '250000' }, incomes: [{ label: 'Vendas', amount: '200000' }], expenses: [{ label: 'Renda', amount: '15000' }], }; const snapshot = serializeFinancialToolsSnapshot(data); assert.deepEqual(restoreFinancialToolsSnapshot(snapshot), data); assert.equal(restoreFinancialToolsSnapshot('{"version":2}'), null); assert.equal(restoreFinancialToolsSnapshot('x'.repeat(30001)), null); assert.equal( restoreFinancialToolsSnapshot( serializeFinancialToolsSnapshot({ ...data, incomes: [] }), ), null, ); assert.equal( restoreFinancialToolsSnapshot( serializeFinancialToolsSnapshot({ ...data, incomes: Array.from({ length: 51 }, () => ({ label: 'x', amount: '1', })), }), ), null, ); }); void test('loan payment handles zero interest exactly', () => { assert.deepEqual(calculateLoanPayment('1200.00', '0', 12), { monthlyPayment: '100.00', totalPaid: '1200.00', status: 'estimated', }); }); void test('loan payment computes a reproducible amortized estimate', () => { assert.deepEqual(calculateLoanPayment('1000.00', '12', 12), { monthlyPayment: '88.85', totalPaid: '1066.19', status: 'estimated', }); }); void test('break-even rounds required units upward and rejects nonpositive margin', () => { assert.deepEqual(calculateBreakEven('100.00', '30.00', '20.00'), { contributionPerUnit: '10.00', breakEvenUnits: '10', breakEvenRevenue: '300.00', status: 'estimated', }); assert.throws(() => calculateBreakEven('10', '20', '20'), /superior/); }); void test('an invalid indicator does not hide independent financial calculations', () => { const result = calculateFinancialTools({ loan: { principal: '1200', annualRatePercent: '0', termMonths: 12, periodMonths: 1, rateType: 'nominal', }, breakEven: { fixedCosts: '100', unitPrice: '20', variableCostPerUnit: '20', }, funding: { totalInvestment: '5000', securedFunding: '1000' }, incomes: ['500'], expenses: ['600'], }); assert.equal(result.loan.value?.schedule[0].totalPayment, '100.00'); assert.match(result.breakEven.error ?? '', /superior/); assert.equal(result.funding.value?.gap, '4000.00'); assert.equal(result.budget.value?.operatingBalance, '-100.00'); }); void test('funding gap distinguishes zero from a negative surplus', () => { assert.deepEqual(calculateFundingGap('100.00', '120.00'), { gap: '-20.00', reconciles: false, status: 'estimated', }); }); void test('invalid precision and negative inputs are rejected', () => { assert.throws(() => calculateFundingGap('-1', '0'), /decimal/); assert.throws(() => calculateLoanPayment('1.001', '0', 1), /casas/); }); void test('loan schedule supports nominal rates, interest-only grace and separate fees', () => { const result = buildLoanSchedule({ principal: '1200.00', annualRatePercent: '12', termMonths: 4, periodMonths: 1, rateType: 'nominal', gracePeriods: 2, upfrontFee: '10', recurringFee: '2', }); assert.equal(result.periodicRatePercent, '1.000000'); assert.equal(result.netDisbursed, '1190.00'); assert.equal(result.schedule[0].interest, '12.00'); assert.equal(result.schedule[0].principal, '0.00'); assert.equal(result.schedule[0].fee, '2.00'); assert.equal(result.schedule.at(-1).closingBalance, '0.00'); assert.equal( result.schedule.reduce((sum, row) => sum + cents(row.principal), 0n), 120000n, ); assert.equal( result.schedule.reduce( (sum, row) => sum + cents(row.totalPayment), 0n, ) + cents(result.upfrontFee), cents(result.totalPaid), ); for (const row of result.schedule) { assert.equal( cents(row.openingBalance) - cents(row.principal), cents(row.closingBalance), ); } }); void test('loan schedule reconciles exact cents and adjusts the final installment', () => { const result = buildLoanSchedule({ principal: '1000.01', annualRatePercent: '7.123456', termMonths: 7, periodMonths: 1, rateType: 'nominal', }); assert.equal(result.periodicRatePercent, '0.593621'); assert.equal(result.totalInterest, '23.88'); assert.equal(result.totalPaid, '1023.89'); assert.equal(result.schedule.at(-1).closingBalance, '0.00'); assert.equal( result.schedule.reduce((sum, row) => sum + cents(row.principal), 0n), 100001n, ); assert.equal( result.schedule.reduce((sum, row) => sum + cents(row.totalPayment), 0n), cents(result.totalPaid), ); }); void test('effective annual rate is converted to the selected repayment period', () => { const result = buildLoanSchedule({ principal: '1000', annualRatePercent: '12', termMonths: 12, periodMonths: 12, rateType: 'effective', }); assert.equal(result.periodicRatePercent, '12.000000'); assert.equal(result.schedule[0].interest, '120.00'); assert.equal(result.totalPaid, '1120.00'); const quarterly = buildLoanSchedule({ principal: '1000', annualRatePercent: '12', termMonths: 12, periodMonths: 3, rateType: 'effective', }); assert.equal(quarterly.periodicRatePercent, '2.873734'); assert.equal(quarterly.schedule.at(-1).closingBalance, '0.00'); }); void test('loan schedules reconcile across rates, periodicities and grace periods', () => { for (const periodMonths of [1, 2, 3, 4, 6, 12]) { for (const rateType of ['nominal', 'effective']) { for (const annualRatePercent of ['0', '8.500000', '24']) { for (const gracePeriods of [0, 2]) { const result = buildLoanSchedule({ principal: '12345.67', annualRatePercent, termMonths: periodMonths * 12, periodMonths, rateType, gracePeriods, upfrontFee: '15.25', recurringFee: '2.37', }); assert.equal(result.schedule.length, 12); assert.equal(result.schedule.at(-1).closingBalance, '0.00'); assert.equal( result.schedule.reduce( (sum, row) => sum + cents(row.principal), 0n, ), 1_234_567n, ); for (const row of result.schedule) { assert.equal( cents(row.openingBalance) - cents(row.principal), cents(row.closingBalance), ); assert.ok(cents(row.closingBalance) >= 0n); } assert.equal( result.schedule.reduce( (sum, row) => sum + cents(row.totalPayment), 0n, ) + cents(result.upfrontFee), cents(result.totalPaid), ); assert.equal( cents(result.totalPaid), 1_234_567n + cents(result.totalInterest) + cents(result.totalFees), ); } } } } }); void test('budget preserves an operating deficit and rejects invalid values', () => { assert.deepEqual(calculateBudget(['100.00', '25.50'], ['140.00']), { incomeTotal: '125.50', expenseTotal: '140.00', operatingBalance: '-14.50', status: 'estimated', }); assert.throws(() => calculateBudget(['0'], ['-1']), /não negativo/); }); void test('budget distinguishes absent income or expense rows from explicit zero', () => { assert.throws(() => calculateBudget([], ['0']), /pelo menos uma linha/); assert.throws(() => calculateBudget(['0'], []), /pelo menos uma linha/); assert.deepEqual(calculateBudget(['0'], ['0']), { incomeTotal: '0.00', expenseTotal: '0.00', operatingBalance: '0.00', status: 'estimated', }); }); void test('loan terms reject invalid periodicity, excessive grace and commission larger than principal', () => { const base = { principal: '1000', annualRatePercent: '0', termMonths: 12, periodMonths: 1, rateType: 'nominal', }; assert.throws( () => buildLoanSchedule({ ...base, periodMonths: 5 }), /Periodicidade/, ); assert.throws( () => buildLoanSchedule({ ...base, gracePeriods: 12 }), /carência/, ); assert.throws( () => buildLoanSchedule({ ...base, upfrontFee: '1001' }), /exceder o capital/, ); }); void test('financial tools CSV records reproducible assumptions, periods, formulas and matching results', () => { const csv = exportFinancialToolsCsv({ currency: 'MZN', generatedAt: '2026-10-02T10:00:00.000Z', budgetPeriod: 'Mensal', loan: { principal: '1200', annualRatePercent: '12', termMonths: 12, periodMonths: 1, rateType: 'nominal', gracePeriods: 0, upfrontFee: '10', recurringFee: '2', }, breakEven: { fixedCosts: '1000', unitPrice: '30', variableCostPerUnit: '20', }, funding: { totalInvestment: '5000', securedFunding: '3000' }, incomes: [ { label: '=HYPERLINK("https://example.invalid")', amount: '2500' }, ], expenses: [{ label: 'Operação, mensal', amount: '1800' }], }); assert.match(csv, /^\uFEFF/); assert.match(csv, /financial-tools-v1/); assert.match(csv, /valores iniciais sao exemplos demonstrativos/); assert.match(csv, /2026-10-02T10:00:00\.000Z/); assert.match(csv, /Período do orçamento e dos custos fixos,Mensal/); assert.match(csv, /saldo decrescente/i); assert.match(csv, /Saldo operacional,,700\.00/); assert.match(csv, /Unidades de equilíbrio,100/); assert.match(csv, /Diferença por financiar,2000\.00/); assert.match(csv, /"'=HYPERLINK/); assert.match(csv, /"Operação, mensal"/); assert.match(csv, /não inclui impostos/); });