172 lines
6.1 KiB
JavaScript
172 lines
6.1 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { buildFinancialFiscalBridge } from '../../resources/js/lib/financial-fiscal-bridge.ts';
|
|
import { forecastBusinessPlan } from '../../resources/js/lib/business-financial-plan.ts';
|
|
|
|
const plan = {
|
|
currency: 'MZN',
|
|
openingCash: '0',
|
|
products: [
|
|
{
|
|
label: 'Serviço',
|
|
monthlyUnits: '10',
|
|
unitPrice: '100',
|
|
unitVariableCost: '30',
|
|
},
|
|
],
|
|
monthlyCollections: '800',
|
|
monthlyVariableCostPayments: '250',
|
|
monthlyOperatingExpenses: '100',
|
|
monthlyOperatingPayments: '80',
|
|
monthlyPayrollExpense: '50',
|
|
monthlyPayrollPayments: '40',
|
|
assetInvestment: '0',
|
|
inventory: '0',
|
|
receivables: '0',
|
|
payables: '0',
|
|
equity: '0',
|
|
shareholderLoans: '0',
|
|
bankLoanProceeds: '0',
|
|
grants: '0',
|
|
assetDepreciation: '0',
|
|
depreciationLifeMonths: 12,
|
|
monthlyInterestExpense: '10',
|
|
monthlyInterestPayment: '5',
|
|
monthlyPrincipalRepayment: '60',
|
|
periods: 2,
|
|
};
|
|
|
|
void test('financial to fiscal bridge reconciles accounting result while leaving tax base indeterminate', () => {
|
|
const forecast = forecastBusinessPlan(plan);
|
|
const bridge = buildFinancialFiscalBridge(forecast);
|
|
const byCode = Object.fromEntries(
|
|
bridge.accountingLines.map((line) => [line.code, line]),
|
|
);
|
|
const cents = (value) => {
|
|
const [whole, fraction] = value.split('.');
|
|
return BigInt(whole) * 100n + BigInt(fraction);
|
|
};
|
|
const accountingProfit = [
|
|
byCode.revenue.total,
|
|
...Object.values(byCode)
|
|
.filter((line) => line.accountingEffect === 'expense')
|
|
.map((line) => `-${line.total}`),
|
|
].reduce((sum, value) => sum + cents(value), 0n);
|
|
|
|
assert.equal(bridge.status, 'incomplete');
|
|
assert.equal(bridge.accountingProfitBeforeTax, '1080.00');
|
|
assert.equal(accountingProfit, cents(bridge.accountingProfitBeforeTax));
|
|
assert.equal(bridge.taxableBase.status, 'indeterminate');
|
|
assert.equal(bridge.taxableBase.amount, null);
|
|
assert.ok(
|
|
bridge.taxableBase.missingInputs.includes(
|
|
'expense_deductibility_classification',
|
|
),
|
|
);
|
|
assert.equal(
|
|
bridge.accountingLines.every(
|
|
(line) => line.taxTreatment === 'unclassified',
|
|
),
|
|
true,
|
|
);
|
|
assert.equal(byCode.revenue.monthlyAmounts[0].amount, '1000.00');
|
|
assert.equal(byCode.cost_of_sales.monthlyAmounts[0].amount, '300.00');
|
|
assert.equal(bridge.cashSchedule[0].collections, '800.00');
|
|
assert.equal(bridge.cashSchedule[0].collectionMode, 'manual');
|
|
assert.equal(bridge.cashSchedule[0].salesCollections, null);
|
|
assert.equal(bridge.cashSchedule[0].openingReceivablesCollections, '0.00');
|
|
assert.equal(bridge.cashSchedule[0].otherCollections, '800.00');
|
|
assert.equal(bridge.cashSchedule[0].collectionsTotal, '800.00');
|
|
assert.equal(bridge.cashSchedule[0].principalRepayments, '60.00');
|
|
assert.equal(
|
|
bridge.accountingLines.some(
|
|
(line) => line.code === 'principal_repayment',
|
|
),
|
|
false,
|
|
);
|
|
assert.notEqual(
|
|
bridge.cashSchedule[0].collections,
|
|
byCode.revenue.monthlyAmounts[0].amount,
|
|
);
|
|
});
|
|
|
|
void test('financial bridge separates opening receivables and scheduled sales collections', () => {
|
|
const forecast = forecastBusinessPlan({
|
|
...plan,
|
|
receivables: '200.00',
|
|
monthlyCollections: '0',
|
|
salesCollectionLagMonths: 0,
|
|
salesCollectionPercent: '75',
|
|
openingReceivablesCollectionMonth: 1,
|
|
openingReceivablesCollectionPercent: '50',
|
|
periods: 1,
|
|
});
|
|
const bridge = buildFinancialFiscalBridge(forecast);
|
|
assert.deepEqual(bridge.cashSchedule[0], {
|
|
period: 1,
|
|
collectionMode: 'scheduled',
|
|
salesCollections: '750.00',
|
|
openingReceivablesBalance: '100.00',
|
|
openingReceivablesCollections: '100.00',
|
|
otherCollections: '0.00',
|
|
collections: '850.00',
|
|
collectionsTotal: '850.00',
|
|
salesReceivablesBalance: '250.00',
|
|
supplierPayments: '250.00',
|
|
operatingPayments: '80.00',
|
|
personnelPayments: '40.00',
|
|
interestPayments: '5.00',
|
|
principalRepayments: '60.00',
|
|
netCashFlow: '415.00',
|
|
closingCash: '215.00',
|
|
});
|
|
assert.equal(
|
|
bridge.accountingLines.find((line) => line.code === 'revenue').total,
|
|
'1000.00',
|
|
);
|
|
assert.equal(bridge.taxableBase.status, 'indeterminate');
|
|
});
|
|
|
|
void test('bridge maps monthly recognised expenses but keeps scheduled payments in cash only', () => {
|
|
const forecast = forecastBusinessPlan({
|
|
...plan,
|
|
periods: 2,
|
|
monthlyOperatingExpensesByPeriod: ['100', '250'],
|
|
monthlyOperatingPaymentsByPeriod: ['20', '300'],
|
|
monthlyPayrollExpenseByPeriod: ['50', '75'],
|
|
monthlyPayrollPaymentsByPeriod: ['0', '100'],
|
|
});
|
|
const bridge = buildFinancialFiscalBridge(forecast);
|
|
const byCode = Object.fromEntries(
|
|
bridge.accountingLines.map((line) => [line.code, line]),
|
|
);
|
|
|
|
assert.deepEqual(byCode.operating_expenses.monthlyAmounts, [
|
|
{ period: 1, amount: '100.00' },
|
|
{ period: 2, amount: '250.00' },
|
|
]);
|
|
assert.deepEqual(byCode.personnel.monthlyAmounts, [
|
|
{ period: 1, amount: '50.00' },
|
|
{ period: 2, amount: '75.00' },
|
|
]);
|
|
assert.equal(bridge.cashSchedule[0].operatingPayments, '20.00');
|
|
assert.equal(bridge.cashSchedule[0].personnelPayments, '0.00');
|
|
assert.equal(bridge.cashSchedule[1].operatingPayments, '300.00');
|
|
assert.equal(bridge.cashSchedule[1].personnelPayments, '100.00');
|
|
assert.equal(bridge.taxableBase.status, 'indeterminate');
|
|
});
|
|
|
|
void test('financial bridge rejects a corrupted monthly cash schedule', () => {
|
|
const forecast = forecastBusinessPlan(plan);
|
|
const corrupted = {
|
|
...forecast,
|
|
rows: forecast.rows.map((row, index) =>
|
|
index === 0 ? { ...row, closingCash: '999999.00' } : row,
|
|
),
|
|
};
|
|
assert.throws(
|
|
() => buildFinancialFiscalBridge(corrupted),
|
|
/saldo de caixa mensal não reconcilia/,
|
|
);
|
|
});
|