277 lines
9.4 KiB
JavaScript
277 lines
9.4 KiB
JavaScript
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""\)"/,
|
|
);
|
|
});
|