Files
Investidor-V2/tests/frontend/monthly-cashflow.test.mjs
T
nala 1106509116
tests / ci (push) Canceled after 0s
vrsao 2
2026-10-08 14:02:01 +02:00

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""\)"/,
);
});