import assert from 'node:assert/strict'; import test from 'node:test'; import { exportMonthlyCashflowCsv, projectMonthlyCashflow, } from '../../resources/js/lib/monthly-cashflow.ts'; void test('projects exact monthly balances and reconciles cumulative net flow', () => { const result = projectMonthlyCashflow({ openingCash: '100.00', monthlyRevenue: '200.00', monthlyOperatingCosts: '150.00', monthlyDebtService: '10.00', monthlyTaxAssessed: '0', periods: 2, }); assert.equal(result.rows[0].closingCash, '140.00'); assert.equal(result.endingCash, '180.00'); assert.equal(result.firstDeficitMonth, null); }); void test('flags the first projected deficit without changing input values', () => { const result = projectMonthlyCashflow({ openingCash: '0', monthlyRevenue: '50', monthlyOperatingCosts: '75', monthlyDebtService: '0', monthlyTaxAssessed: '0', periods: 4, }); assert.equal(result.firstDeficitMonth, 1); assert.equal(result.endingCash, '-100.00'); }); void test('keeps VAT credit stages out of cash and includes only an explicitly forecast receipt', () => { const base = { openingCash: '0', monthlyRevenue: '100', monthlyOperatingCosts: '0', monthlyDebtService: '0', monthlyTaxAssessed: '10', vatCreditEstimated: '80', vatRefundRequested: '70', vatRefundRecognized: '60', periods: 3, }; const withoutReceipt = projectMonthlyCashflow(base); assert.equal(withoutReceipt.endingCash, '270.00'); assert.equal(withoutReceipt.rows[0].taxAssessed, '10.00'); assert.equal(withoutReceipt.rows[0].vatRefundExpectedReceipt, '0.00'); const withReceipt = projectMonthlyCashflow({ ...base, vatRefundExpectedReceipt: '60', vatRefundExpectedMonth: '2', }); assert.equal(withReceipt.rows[1].vatRefundExpectedReceipt, '60.00'); assert.equal(withReceipt.endingCash, '330.00'); assert.throws( () => projectMonthlyCashflow({ ...base, vatRefundExpectedReceipt: '60' }), /mês previsto/, ); assert.throws( () => projectMonthlyCashflow({ ...base, vatRefundExpectedReceipt: '60.01', vatRefundExpectedMonth: '2', }), /não pode exceder o montante reconhecido/, ); }); void test('unknown assessed tax keeps the cash projection incomplete instead of assuming zero', () => { const result = projectMonthlyCashflow({ openingCash: '100', monthlyRevenue: '200', monthlyOperatingCosts: '150', monthlyDebtService: '10', periods: 2, }); assert.equal(result.status, 'incomplete'); assert.equal(result.endingCash, null); assert.equal(result.endingCashBeforeUnassessedTax, '180.00'); assert.equal(result.firstDeficitMonth, null); assert.equal(result.rows[0].taxAssessed, null); assert.equal(result.rows[0].netFlow, null); assert.equal(result.rows[0].closingCash, null); assert.equal(result.rows[0].netFlowBeforeUnassessedTax, '40.00'); assert.equal(result.rows[0].closingCashBeforeUnassessedTax, '140.00'); const declaredZero = projectMonthlyCashflow({ openingCash: '100', monthlyRevenue: '200', monthlyOperatingCosts: '150', monthlyDebtService: '10', monthlyTaxAssessed: '0', periods: 2, }); assert.equal(declaredZero.status, 'estimated'); assert.equal(declaredZero.endingCash, '180.00'); }); void test('monthly tax calendar applies varying user assumptions and reconciles each closing balance', () => { const result = projectMonthlyCashflow({ openingCash: '100', monthlyRevenue: '200', monthlyOperatingCosts: '50', monthlyDebtService: '0', monthlyTaxSchedule: ['10', '30'], periods: 2, }); assert.equal(result.status, 'estimated'); assert.equal(result.rows[0].taxAssessed, '10.00'); assert.equal(result.rows[0].closingCash, '240.00'); assert.equal(result.rows[1].openingCash, '240.00'); assert.equal(result.rows[1].taxAssessed, '30.00'); assert.equal(result.endingCash, '360.00'); }); void test('a missing month remains indeterminate while known months and tax amounts reconcile', () => { const result = projectMonthlyCashflow({ openingCash: '100', monthlyRevenue: '200', monthlyOperatingCosts: '50', monthlyDebtService: '0', monthlyTaxSchedule: ['10', '', '30'], periods: 3, }); assert.equal(result.status, 'incomplete'); assert.equal(result.rows[0].closingCash, '240.00'); assert.equal(result.rows[1].taxAssessed, null); assert.equal(result.rows[1].netFlow, null); assert.equal(result.rows[1].closingCash, null); assert.equal(result.rows[1].netFlowBeforeUnassessedTax, '150.00'); assert.equal(result.rows[1].closingCashBeforeUnassessedTax, '390.00'); assert.equal(result.rows[2].openingCash, null); assert.equal(result.rows[2].taxAssessed, '30.00'); assert.equal(result.rows[2].netFlow, '120.00'); assert.equal(result.rows[2].closingCash, null); assert.equal(result.endingCash, null); assert.equal(result.endingCashBeforeUnassessedTax, '510.00'); }); void test('monthly tax schedule requires one entry per month and cannot conflict with the fixed amount', () => { const base = { openingCash: '0', monthlyRevenue: '100', monthlyOperatingCosts: '0', monthlyDebtService: '0', periods: 2, }; assert.throws( () => projectMonthlyCashflow({ ...base, monthlyTaxSchedule: ['10'] }), /cada mês projectado/, ); assert.throws( () => projectMonthlyCashflow({ ...base, monthlyTaxAssessed: '5', monthlyTaxSchedule: ['10', '20'], }), /não ambos/, ); }); void test('allows an opening cash deficit and rejects excess-precision or invalid horizon inputs', () => { assert.equal( projectMonthlyCashflow({ openingCash: '-1.00', monthlyRevenue: '0', monthlyOperatingCosts: '0', monthlyDebtService: '0', monthlyTaxAssessed: '0', periods: 1, }).endingCash, '-1.00', ); assert.throws( () => projectMonthlyCashflow({ openingCash: '0', monthlyRevenue: '1.001', monthlyOperatingCosts: '0', monthlyDebtService: '0', }), /duas casas/, ); assert.throws( () => projectMonthlyCashflow({ openingCash: '0', monthlyRevenue: '0', monthlyOperatingCosts: '0', monthlyDebtService: '0', periods: 61, }), /60 meses/, ); }); void test('CSV export includes assumptions, monthly totals and an explicit scope note', () => { const csv = exportMonthlyCashflowCsv( { openingCash: '10.00', monthlyRevenue: '20.00', monthlyOperatingCosts: '5.00', monthlyDebtService: '0.00', currency: 'MZN', periods: 2, }, '2026-10-03T10:00:00.000Z', ); assert.match(csv, /Saldo inicial,10\.00/); assert.match(csv, /Moeda,MZN/); assert.match(csv, /monthly-cashflow-v1/); assert.match(csv, /2026-10-03T10:00:00\.000Z/); assert.match(csv, /Horizonte,2 meses/); assert.match( csv, /Valores do formulário; os valores pré-preenchidos podem ser exemplos demonstrativos/, ); assert.match(csv, /Estado,incomplete/); assert.match(csv, /indeterminado; não informado/); assert.match(csv, /Fluxo líquido,Saldo final/); assert.match(csv, /indeterminado/); assert.match(csv, /antes de imposto não informado/); assert.match( csv, /Crédito estimado, pedido e montante reconhecido não entram no caixa/, ); assert.match(csv, /^\uFEFF/); }); void test('CSV export records each monthly tax assumption and unknown month explicitly', () => { const csv = exportMonthlyCashflowCsv( { openingCash: '0', monthlyRevenue: '100', monthlyOperatingCosts: '0', monthlyDebtService: '0', monthlyTaxSchedule: ['12.50', '', '0'], currency: 'MZN', periods: 3, }, '2026-10-03T10:00:00.000Z', ); assert.match(csv, /Calendário variável; consultar valores mensais abaixo/); assert.match( csv, /pressuposto do utilizador, não cálculo ou confirmação de obrigação fiscal/, ); assert.match(csv, /Pressuposto mensal de imposto,Mês 1,12\.50/); assert.match( csv, /Pressuposto mensal de imposto,Mês 2,indeterminado; não informado/, ); assert.match(csv, /Pressuposto mensal de imposto,Mês 3,0/); }); void test('cash flow CSV neutralizes formula-like assumption text', () => { const csv = exportMonthlyCashflowCsv({ openingCash: '10.00', monthlyRevenue: '20.00', monthlyOperatingCosts: '5.00', monthlyDebtService: '0.00', currency: 'MZN', vatRefundExpectedMonth: '=HYPERLINK("https://example.invalid")', periods: 1, }); assert.match( csv, /Mês previsto do recebimento IVA,"'=HYPERLINK\(""https:\/\/example\.invalid""\)"/, ); });