198 lines
7.0 KiB
JavaScript
198 lines
7.0 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { forecastBusinessPlan } from '../../resources/js/lib/business-financial-plan.ts';
|
|
import { projectMonthlyCashflow } from '../../resources/js/lib/monthly-cashflow.ts';
|
|
import {
|
|
defaultCashflowDraftInputs,
|
|
serializeCashflowDraftSnapshot,
|
|
restoreCashflowDraftSnapshot,
|
|
} from '../../resources/js/lib/monthly-cashflow.ts';
|
|
import { serializeFinancialToolsSnapshot } from '../../resources/js/lib/financial-calculators.ts';
|
|
import { buildProjectDossierRows } from '../../resources/js/lib/project-dossier.ts';
|
|
import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts';
|
|
|
|
test('cashflow draft recovery preserves unanswered months and invalid editable values without using zero', () => {
|
|
const inputs = {
|
|
...defaultCashflowDraftInputs(),
|
|
openingCash: '77.01',
|
|
monthlyTaxSchedule: ['10', '', '20', ...Array(9).fill('0')],
|
|
};
|
|
const stored = serializeCashflowDraftSnapshot('schedule', inputs);
|
|
const restored = restoreCashflowDraftSnapshot(stored);
|
|
assert.equal(restored.taxMode, 'schedule');
|
|
assert.deepEqual(restored.inputs, inputs);
|
|
assert.equal(restored.inputs.monthlyTaxSchedule[1], '');
|
|
const rows = buildProjectDossierRows({
|
|
currency: 'MZN',
|
|
cashflow_snapshot: stored,
|
|
});
|
|
assert.equal(
|
|
rows.find(
|
|
(row) => row[0] === 'Tesouraria' && row[1] === 'Saldo final total',
|
|
)[2],
|
|
'Indeterminado',
|
|
);
|
|
assert.ok(
|
|
rows
|
|
.find((row) => row[0] === 'Tesouraria' && row[1] === 'Mês 2')[2]
|
|
.includes('Imposto assumido: Indeterminado'),
|
|
);
|
|
const report = createProjectDossierReport(
|
|
{
|
|
currency: 'MZN',
|
|
cashflow_snapshot: stored,
|
|
},
|
|
{
|
|
selectedSections: new Set(['summary', 'finance']),
|
|
version: 'SINTETICO QA',
|
|
},
|
|
);
|
|
assert.equal(
|
|
report.sections
|
|
.find((section) => section.id === 'summary')
|
|
.rows.find(
|
|
(row) => row.field === 'Saldo final de tesouraria — 12 meses',
|
|
).value,
|
|
'Indeterminado',
|
|
);
|
|
const invalid = { ...inputs, monthlyRevenue: 'corrigir' };
|
|
assert.equal(
|
|
restoreCashflowDraftSnapshot(
|
|
serializeCashflowDraftSnapshot('fixed', invalid),
|
|
).inputs.monthlyRevenue,
|
|
'corrigir',
|
|
);
|
|
assert.equal(restoreCashflowDraftSnapshot('{"version":2}'), null);
|
|
assert.equal(
|
|
restoreCashflowDraftSnapshot(
|
|
stored.replace('"version":1', '"version":2'),
|
|
),
|
|
null,
|
|
);
|
|
});
|
|
|
|
test('credit report preserves the complete reproducible recipe alongside the result', () => {
|
|
const rows = buildProjectDossierRows({
|
|
purpose: 'simulate_only',
|
|
journey_type: 'small_business',
|
|
currency: 'MZN',
|
|
financial_tools_snapshot: serializeFinancialToolsSnapshot({
|
|
budgetPeriod: 'Mensal',
|
|
loan: {
|
|
principal: '1000',
|
|
rate: '12',
|
|
months: '12',
|
|
rateType: 'nominal',
|
|
periodMonths: '1',
|
|
gracePeriods: '0',
|
|
upfrontFee: '0',
|
|
recurringFee: '0',
|
|
},
|
|
breakEven: { fixed: '100', price: '20', variable: '10' },
|
|
funding: { total: '1000', secured: '0' },
|
|
incomes: [{ label: 'QA', amount: '100' }],
|
|
expenses: [{ label: 'QA', amount: '50' }],
|
|
}),
|
|
});
|
|
const recipe = rows.find(
|
|
(row) => row[1] === 'Método e pressupostos substituídos',
|
|
)[2];
|
|
for (const text of [
|
|
'1000 MZN',
|
|
'12% nominal anual',
|
|
'12 meses',
|
|
'periodicidade 1 meses',
|
|
'carência 0',
|
|
'comissão inicial 0 MZN',
|
|
'comissão periódica 0 MZN',
|
|
'duas casas decimais',
|
|
'metade para cima',
|
|
'saldo residual',
|
|
])
|
|
assert.ok(recipe.includes(text), text);
|
|
assert.equal(
|
|
rows.find((row) => row[1] === 'Prestação do primeiro período')[2],
|
|
'88.85 MZN',
|
|
);
|
|
});
|
|
|
|
// Synthetic QA control ledger, independently calculated before invoking the engines.
|
|
// Sales 200; variable expense 80; operations 20; payroll 10;
|
|
// depreciation 10; interest expense 5 => profit before tax 75/month.
|
|
// Cash: receipts 180; inventory 70; operations 25; payroll 10;
|
|
// interest paid 7; principal paid 12 => cash increases 56/month.
|
|
// Initial funding 1200; asset 600 + inventory 100 + receivables 50 - payables 20 = 730;
|
|
// opening cash 470 => closing 638 after three months. No tax rate is assumed.
|
|
test('independent three-month ledger separates profit working capital and cash', () => {
|
|
const result = forecastBusinessPlan({
|
|
currency: 'MZN',
|
|
openingCash: '0',
|
|
products: [
|
|
{
|
|
label: 'SINTETICO QA',
|
|
monthlyUnits: '10',
|
|
unitPrice: '20',
|
|
unitVariableCost: '8',
|
|
},
|
|
],
|
|
monthlyCollections: '180',
|
|
monthlyVariableCostPayments: '70',
|
|
monthlyInventoryPurchases: '70',
|
|
monthlyOperatingExpenses: '20',
|
|
monthlyOperatingPayments: '25',
|
|
monthlyPayrollExpense: '10',
|
|
monthlyPayrollPayments: '10',
|
|
assetInvestment: '600',
|
|
inventory: '100',
|
|
receivables: '50',
|
|
payables: '20',
|
|
equity: '1000',
|
|
shareholderLoans: '0',
|
|
bankLoanProceeds: '200',
|
|
grants: '0',
|
|
assetDepreciation: '600',
|
|
depreciationLifeMonths: 60,
|
|
monthlyInterestExpense: '5',
|
|
monthlyInterestPayment: '7',
|
|
monthlyPrincipalRepayment: '12',
|
|
periods: 3,
|
|
});
|
|
assert.equal(result.workingCapital, '130.00');
|
|
assert.equal(result.initialUses, '730.00');
|
|
assert.equal(result.openingCashAfterInitialUses, '470.00');
|
|
assert.equal(result.totalProfitBeforeTax, '225.00');
|
|
assert.equal(result.endingCash, '638.00');
|
|
assert.equal(result.totalCashCollections, '540.00');
|
|
});
|
|
|
|
test('independent cash ledger excludes VAT stages and preserves the unknown tax month', () => {
|
|
const input = {
|
|
currency: 'MZN',
|
|
openingCash: '100',
|
|
monthlyRevenue: '200',
|
|
monthlyOperatingCosts: '150',
|
|
monthlyDebtService: '20',
|
|
monthlyTaxSchedule: ['10', '', '20'],
|
|
vatCreditEstimated: '1000',
|
|
vatRefundRequested: '900',
|
|
vatRefundRecognized: '800',
|
|
vatRefundExpectedReceipt: '50',
|
|
vatRefundExpectedMonth: '2',
|
|
periods: 3,
|
|
};
|
|
const result = projectMonthlyCashflow(input);
|
|
// Known cash: 100 + 20 + 80 + 10 = 210; month 2 tax is unknown.
|
|
assert.equal(result.rows[0].closingCash, '120.00');
|
|
assert.equal(result.rows[1].closingCash, null);
|
|
assert.equal(result.endingCash, null);
|
|
assert.equal(result.endingCashBeforeUnassessedTax, '210.00');
|
|
assert.equal(
|
|
projectMonthlyCashflow({
|
|
...input,
|
|
monthlyTaxSchedule: ['10', '0', '20'],
|
|
}).endingCash,
|
|
'210.00',
|
|
);
|
|
assert.equal(input.monthlyTaxSchedule[1], '');
|
|
});
|