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

2056 lines
69 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import {
compareBusinessScenarios,
exportBusinessPlanCsv,
forecastBusinessPlan,
restoreBusinessPlanSnapshot,
serializeBusinessPlanSnapshot,
} from '../../resources/js/lib/business-financial-plan.ts';
import {
partitionSimulationDraft,
restoreSimulationDraft,
} from '../../resources/js/lib/simulation-form.ts';
const assumptions = {
currency: 'MZN',
openingCash: '0',
products: [
{
label: 'Serviço',
monthlyUnits: '100',
unitPrice: '1000',
unitVariableCost: '400',
},
],
monthlyCollections: '90000',
salesCollectionLagMonths: null,
salesCollectionPercent: '100',
salesRecoverySchedule: [],
monthlyVariableCostPayments: '42000',
monthlyVariableCostPaymentsByPeriod: null,
monthlyInventoryPurchases: '40000',
inventoryPurchaseLeadMonths: 0,
inventoryPurchaseSchedule: null,
useInventoryPurchaseSchedule: false,
useProductInventorySchedule: false,
useSupplierPaymentTerms: false,
monthlyOperatingExpenses: '20000',
monthlyOperatingPayments: '18000',
monthlyPayrollExpense: '15000',
monthlyPayrollPayments: '15000',
assetInvestment: '100000',
inventory: '20000',
receivables: '10000',
openingReceivablesCollectionMonth: null,
openingReceivablesCollectionPercent: '100',
openingReceivablesInstallments: [],
openingPayablesInstallments: [],
payables: '5000',
equity: '80000',
shareholderLoans: '0',
bankLoanProceeds: '50000',
grants: '0',
assetDepreciation: '100000',
depreciationLifeMonths: 60,
monthlyInterestExpense: '1200',
monthlyInterestPayment: '1000',
monthlyPrincipalRepayment: '5000',
periods: 12,
};
void test('financial forecast reconciles revenue, finance sources, working capital and monthly cash', () => {
const result = forecastBusinessPlan(assumptions);
assert.equal(result.workingCapital, '25000.00');
assert.equal(result.initialUses, '125000.00');
assert.equal(result.financing, '130000.00');
assert.equal(result.fundingGap, '-5000.00');
assert.equal(result.fundingStatus, 'surplus');
assert.equal(
forecastBusinessPlan({ ...assumptions, equity: '75000' }).fundingStatus,
'balanced',
);
assert.equal(
forecastBusinessPlan({ ...assumptions, equity: '0' }).fundingStatus,
'shortfall',
);
assert.equal(result.openingCashAfterInitialUses, '5000.00');
assert.equal(result.totalRevenue, '1200000.00');
assert.equal(result.totalCashCollections, '1080000.00');
assert.equal(result.inventoryProjection.status, 'estimated');
assert.equal(result.inventoryProjection.endingInventory, '20000.00');
assert.equal(result.rows[0].inventoryConsumption, '40000.00');
assert.equal(result.rows[0].revenue, '100000.00');
assert.equal(result.rows[0].cashCollections, '90000.00');
assert.equal(result.rows[0].salesCollections, null);
assert.equal(result.totalSalesCollections, null);
assert.equal(result.rows[0].principalRepayment, '5000.00');
assert.equal(result.rows[0].interestExpense, '1200.00');
assert.equal(result.rows[0].depreciation, '1666.67');
assert.equal(result.endingCash, '113000.00');
});
void test('opening payable installments reduce cash without changing profit and retain unpaid balances', () => {
const opening = {
...assumptions,
periods: 3,
products: [
{
...assumptions.products[0],
monthlyUnits: '0',
unitPrice: '0',
unitVariableCost: '0',
},
],
monthlyCollections: '0',
monthlyVariableCostPayments: '0',
monthlyInventoryPurchases: '0',
monthlyOperatingExpenses: '0',
monthlyOperatingPayments: '0',
monthlyPayrollExpense: '0',
monthlyPayrollPayments: '0',
assetInvestment: '1000',
inventory: '0',
receivables: '0',
payables: '1000',
equity: '0',
shareholderLoans: '0',
bankLoanProceeds: '0',
grants: '0',
assetDepreciation: '0',
monthlyInterestExpense: '0',
monthlyInterestPayment: '0',
monthlyPrincipalRepayment: '0',
};
const unscheduled = forecastBusinessPlan(opening);
const scheduled = forecastBusinessPlan({
...opening,
openingPayablesInstallments: [
{ month: 1, percent: '50' },
{ month: 3, percent: '25' },
],
});
assert.equal(unscheduled.initialUses, '0.00');
assert.equal(unscheduled.endingCash, '0.00');
assert.equal(unscheduled.openingPayablesAtEnd, '1000.00');
assert.deepEqual(
scheduled.rows.map((row) => row.openingPayablesPayments),
['500.00', '0.00', '250.00'],
);
assert.deepEqual(
scheduled.rows.map((row) => row.openingPayablesBalance),
['500.00', '500.00', '250.00'],
);
assert.equal(scheduled.totalOpeningPayablesPayments, '750.00');
assert.equal(scheduled.openingPayablesAtEnd, '250.00');
assert.equal(scheduled.totalPayments, '750.00');
assert.equal(scheduled.endingCash, '-750.00');
assert.equal(
scheduled.totalProfitBeforeTax,
unscheduled.totalProfitBeforeTax,
);
assert.throws(
() =>
forecastBusinessPlan({
...opening,
openingPayablesInstallments: [
{ month: 1, percent: '60' },
{ month: 2, percent: '40.01' },
],
}),
/100%/,
);
assert.throws(
() =>
forecastBusinessPlan({
...opening,
openingPayablesInstallments: [{ month: 4, percent: '50' }],
}),
/dentro do horizonte/,
);
});
void test('opening payable schedule caps cent rounding at the remaining balance', () => {
const rounded = forecastBusinessPlan({
...assumptions,
periods: 2,
products: [
{
...assumptions.products[0],
monthlyUnits: '0',
unitPrice: '0',
unitVariableCost: '0',
},
],
monthlyCollections: '0',
monthlyVariableCostPayments: '0',
monthlyOperatingExpenses: '0',
monthlyOperatingPayments: '0',
monthlyPayrollExpense: '0',
monthlyPayrollPayments: '0',
assetInvestment: '0.01',
inventory: '0',
receivables: '0',
payables: '0.01',
equity: '0',
shareholderLoans: '0',
bankLoanProceeds: '0',
grants: '0',
assetDepreciation: '0',
monthlyInterestExpense: '0',
monthlyInterestPayment: '0',
monthlyPrincipalRepayment: '0',
openingPayablesInstallments: [
{ month: 1, percent: '50' },
{ month: 2, percent: '50' },
],
});
assert.deepEqual(
rounded.rows.map((row) => row.openingPayablesPayments),
['0.01', '0.00'],
);
assert.equal(rounded.openingPayablesAtEnd, '0.00');
});
void test('monthly unit schedules drive revenue, costs, inventory and scenario volume', () => {
const seasonal = {
...assumptions,
periods: 3,
products: [
{
...assumptions.products[0],
monthlyUnitsByPeriod: ['100', '50', '0', '20'],
},
],
monthlyCollections: '0',
monthlyVariableCostPayments: '0',
monthlyInventoryPurchases: '0',
monthlyOperatingExpenses: '0',
monthlyOperatingPayments: '0',
monthlyPayrollExpense: '0',
monthlyPayrollPayments: '0',
assetInvestment: '0',
inventory: '0',
receivables: '0',
payables: '0',
equity: '0',
bankLoanProceeds: '0',
assetDepreciation: '0',
monthlyInterestExpense: '0',
monthlyInterestPayment: '0',
monthlyPrincipalRepayment: '0',
};
const result = forecastBusinessPlan(seasonal);
assert.deepEqual(
result.rows.map((row) => row.revenue),
['100000.00', '50000.00', '0.00'],
);
assert.deepEqual(
result.rows.map((row) => row.variableCost),
['40000.00', '20000.00', '0.00'],
);
assert.equal(result.totalRevenue, '150000.00');
assert.equal(result.inventoryProjection.totalConsumption, '60000.00');
assert.equal(
forecastBusinessPlan({ ...seasonal, periods: 2 }).totalRevenue,
'150000.00',
);
const scenarios = compareBusinessScenarios({
...seasonal,
scenarioAdjustments: {
favorable: {
unitVolumePercent: '20',
unitPricePercent: '0',
unitCostPercent: '0',
operatingCostsPercent: '0',
cashCollectionsPercent: '0',
},
unfavorable: {
unitVolumePercent: '-20',
unitPricePercent: '0',
unitCostPercent: '0',
operatingCostsPercent: '0',
cashCollectionsPercent: '0',
},
},
});
assert.deepEqual(
scenarios.favorable.result.rows.map((row) => row.revenue),
['120000.00', '60000.00', '0.00'],
);
assert.deepEqual(
scenarios.unfavorable.result.rows.map((row) => row.revenue),
['80000.00', '40000.00', '0.00'],
);
});
void test('monthly unit schedules must match the horizon and contain valid quantities', () => {
assert.throws(
() =>
forecastBusinessPlan({
...assumptions,
periods: 2,
products: [
{
...assumptions.products[0],
monthlyUnitsByPeriod: ['10'],
},
],
}),
/cobrir os 2 meses/,
);
assert.throws(
() =>
forecastBusinessPlan({
...assumptions,
periods: 1,
products: [
{
...assumptions.products[0],
monthlyUnitsByPeriod: ['-1'],
},
],
}),
/Quantidade/,
);
assert.throws(
() =>
forecastBusinessPlan({
...assumptions,
periods: 1,
products: [
{
...assumptions.products[0],
monthlyUnitsByPeriod: ['1.0001'],
},
],
}),
/Quantidade/,
);
});
void test('monthly prices and unit costs affect revenue, consumption and scenarios', () => {
const plan = {
...assumptions,
periods: 3,
products: [
{
...assumptions.products[0],
monthlyUnitsByPeriod: ['100', '50', '0', '20'],
unitPriceByPeriod: ['1000', '1200', '900', '500'],
unitVariableCostByPeriod: ['400', '550', '300', '200'],
},
],
};
const result = forecastBusinessPlan(plan);
assert.deepEqual(
result.rows.map((row) => row.revenue),
['100000.00', '60000.00', '0.00'],
);
assert.deepEqual(
result.rows.map((row) => row.variableCost),
['40000.00', '27500.00', '0.00'],
);
assert.equal(result.totalRevenue, '160000.00');
assert.equal(result.inventoryProjection.totalConsumption, '67500.00');
const scenarios = compareBusinessScenarios({
...plan,
scenarioAdjustments: {
favorable: {
unitVolumePercent: '0',
unitPricePercent: '10',
unitCostPercent: '-20',
operatingCostsPercent: '0',
cashCollectionsPercent: '0',
},
unfavorable: {
unitVolumePercent: '0',
unitPricePercent: '-10',
unitCostPercent: '20',
operatingCostsPercent: '0',
cashCollectionsPercent: '0',
},
},
});
assert.deepEqual(
scenarios.favorable.result.rows.map((row) => row.revenue),
['110000.00', '66000.00', '0.00'],
);
assert.deepEqual(
scenarios.favorable.result.rows.map((row) => row.variableCost),
['32000.00', '22000.00', '0.00'],
);
});
void test('monthly money schedules cover the horizon and reject invalid amounts', () => {
assert.throws(
() =>
forecastBusinessPlan({
...assumptions,
periods: 2,
products: [
{
...assumptions.products[0],
unitPriceByPeriod: ['1000'],
},
],
}),
/agenda deve cobrir os 2 meses/,
);
assert.throws(
() =>
forecastBusinessPlan({
...assumptions,
periods: 1,
products: [
{
...assumptions.products[0],
unitVariableCostByPeriod: ['-1'],
},
],
}),
/não negativo/,
);
assert.throws(
() =>
forecastBusinessPlan({
...assumptions,
periods: 1,
products: [
{
...assumptions.products[0],
unitPriceByPeriod: ['1.001'],
},
],
}),
/até duas casas decimais/,
);
});
void test('monthly operating and payroll expense schedules remain separate from payments', () => {
const result = forecastBusinessPlan({
...assumptions,
periods: 3,
monthlyOperatingExpensesByPeriod: ['100', '200', '300'],
monthlyOperatingPaymentsByPeriod: ['0', '150', '450'],
monthlyPayrollExpenseByPeriod: ['50', '75', '100'],
monthlyPayrollPaymentsByPeriod: ['25', '100', '100'],
});
assert.deepEqual(
result.rows.map((row) => row.operatingExpenses),
['100.00', '200.00', '300.00'],
);
assert.deepEqual(
result.rows.map((row) => row.operatingPayments),
['0.00', '150.00', '450.00'],
);
assert.deepEqual(
result.rows.map((row) => row.payrollExpense),
['50.00', '75.00', '100.00'],
);
assert.deepEqual(
result.rows.map((row) => row.payrollPayments),
['25.00', '100.00', '100.00'],
);
assert.equal(result.totalProfitBeforeTax, '170574.99');
const scenarios = compareBusinessScenarios({
...assumptions,
periods: 1,
monthlyOperatingExpensesByPeriod: ['1000'],
monthlyPayrollExpenseByPeriod: ['500'],
scenarioAdjustments: {
favorable: {
unitVolumePercent: '0',
unitPricePercent: '0',
unitCostPercent: '0',
operatingCostsPercent: '-10',
cashCollectionsPercent: '0',
},
unfavorable: {
unitVolumePercent: '0',
unitPricePercent: '0',
unitCostPercent: '0',
operatingCostsPercent: '10',
cashCollectionsPercent: '0',
},
},
});
assert.equal(
scenarios.favorable.result.rows[0].operatingExpenses,
'900.00',
);
assert.equal(scenarios.favorable.result.rows[0].payrollExpense, '450.00');
});
void test('monthly operating schedules require full horizon, exact cents and current snapshot version', () => {
assert.throws(
() =>
forecastBusinessPlan({
...assumptions,
periods: 2,
monthlyOperatingPaymentsByPeriod: ['10'],
}),
/deve cobrir os 2 meses/,
);
assert.throws(
() =>
forecastBusinessPlan({
...assumptions,
periods: 1,
monthlyPayrollExpenseByPeriod: ['-1'],
}),
/não negativo/,
);
const restored = restoreBusinessPlanSnapshot(
serializeBusinessPlanSnapshot({
...assumptions,
periods: 2,
monthlyOperatingExpensesByPeriod: ['100', '200'],
monthlyOperatingPaymentsByPeriod: ['25', '75'],
}),
);
assert.deepEqual(restored?.monthlyOperatingExpensesByPeriod, [
'100',
'200',
]);
assert.deepEqual(restored?.monthlyOperatingPaymentsByPeriod, ['25', '75']);
assert.equal(
restoreBusinessPlanSnapshot(
JSON.stringify({
version: 10,
plan: {
...assumptions,
monthlyOperatingExpensesByPeriod: ['100'],
},
}),
),
null,
);
const csv = exportBusinessPlanCsv({
...assumptions,
periods: 2,
monthlyOperatingExpensesByPeriod: ['100', '200'],
monthlyOperatingPaymentsByPeriod: ['25', '75'],
monthlyPayrollExpenseByPeriod: ['40', '50'],
monthlyPayrollPaymentsByPeriod: ['30', '60'],
});
assert.ok(csv.includes('"[""100"",""200""]"'));
assert.ok(csv.includes('"[""25"",""75""]"'));
assert.ok(csv.includes('"[""40"",""50""]"'));
assert.ok(csv.includes('"[""30"",""60""]"'));
assert.match(csv, /Pagamentos operacionais/);
assert.match(csv, /Pagamentos de pessoal/);
});
void test('losses and negative closing cash remain visible instead of being clamped to zero', () => {
const result = forecastBusinessPlan({
...assumptions,
openingCash: '-100',
products: [{ ...assumptions.products[0], monthlyUnits: '0' }],
monthlyCollections: '0',
monthlyVariableCostPayments: '0',
monthlyOperatingPayments: '50000',
monthlyPayrollPayments: '0',
assetInvestment: '0',
inventory: '0',
receivables: '0',
payables: '0',
equity: '0',
bankLoanProceeds: '0',
monthlyInterestPayment: '0',
monthlyPrincipalRepayment: '0',
periods: 1,
});
assert.ok(Number(result.rows[0].profitBeforeTax) < 0);
assert.ok(Number(result.endingCash) < 0);
});
void test('principal repayments affect cash but are excluded from profit expenses', () => {
const withPrincipal = forecastBusinessPlan(assumptions);
const withoutPrincipal = forecastBusinessPlan({
...assumptions,
monthlyPrincipalRepayment: '0',
});
assert.equal(
withPrincipal.rows[0].profitBeforeTax,
withoutPrincipal.rows[0].profitBeforeTax,
);
assert.equal(
Number(withoutPrincipal.rows[0].closingCash) -
Number(withPrincipal.rows[0].closingCash),
5000,
);
});
void test('depreciation allocation reconciles exactly over useful life', () => {
const result = forecastBusinessPlan({
...assumptions,
assetDepreciation: '100.00',
depreciationLifeMonths: 3,
periods: 3,
});
assert.deepEqual(
result.rows.map((row) => row.depreciation),
['33.33', '33.33', '33.34'],
);
});
void test('missing currency, products, invalid numbers and invalid horizons are explicit errors', () => {
assert.throws(
() => forecastBusinessPlan({ ...assumptions, currency: '' }),
/moeda ISO/,
);
assert.throws(
() => forecastBusinessPlan({ ...assumptions, products: [] }),
/produto/,
);
assert.throws(
() =>
forecastBusinessPlan({ ...assumptions, monthlyCollections: '-10' }),
/não negativo/,
);
assert.throws(
() => forecastBusinessPlan({ ...assumptions, periods: 61 }),
/1 e 60/,
);
assert.throws(
() =>
forecastBusinessPlan({
...assumptions,
inventoryPurchaseLeadMonths: 13,
}),
/entre 0 e 12 meses/,
);
assert.throws(
() =>
forecastBusinessPlan({
...assumptions,
inventoryPurchaseLeadMonths: 1.5,
}),
/entre 0 e 12 meses/,
);
assert.throws(
() =>
forecastBusinessPlan({
...assumptions,
salesCollectionLagMonths: 13,
}),
/prazo de cobrança.*0 e 12 meses/,
);
});
void test('versioned plan snapshots restore assumptions and reject unsupported or oversized data', () => {
const saved = serializeBusinessPlanSnapshot(assumptions);
assert.deepEqual(restoreBusinessPlanSnapshot(saved), assumptions);
assert.equal(
restoreBusinessPlanSnapshot('{"version":4,"plan":{"products":[]}}'),
null,
);
assert.equal(
restoreBusinessPlanSnapshot('{"version":"3","plan":{"products":[]}}'),
null,
);
assert.equal(
restoreBusinessPlanSnapshot('{"version":3,"plan":{"products":[]}}'),
null,
);
assert.equal(
restoreBusinessPlanSnapshot('{"version":1,"plan":{"products":[null]}}'),
null,
);
assert.equal(restoreBusinessPlanSnapshot('x'.repeat(60001)), null);
const legacyPlan = { ...assumptions };
delete legacyPlan.monthlyInventoryPurchases;
const restoredLegacy = restoreBusinessPlanSnapshot(
JSON.stringify({ version: 1, plan: legacyPlan }),
);
assert.equal(restoredLegacy?.monthlyInventoryPurchases, undefined);
assert.equal(restoredLegacy?.inventoryPurchaseLeadMonths, 0);
assert.equal(
forecastBusinessPlan(restoredLegacy).inventoryProjection.status,
'indeterminate',
);
const v2Plan = { ...assumptions };
delete v2Plan.inventoryPurchaseLeadMonths;
delete v2Plan.salesCollectionLagMonths;
assert.equal(
restoreBusinessPlanSnapshot(
JSON.stringify({ version: 2, plan: v2Plan }),
)?.inventoryPurchaseLeadMonths,
0,
);
assert.equal(
restoreBusinessPlanSnapshot(
JSON.stringify({
version: 3,
plan: { ...assumptions, salesCollectionLagMonths: undefined },
}),
)?.salesCollectionLagMonths,
null,
);
const legacyV4 = restoreBusinessPlanSnapshot(
JSON.stringify({ version: 4, plan: assumptions }),
);
assert.equal(legacyV4?.salesCollectionPercent, '100');
const legacyV5Plan = { ...assumptions };
delete legacyV5Plan.openingReceivablesCollectionMonth;
delete legacyV5Plan.openingReceivablesCollectionPercent;
delete legacyV5Plan.openingReceivablesInstallments;
const legacyV5 = restoreBusinessPlanSnapshot(
JSON.stringify({ version: 5, plan: legacyV5Plan }),
);
assert.equal(legacyV5?.openingReceivablesCollectionMonth, null);
assert.equal(legacyV5?.openingReceivablesCollectionPercent, '100');
const current = JSON.parse(serializeBusinessPlanSnapshot(assumptions));
assert.equal(current.version, 17);
assert.equal(current.plan.salesCollectionPercent, '100');
assert.equal(current.plan.openingReceivablesCollectionMonth, null);
assert.deepEqual(current.plan.openingReceivablesInstallments, []);
assert.deepEqual(current.plan.openingPayablesInstallments, []);
assert.deepEqual(current.plan.salesRecoverySchedule, []);
assert.equal(current.plan.useInventoryPurchaseSchedule, false);
assert.equal(current.plan.inventoryPurchaseSchedule, null);
const legacyV16Snapshot = JSON.parse(
serializeBusinessPlanSnapshot(assumptions),
);
legacyV16Snapshot.version = 16;
delete legacyV16Snapshot.plan.openingPayablesInstallments;
assert.deepEqual(
restoreBusinessPlanSnapshot(JSON.stringify(legacyV16Snapshot))
?.openingPayablesInstallments,
[],
);
const paymentSnapshot = serializeBusinessPlanSnapshot({
...assumptions,
periods: 2,
monthlyVariableCostPaymentsByPeriod: ['0.01', '2500.50'],
});
assert.deepEqual(restoreBusinessPlanSnapshot(paymentSnapshot), {
...assumptions,
periods: 2,
monthlyVariableCostPaymentsByPeriod: ['0.01', '2500.50'],
});
const invalidPaymentSnapshot = JSON.parse(paymentSnapshot);
invalidPaymentSnapshot.plan.monthlyVariableCostPaymentsByPeriod = ['10'];
assert.equal(
restoreBusinessPlanSnapshot(JSON.stringify(invalidPaymentSnapshot)),
null,
);
const productInventoryPlan = {
...assumptions,
periods: 2,
inventory: '1500',
useProductInventorySchedule: true,
products: [
{
...assumptions.products[0],
productInventoryOpening: '1500',
productInventoryPurchasesByPeriod: [
{ amount: '20.50', leadMonths: 1 },
{ amount: '0', leadMonths: 0 },
],
},
],
};
const productInventorySnapshot =
serializeBusinessPlanSnapshot(productInventoryPlan);
assert.deepEqual(
restoreBusinessPlanSnapshot(productInventorySnapshot),
productInventoryPlan,
);
const legacyProductInventorySnapshot = JSON.parse(productInventorySnapshot);
legacyProductInventorySnapshot.version = 14;
delete legacyProductInventorySnapshot.plan.useSupplierPaymentTerms;
assert.deepEqual(
restoreBusinessPlanSnapshot(
JSON.stringify(legacyProductInventorySnapshot),
),
productInventoryPlan,
);
const malformedProductInventorySnapshot = JSON.parse(
productInventorySnapshot,
);
malformedProductInventorySnapshot.plan.products[0].productInventoryPurchasesByPeriod[0].leadMonths = 13;
assert.equal(
restoreBusinessPlanSnapshot(
JSON.stringify(malformedProductInventorySnapshot),
),
null,
);
const supplierTermsPlan = {
...productInventoryPlan,
useSupplierPaymentTerms: true,
products: productInventoryPlan.products.map((product) => ({
...product,
productInventoryPurchasesByPeriod:
product.productInventoryPurchasesByPeriod?.map((entry) => ({
...entry,
paymentLagMonths: 2,
})),
})),
};
const supplierTermsSnapshot =
serializeBusinessPlanSnapshot(supplierTermsPlan);
assert.deepEqual(restoreBusinessPlanSnapshot(supplierTermsSnapshot), {
...supplierTermsPlan,
products: supplierTermsPlan.products.map((product) => ({
...product,
productInventoryPurchasesByPeriod:
product.productInventoryPurchasesByPeriod?.map((entry) => ({
...entry,
paymentInstallments: [{ lagMonths: 2, percent: '100' }],
})),
})),
});
const legacySupplierSnapshot = JSON.parse(supplierTermsSnapshot);
legacySupplierSnapshot.version = 15;
for (const product of legacySupplierSnapshot.plan.products) {
for (const entry of product.productInventoryPurchasesByPeriod) {
delete entry.paymentInstallments;
}
}
assert.deepEqual(
restoreBusinessPlanSnapshot(JSON.stringify(legacySupplierSnapshot)),
supplierTermsPlan,
);
const missingSupplierTermSnapshot = JSON.parse(supplierTermsSnapshot);
delete missingSupplierTermSnapshot.plan.products[0]
.productInventoryPurchasesByPeriod[0].paymentInstallments;
assert.equal(
restoreBusinessPlanSnapshot(
JSON.stringify(missingSupplierTermSnapshot),
),
null,
);
const scheduledInventory = {
...assumptions,
periods: 2,
inventoryPurchaseSchedule: [
{ amount: '125.50', leadMonths: 2 },
{ amount: '25.00', leadMonths: 0 },
],
useInventoryPurchaseSchedule: true,
};
const inventorySnapshot = serializeBusinessPlanSnapshot(scheduledInventory);
assert.deepEqual(
restoreBusinessPlanSnapshot(inventorySnapshot),
scheduledInventory,
);
const invalidInventorySchedule = JSON.parse(inventorySnapshot);
invalidInventorySchedule.plan.inventoryPurchaseSchedule[1].leadMonths = 13;
assert.equal(
restoreBusinessPlanSnapshot(JSON.stringify(invalidInventorySchedule)),
null,
);
assert.equal(
restoreBusinessPlanSnapshot(
JSON.stringify({
version: 9,
plan: {
...assumptions,
products: [
{
...assumptions.products[0],
monthlyUnitsByPeriod: ['100', '50'],
},
],
},
}),
)?.products[0].unitPriceByPeriod,
undefined,
);
assert.equal(
restoreBusinessPlanSnapshot(
JSON.stringify({
version: 8,
plan: { ...assumptions, salesRecoverySchedule: [] },
}),
)?.products[0].monthlyUnitsByPeriod,
undefined,
);
const scheduledSnapshot = serializeBusinessPlanSnapshot({
...assumptions,
periods: 2,
products: [
{
...assumptions.products[0],
monthlyUnitsByPeriod: ['100', '50', '0', '20'],
},
],
});
assert.deepEqual(
restoreBusinessPlanSnapshot(scheduledSnapshot)?.products[0]
.monthlyUnitsByPeriod,
['100', '50', '0', '20'],
);
assert.equal(
restoreBusinessPlanSnapshot(
serializeBusinessPlanSnapshot({
...assumptions,
products: [
{
...assumptions.products[0],
monthlyUnitsByPeriod: ['100'],
},
],
}),
),
null,
);
const legacyV7Plan = { ...assumptions, openingReceivablesInstallments: [] };
delete legacyV7Plan.salesRecoverySchedule;
assert.deepEqual(
restoreBusinessPlanSnapshot(
JSON.stringify({ version: 7, plan: legacyV7Plan }),
)?.salesRecoverySchedule,
[],
);
assert.equal(
restoreBusinessPlanSnapshot(
JSON.stringify({
version: 8,
plan: {
...assumptions,
salesRecoverySchedule: [{ month: 1, percent: '100.01' }],
},
}),
),
null,
);
assert.equal(
restoreBusinessPlanSnapshot(
JSON.stringify({
version: 8,
plan: {
...assumptions,
salesRecoverySchedule: [
{ month: 1, percent: '50' },
{ month: 1, percent: '25' },
],
},
}),
),
null,
);
const legacyV6Plan = {
...assumptions,
openingReceivablesCollectionMonth: 2,
openingReceivablesCollectionPercent: '40',
};
delete legacyV6Plan.openingReceivablesInstallments;
const legacyV6 = restoreBusinessPlanSnapshot(
JSON.stringify({
version: 6,
plan: legacyV6Plan,
}),
);
assert.equal(legacyV6?.openingReceivablesCollectionMonth, 2);
assert.equal(legacyV6?.openingReceivablesCollectionPercent, '40');
assert.deepEqual(legacyV6?.openingReceivablesInstallments, []);
const missingV7Schedule = { ...assumptions };
delete missingV7Schedule.openingReceivablesInstallments;
assert.equal(
restoreBusinessPlanSnapshot(
JSON.stringify({ version: 7, plan: missingV7Schedule }),
),
null,
);
assert.equal(
restoreBusinessPlanSnapshot(
JSON.stringify({
version: 7,
plan: {
...assumptions,
openingReceivablesInstallments: [
{ month: 1, percent: '70' },
{ month: 2, percent: '31' },
],
},
}),
),
null,
);
});
void test('business plan serializer refuses snapshots above the server storage limit', () => {
assert.throws(
() =>
serializeBusinessPlanSnapshot({
...assumptions,
products: [
{
...assumptions.products[0],
label: 'x'.repeat(60_000),
},
],
}),
/excede o limite de armazenamento de 60 KB/,
);
});
void test('sales invoicing and receipts follow an optional declared collection lag', () => {
const delayed = forecastBusinessPlan({
...assumptions,
monthlyCollections: '0',
salesCollectionLagMonths: 1,
periods: 2,
});
assert.deepEqual(
delayed.rows.map((row) => row.salesCollections),
['0.00', '100000.00'],
);
assert.deepEqual(
delayed.rows.map((row) => row.salesReceivablesClosing),
['100000.00', '100000.00'],
);
assert.equal(delayed.totalRevenue, '200000.00');
assert.equal(delayed.totalSalesCollections, '100000.00');
assert.equal(delayed.salesReceivablesAtEnd, '100000.00');
assert.equal(delayed.totalCashCollections, '100000.00');
assert.equal(delayed.totalProfitBeforeTax, '44266.66');
const sameMonth = forecastBusinessPlan({
...assumptions,
monthlyCollections: '0',
salesCollectionLagMonths: 0,
periods: 2,
});
assert.equal(sameMonth.totalSalesCollections, '200000.00');
assert.equal(sameMonth.salesReceivablesAtEnd, '0.00');
assert.equal(sameMonth.totalProfitBeforeTax, delayed.totalProfitBeforeTax);
});
void test('scheduled collection percentage keeps unpaid invoices in receivables with exact rounding', () => {
const partial = forecastBusinessPlan({
...assumptions,
monthlyCollections: '0',
salesCollectionLagMonths: 0,
salesCollectionPercent: '60',
periods: 2,
});
assert.deepEqual(
partial.rows.map((row) => row.salesCollections),
['60000.00', '60000.00'],
);
assert.deepEqual(
partial.rows.map((row) => row.salesReceivablesClosing),
['40000.00', '80000.00'],
);
assert.equal(partial.totalSalesCollections, '120000.00');
assert.equal(partial.salesReceivablesAtEnd, '80000.00');
const halfCent = forecastBusinessPlan({
...assumptions,
products: [
{
label: 'Unitário',
monthlyUnits: '1',
unitPrice: '0.01',
unitVariableCost: '0',
},
],
monthlyCollections: '0',
salesCollectionLagMonths: 0,
salesCollectionPercent: '50',
periods: 1,
});
assert.equal(halfCent.rows[0].salesCollections, '0.01');
assert.equal(halfCent.salesReceivablesAtEnd, '0.00');
assert.throws(
() =>
forecastBusinessPlan({
...assumptions,
salesCollectionLagMonths: 0,
salesCollectionPercent: '100.01',
}),
/entre 0 e 100/,
);
});
void test('sales recovery applies only to overdue invoices and never exceeds open balances', () => {
const recovered = forecastBusinessPlan({
...assumptions,
monthlyCollections: '0',
salesCollectionLagMonths: 0,
salesCollectionPercent: '60',
salesRecoverySchedule: [
{ month: 2, percent: '50' },
{ month: 3, percent: '50' },
],
periods: 3,
});
assert.deepEqual(
recovered.rows.map((row) => row.salesScheduledCollections),
['60000.00', '60000.00', '60000.00'],
);
assert.deepEqual(
recovered.rows.map((row) => row.salesRecoveryCollections),
['0.00', '20000.00', '30000.00'],
);
assert.deepEqual(
recovered.rows.map((row) => row.salesReceivablesClosing),
['40000.00', '60000.00', '70000.00'],
);
assert.equal(recovered.totalScheduledSalesCollections, '180000.00');
assert.equal(recovered.totalSalesRecoveryCollections, '50000.00');
assert.equal(recovered.totalSalesCollections, '230000.00');
assert.equal(recovered.salesReceivablesAtEnd, '70000.00');
const noRecovery = forecastBusinessPlan({
...assumptions,
monthlyCollections: '0',
salesCollectionLagMonths: 0,
salesCollectionPercent: '60',
periods: 2,
});
assert.equal(noRecovery.totalSalesRecoveryCollections, '0.00');
assert.equal(noRecovery.salesReceivablesAtEnd, '80000.00');
assert.throws(
() =>
forecastBusinessPlan({
...assumptions,
salesCollectionLagMonths: 0,
periods: 2,
salesRecoverySchedule: [{ month: 3, percent: '50' }],
}),
/dentro do horizonte/,
);
assert.throws(
() =>
forecastBusinessPlan({
...assumptions,
salesCollectionLagMonths: 0,
salesRecoverySchedule: [{ month: 1, percent: '100.01' }],
}),
/entre 0 e 100/,
);
assert.throws(
() =>
forecastBusinessPlan({
...assumptions,
salesCollectionLagMonths: 0,
salesRecoverySchedule: [
{ month: 1, percent: '50' },
{ month: 1, percent: '50' },
],
}),
/único dentro do horizonte/,
);
});
void test('opening receivables affect cash only when a month and collection percentage are declared', () => {
const unscheduled = forecastBusinessPlan({
...assumptions,
monthlyCollections: '0',
periods: 2,
});
const scheduled = forecastBusinessPlan({
...assumptions,
monthlyCollections: '0',
openingReceivablesCollectionMonth: 2,
openingReceivablesCollectionPercent: '50',
periods: 2,
});
assert.deepEqual(
scheduled.rows.map((row) => row.openingReceivablesCollections),
['0.00', '5000.00'],
);
assert.equal(scheduled.totalOpeningReceivablesCollections, '5000.00');
assert.equal(scheduled.openingReceivablesAtEnd, '5000.00');
assert.equal(scheduled.initialUses, unscheduled.initialUses);
assert.equal(
Number(scheduled.endingCash) - Number(unscheduled.endingCash),
5000,
);
assert.equal(
scheduled.totalProfitBeforeTax,
unscheduled.totalProfitBeforeTax,
);
const halfCent = forecastBusinessPlan({
...assumptions,
receivables: '0.01',
openingReceivablesCollectionMonth: 1,
openingReceivablesCollectionPercent: '50',
periods: 1,
});
assert.equal(halfCent.rows[0].openingReceivablesCollections, '0.01');
assert.equal(halfCent.openingReceivablesAtEnd, '0.00');
assert.throws(
() =>
forecastBusinessPlan({
...assumptions,
openingReceivablesCollectionMonth: 3,
periods: 2,
}),
/dentro do horizonte/,
);
});
void test('opening receivable installments reconcile to the initial balance across months', () => {
const scheduled = forecastBusinessPlan({
...assumptions,
monthlyCollections: '0',
receivables: '10000',
openingReceivablesInstallments: [
{ month: 1, percent: '25' },
{ month: 3, percent: '35' },
],
periods: 3,
});
assert.deepEqual(
scheduled.rows.map((row) => row.openingReceivablesCollections),
['2500.00', '0.00', '3500.00'],
);
assert.deepEqual(
scheduled.rows.map((row) => row.openingReceivablesBalance),
['7500.00', '7500.00', '4000.00'],
);
assert.equal(scheduled.totalOpeningReceivablesCollections, '6000.00');
assert.equal(scheduled.openingReceivablesAtEnd, '4000.00');
assert.deepEqual(scheduled.openingReceivablesInstallments, [
{ month: 1, percent: '25', amount: '2500.00' },
{ month: 3, percent: '35', amount: '3500.00' },
]);
assert.match(
exportBusinessPlanCsv({
...assumptions,
receivables: '10000',
openingReceivablesInstallments: [
{ month: 1, percent: '25' },
{ month: 3, percent: '35' },
],
periods: 3,
}),
/Parcelas adicionais de recebíveis de abertura,"\[.*2500\.00.*3500\.00.*\]"/,
);
const legacyAndInstallments = forecastBusinessPlan({
...assumptions,
receivables: '10000',
openingReceivablesCollectionMonth: 1,
openingReceivablesCollectionPercent: '20',
openingReceivablesInstallments: [{ month: 2, percent: '80' }],
periods: 2,
});
assert.equal(
legacyAndInstallments.totalOpeningReceivablesCollections,
'10000.00',
);
assert.equal(legacyAndInstallments.openingReceivablesAtEnd, '0.00');
assert.throws(
() =>
forecastBusinessPlan({
...assumptions,
openingReceivablesCollectionMonth: 1,
openingReceivablesCollectionPercent: '20',
openingReceivablesInstallments: [{ month: 2, percent: '81' }],
periods: 2,
}),
/não pode exceder 100%/,
);
assert.throws(
() =>
forecastBusinessPlan({
...assumptions,
openingReceivablesInstallments: [{ month: 13, percent: '10' }],
}),
/dentro do horizonte/,
);
});
void test('opening receivable installments cap independently rounded cents at the available balance', () => {
const rounded = forecastBusinessPlan({
...assumptions,
receivables: '0.01',
openingReceivablesInstallments: [
{ month: 1, percent: '50' },
{ month: 2, percent: '50' },
],
periods: 2,
});
assert.deepEqual(
rounded.rows.map((row) => row.openingReceivablesCollections),
['0.01', '0.00'],
);
assert.equal(rounded.openingReceivablesAtEnd, '0.00');
});
void test('inventory purchases, consumption, closing stock and stock shortfall reconcile independently of payments', () => {
const supplied = forecastBusinessPlan({
...assumptions,
inventory: '0',
monthlyInventoryPurchases: '10000',
monthlyVariableCostPayments: '0',
periods: 2,
});
assert.equal(supplied.rows[0].inventoryOpening, '0.00');
assert.equal(supplied.rows[0].inventoryPurchases, '10000.00');
assert.equal(supplied.rows[0].inventoryConsumption, '40000.00');
assert.equal(supplied.rows[0].inventoryShortfall, '30000.00');
assert.equal(supplied.rows[0].inventoryClosing, '0.00');
assert.equal(
supplied.inventoryProjection.status,
'estimated_with_shortfall',
);
assert.equal(supplied.inventoryProjection.totalShortfall, '60000.00');
assert.equal(supplied.totalPayments, '78000.00');
assert.equal(supplied.rows[0].variableCostPayments, '0.00');
});
void test('inventory lead time separates monthly orders, receipts, shortages and goods still in transit', () => {
const result = forecastBusinessPlan({
...assumptions,
inventory: '0',
monthlyInventoryPurchases: '10000',
inventoryPurchaseLeadMonths: 1,
monthlyVariableCostPayments: '0',
periods: 2,
});
const noDelay = forecastBusinessPlan({
...assumptions,
inventory: '0',
monthlyInventoryPurchases: '10000',
inventoryPurchaseLeadMonths: 0,
monthlyVariableCostPayments: '0',
periods: 2,
});
assert.equal(result.rows[0].inventoryPurchases, '10000.00');
assert.equal(result.rows[0].inventoryReceipts, '0.00');
assert.equal(result.rows[0].inventoryShortfall, '40000.00');
assert.equal(result.rows[0].inventoryInTransit, '10000.00');
assert.equal(result.rows[1].inventoryReceipts, '10000.00');
assert.equal(result.rows[1].inventoryInTransit, '10000.00');
assert.equal(result.inventoryProjection.totalPurchases, '20000.00');
assert.equal(result.inventoryProjection.totalReceipts, '10000.00');
assert.equal(result.inventoryProjection.inTransitAtEnd, '10000.00');
assert.equal(result.inventoryProjection.totalShortfall, '70000.00');
assert.equal(result.totalProfitBeforeTax, noDelay.totalProfitBeforeTax);
assert.equal(result.endingCash, noDelay.endingCash);
});
void test('monthly inventory purchase schedule reconciles variable orders, lead times, receipts and transit', () => {
const result = forecastBusinessPlan({
...assumptions,
periods: 4,
products: [
{
label: 'Stock',
monthlyUnits: '0',
unitPrice: '0',
unitVariableCost: '0',
},
],
inventoryPurchaseSchedule: [
{ amount: '10000', leadMonths: 2 },
{ amount: '20000', leadMonths: 0 },
{ amount: '5000', leadMonths: 1 },
{ amount: '0', leadMonths: 0 },
],
useInventoryPurchaseSchedule: true,
});
assert.deepEqual(
result.rows.map((row) => [
row.inventoryPurchases,
row.inventoryLeadMonths,
row.inventoryReceipts,
row.inventoryInTransit,
row.inventoryClosing,
]),
[
['10000.00', 2, '0.00', '10000.00', '20000.00'],
['20000.00', 0, '20000.00', '10000.00', '40000.00'],
['5000.00', 1, '10000.00', '5000.00', '50000.00'],
['0.00', 0, '5000.00', '0.00', '55000.00'],
],
);
assert.equal(result.inventoryProjection.leadMonths, null);
assert.equal(result.inventoryProjection.totalPurchases, '35000.00');
assert.deepEqual(
result.inventoryProjection.purchaseSchedule.map((entry) => [
entry.month,
entry.amount,
entry.leadMonths,
]),
[
[1, '10000.00', 2],
[2, '20000.00', 0],
[3, '5000.00', 1],
[4, '0.00', 0],
],
);
assert.deepEqual(
result.rows.map((row) => row.variableCostPayments),
['42000.00', '42000.00', '42000.00', '42000.00'],
);
assert.throws(
() =>
forecastBusinessPlan({
...assumptions,
periods: 2,
useInventoryPurchaseSchedule: true,
inventoryPurchaseSchedule: [{ amount: '10', leadMonths: 0 }],
}),
/agenda de compras deve cobrir os 2 meses/,
);
assert.throws(
() =>
forecastBusinessPlan({
...assumptions,
useInventoryPurchaseSchedule: true,
inventoryPurchaseSchedule: Array.from(
{ length: assumptions.periods },
() => ({ amount: '10', leadMonths: 13 }),
),
}),
/prazos de entrega entre 0 e 12 meses/,
);
const csv = exportBusinessPlanCsv(
{
...assumptions,
periods: 4,
products: [
{
label: 'Stock',
monthlyUnits: '0',
unitPrice: '0',
unitVariableCost: '0',
},
],
inventoryPurchaseSchedule: [
{ amount: '10000', leadMonths: 2 },
{ amount: '20000', leadMonths: 0 },
{ amount: '5000', leadMonths: 1 },
{ amount: '0', leadMonths: 0 },
],
useInventoryPurchaseSchedule: true,
},
'2026-10-03T10:00:00.000Z',
);
assert.match(csv, /Agenda mensal de compras e recepção/);
assert.match(csv, /Prazo da encomenda \(meses\)/);
});
void test('monthly variable purchase payments affect cash independently from inventory orders and profit', () => {
const base = {
...assumptions,
periods: 3,
monthlyVariableCostPayments: '1000',
monthlyInventoryPurchases: '2000',
};
const scheduled = forecastBusinessPlan({
...base,
monthlyVariableCostPaymentsByPeriod: ['0', '1000', '3000'],
});
const constant = forecastBusinessPlan(base);
assert.deepEqual(
scheduled.rows.map((row) => row.variableCostPayments),
['0.00', '1000.00', '3000.00'],
);
assert.equal(
Number(scheduled.endingCash) - Number(constant.endingCash),
-1000,
);
assert.equal(scheduled.totalProfitBeforeTax, constant.totalProfitBeforeTax);
assert.equal(
scheduled.inventoryProjection.totalPurchases,
constant.inventoryProjection.totalPurchases,
);
});
void test('product inventory schedules reconcile independently and do not transfer stock between products', () => {
const plan = {
...assumptions,
periods: 2,
inventory: '1500',
useInventoryPurchaseSchedule: false,
useProductInventorySchedule: true,
products: [
{
label: 'Produto A',
monthlyUnits: '10',
unitPrice: '20',
unitVariableCost: '10',
productInventoryOpening: '0',
productInventoryPurchasesByPeriod: [
{ amount: '50', leadMonths: 1 },
{ amount: '0', leadMonths: 0 },
],
},
{
label: 'Produto B',
monthlyUnits: '5',
unitPrice: '40',
unitVariableCost: '20',
productInventoryOpening: '1500',
productInventoryPurchasesByPeriod: [
{ amount: '0', leadMonths: 0 },
{ amount: '0', leadMonths: 0 },
],
},
],
};
const result = forecastBusinessPlan(plan);
assert.deepEqual(
result.inventoryProjection.productSchedules.map((product) => [
product.label,
product.totalConsumption,
product.endingInventory,
product.totalShortfall,
]),
[
['Produto A', '200.00', '0.00', '150.00'],
['Produto B', '200.00', '1300.00', '0.00'],
],
);
assert.deepEqual(
result.inventoryProjection.productSchedules[0].monthlyRows.map(
(row) => [row.receipts, row.inTransit, row.shortfall],
),
[
['0.00', '50.00', '100.00'],
['50.00', '0.00', '50.00'],
],
);
assert.equal(result.inventoryProjection.totalShortfall, '150.00');
assert.equal(result.inventoryProjection.totalPurchases, '50.00');
assert.equal(result.rows[0].inventoryLeadMonths, null);
const report = exportBusinessPlanCsv(plan);
assert.match(report, /Existencias por produto/);
assert.match(report, /Produto A,1,0\.00,50\.00,1,0\.00,50\.00/);
assert.throws(
() => forecastBusinessPlan({ ...plan, inventory: '1499' }),
/soma das existencias iniciais por produto/,
);
assert.throws(
() =>
forecastBusinessPlan({
...plan,
useInventoryPurchaseSchedule: true,
inventoryPurchaseSchedule: [
{ amount: '0', leadMonths: 0 },
{ amount: '0', leadMonths: 0 },
],
}),
/compras agregadas ou stock por produto/,
);
});
void test('supplier payment terms drive purchase cash flow and expose amounts due beyond the horizon', () => {
const plan = {
...assumptions,
periods: 2,
inventory: '1000',
monthlyVariableCostPayments: '999',
monthlyVariableCostPaymentsByPeriod: ['999', '999'],
useProductInventorySchedule: true,
useSupplierPaymentTerms: true,
products: [
{
...assumptions.products[0],
monthlyUnits: '0',
productInventoryOpening: '1000',
productInventoryPurchasesByPeriod: [
{
amount: '100',
leadMonths: 0,
paymentLagMonths: 1,
},
{
amount: '200',
leadMonths: 0,
paymentLagMonths: 2,
},
],
},
],
};
const result = forecastBusinessPlan(plan);
assert.deepEqual(
result.rows.map((row) => row.variableCostPayments),
['0.00', '100.00'],
);
assert.equal(result.supplierPaymentsOutstandingAtEnd, '200.00');
assert.deepEqual(
result.inventoryProjection.productSchedules[0].paymentSchedule.map(
(payment) => [
payment.orderMonth,
payment.paymentMonth,
payment.amount,
payment.paidWithinHorizon,
],
),
[
[1, 2, '100.00', true],
[2, 4, '200.00', false],
],
);
const report = exportBusinessPlanCsv(plan);
assert.match(report, /Pagamentos a fornecedores por prazo declarado/);
assert.match(report, /,2,4,100,200\.00,por pagar/);
assert.throws(
() =>
forecastBusinessPlan({
...plan,
useProductInventorySchedule: false,
}),
/exigem compras planeadas por produto/,
);
assert.throws(
() =>
forecastBusinessPlan({
...plan,
products: [
{
...plan.products[0],
productInventoryPurchasesByPeriod: [
{
amount: '0',
leadMonths: 0,
paymentLagMonths: 13,
},
{ amount: '0', leadMonths: 0, paymentLagMonths: 0 },
],
},
],
}),
/declare encomendas e prazos de entrega/,
);
});
void test('supplier order installments reconcile exact cents, cash dates and unallocated balances', () => {
const plan = {
...assumptions,
periods: 2,
inventory: '1010.01',
useProductInventorySchedule: true,
useSupplierPaymentTerms: true,
products: [
{
...assumptions.products[0],
monthlyUnits: '0',
productInventoryOpening: '1010.01',
productInventoryPurchasesByPeriod: [
{
amount: '10.01',
leadMonths: 0,
paymentInstallments: [
{ lagMonths: 0, percent: '33.333333' },
{ lagMonths: 2, percent: '33.333333' },
],
},
{
amount: '200',
leadMonths: 0,
paymentInstallments: [{ lagMonths: 2, percent: '100' }],
},
],
},
],
};
const result = forecastBusinessPlan(plan);
assert.deepEqual(
result.rows.map((row) => row.variableCostPayments),
['3.34', '0.00'],
);
assert.equal(result.supplierPaymentsOutstandingAtEnd, '206.67');
assert.deepEqual(
result.inventoryProjection.productSchedules[0].paymentSchedule.map(
(payment) => [
payment.orderMonth,
payment.paymentMonth,
payment.amount,
payment.paidWithinHorizon,
],
),
[
[1, 1, '3.34', true],
[1, 3, '3.34', false],
[1, null, '3.33', false],
[2, 4, '200.00', false],
],
);
const malformedPlan = structuredClone(plan);
malformedPlan.products[0].productInventoryPurchasesByPeriod[0].paymentInstallments =
[
{ lagMonths: 0, percent: '60' },
{ lagMonths: 1, percent: '50' },
];
assert.throws(
() => forecastBusinessPlan(malformedPlan),
/declare encomendas/,
);
const snapshot = JSON.parse(serializeBusinessPlanSnapshot(plan));
snapshot.plan.products[0].productInventoryPurchasesByPeriod[0].paymentInstallments[1].lagMonths = 13;
assert.equal(restoreBusinessPlanSnapshot(JSON.stringify(snapshot)), null);
});
void test('business assumptions survive the existing encrypted-draft snapshot split and restore', () => {
const encoded = serializeBusinessPlanSnapshot(assumptions);
const snapshots = partitionSimulationDraft({
purpose: 'own_investment',
business_plan_snapshot: encoded,
});
const restoredDraft = restoreSimulationDraft(
snapshots.economic_snapshot,
snapshots.personal_snapshot,
);
assert.deepEqual(
restoreBusinessPlanSnapshot(
String(restoredDraft.business_plan_snapshot),
),
assumptions,
);
});
void test('presentation currency conversion requires and reports its rate, date, and declared source', () => {
const result = forecastBusinessPlan({
...assumptions,
presentationCurrency: 'USD',
fxRate: '0.01',
fxDate: '2026-10-02',
fxSource: 'Taxa introduzida pelo utilizador',
openingReceivablesInstallments: [{ month: 1, percent: '40' }],
});
assert.equal(result.currency, 'MZN');
assert.equal(result.presentationCurrency, 'USD');
assert.equal(result.conversion.totalRevenue, '12000.00');
assert.equal(result.conversion.endingCash, '1170.00');
assert.equal(result.conversion.fundingStatus, 'surplus');
assert.equal(result.conversion.rows.at(-1).closingCash, '1170.00');
assert.equal(
result.conversion.openingReceivablesInstallments[0].amount,
'40.00',
);
assert.throws(
() =>
forecastBusinessPlan({
...assumptions,
presentationCurrency: 'USD',
}),
/taxa, data efectiva e fonte/,
);
assert.throws(
() =>
forecastBusinessPlan({
...assumptions,
presentationCurrency: 'USD',
fxRate: '0',
fxDate: '2026-10-02',
fxSource: 'Fonte',
}),
/superior a zero/,
);
assert.throws(
() =>
forecastBusinessPlan({
...assumptions,
presentationCurrency: 'USD',
fxRate: '1',
fxDate: '2026-02-30',
fxSource: 'Fonte',
}),
/data de câmbio válida/,
);
});
void test('CSV marks presentation currency conversion as an unverified user planning assumption', () => {
const csv = exportBusinessPlanCsv(
{
...assumptions,
presentationCurrency: 'USD',
fxRate: '0.01',
fxDate: '2026-10-02',
fxSource: 'Cotacao informada pelo investidor',
},
'2026-10-04T00:00:00.000Z',
);
assert.match(csv, /Estado da conversao cambial/);
assert.match(
csv,
/pressuposto de planeamento declarado pelo utilizador; nao verificado pelo portal/,
);
});
void test('business plan export carries its assumptions, formulas, period, currency, and matching result', () => {
const csv = exportBusinessPlanCsv(
{ ...assumptions, periods: 1 },
'2026-10-02T10:00:00.000Z',
);
assert.match(csv, /^\uFEFF/);
assert.match(csv, /business-plan-v17/);
assert.match(
csv,
/Valores do formulário; os valores iniciais podem ser exemplos demonstrativos/,
);
assert.match(csv, /Ponte contabilístico-fiscal,incomplete/);
assert.match(csv, /Matéria colectável,indeterminate/);
assert.match(csv, /expense_deductibility_classification/);
assert.match(csv, /2026-10-02T10:00:00\.000Z/);
assert.match(csv, /Moeda de cálculo,MZN/);
assert.match(csv, /Resultado antes de impostos/);
assert.match(csv, /não calculados nem incluídos/);
assert.match(csv, /Caixa final projectada,14000\.00/);
assert.match(csv, /Projecção de existências,estimated/);
assert.match(csv, /Compras mensais para existências,40000/);
assert.match(csv, /Prazo de entrega das compras,0 meses/);
assert.match(csv, /Recepções de compras no horizonte,40000\.00/);
assert.match(csv, /Existências em trânsito no fim do horizonte,0\.00/);
});
void test('business plan CSV preserves declared monthly product schedules', () => {
const csv = exportBusinessPlanCsv(
{
...assumptions,
periods: 3,
products: [
{
...assumptions.products[0],
monthlyUnitsByPeriod: ['10', '5.5', '0', '9'],
unitPriceByPeriod: ['100', '125', '90', '80'],
unitVariableCostByPeriod: ['40', '55', '30', '20'],
},
],
},
'2026-10-03T10:00:00.000Z',
);
assert.ok(csv.includes('"[""10"",""5.5"",""0"",""9""]"'));
assert.ok(csv.includes('"[""100"",""125"",""90"",""80""]"'));
assert.ok(csv.includes('"[""40"",""55"",""30"",""20""]"'));
});
void test('CSV export exposes the sales collection assumption and outstanding receivable', () => {
const csv = exportBusinessPlanCsv(
{
...assumptions,
monthlyCollections: '0',
salesCollectionLagMonths: 1,
openingReceivablesCollectionMonth: 2,
openingReceivablesCollectionPercent: '25',
periods: 2,
},
'2026-10-02T10:00:00.000Z',
);
assert.match(csv, /Prazo de cobrança das vendas,1 meses/);
assert.match(csv, /Cobranças da facturação modelada,100000\.00/);
assert.match(csv, /Facturação por cobrar no fim,100000\.00/);
assert.match(csv, /Cobrança de valores a receber de abertura,2500\.00/);
assert.match(csv, /Valores de abertura por receber no fim,7500\.00/);
assert.match(
csv,
/Mês,Facturação,Cobranças,Cobranças de vendas,Cobranças iniciais das vendas,Recuperações de vendas vencidas,Cobrança de valores de abertura,Valores de abertura por receber,Pagamento de valores a pagar de abertura,Saldo de valores a pagar de abertura,Outras cobranças/,
);
});
void test('business plan CSV neutralizes formula-like labels and preserves negative financial values', () => {
const csv = exportBusinessPlanCsv({
...assumptions,
products: [
{
...assumptions.products[0],
label: '=HYPERLINK("https://example.invalid")',
},
],
periods: 1,
});
assert.match(csv, /"'=HYPERLINK\(""https:\/\/example\.invalid""\)"/);
assert.match(csv, /Estado da reconciliação do financiamento,surplus/);
assert.match(csv, /Diferença entre usos e fontes,-5000\.00/);
});
void test('financial scenarios expose user-declared changes without creating tax values', () => {
const scenarioPlan = {
...assumptions,
periods: 1,
monthlyCollections: '0',
salesCollectionLagMonths: 0,
salesCollectionPercent: '80',
scenarioAdjustments: {
favorable: {
unitVolumePercent: '20',
unitPricePercent: '10',
unitCostPercent: '-5',
operatingCostsPercent: '-10',
cashCollectionsPercent: '10',
},
unfavorable: {
unitVolumePercent: '-20',
unitPricePercent: '-10',
unitCostPercent: '15',
operatingCostsPercent: '20',
cashCollectionsPercent: '-20',
},
},
};
const result = compareBusinessScenarios(scenarioPlan);
assert.equal(result.base.totalRevenue, '100000.00');
assert.equal(result.favorable.result.totalRevenue, '132000.00');
assert.equal(result.favorable.result.rows[0].variableCost, '45600.00');
const scheduledPaymentScenarios = compareBusinessScenarios({
...scenarioPlan,
monthlyVariableCostPaymentsByPeriod: ['10000'],
});
assert.equal(
scheduledPaymentScenarios.favorable.result.rows[0].variableCostPayments,
'11400.00',
);
assert.equal(
scheduledPaymentScenarios.unfavorable.result.rows[0]
.variableCostPayments,
'9200.00',
);
assert.equal(result.unfavorable.result.totalRevenue, '72000.00');
assert.equal(result.favorable.result.totalSalesCollections, '116160.00');
assert.equal(result.unfavorable.result.totalSalesCollections, '46080.00');
assert.notEqual(
result.favorable.result.endingCash,
result.unfavorable.result.endingCash,
);
assert.deepEqual(result.changedAssumptions, [
'unidades vendidas',
'preço unitário',
'custo variável unitário',
'compras para existências',
'despesas e pessoal',
'cobranças',
]);
const receivableScenarios = compareBusinessScenarios({
...scenarioPlan,
receivables: '10000',
openingReceivablesCollectionMonth: 1,
openingReceivablesCollectionPercent: '40',
openingReceivablesInstallments: [{ month: 1, percent: '20' }],
});
assert.equal(
receivableScenarios.favorable.result.totalOpeningReceivablesCollections,
'6600.00',
);
assert.equal(
receivableScenarios.unfavorable.result
.totalOpeningReceivablesCollections,
'4800.00',
);
const inventoryScenarios = compareBusinessScenarios({
...scenarioPlan,
periods: 2,
inventoryPurchaseSchedule: [
{ amount: '10000', leadMonths: 1 },
{ amount: '5000', leadMonths: 2 },
],
useInventoryPurchaseSchedule: true,
});
assert.equal(
inventoryScenarios.base.inventoryProjection.totalPurchases,
'15000.00',
);
assert.equal(
inventoryScenarios.favorable.result.inventoryProjection.totalPurchases,
'17100.00',
);
assert.equal(
inventoryScenarios.unfavorable.result.inventoryProjection
.totalPurchases,
'13800.00',
);
const productInventoryScenarios = compareBusinessScenarios({
...scenarioPlan,
inventory: '20000',
useProductInventorySchedule: true,
products: [
{
...scenarioPlan.products[0],
productInventoryOpening: '20000',
productInventoryPurchasesByPeriod: [
{ amount: '10000', leadMonths: 1 },
],
},
],
});
assert.equal(
productInventoryScenarios.favorable.result.inventoryProjection
.productSchedules[0].purchaseSchedule[0].amount,
'11400.00',
);
assert.equal(
productInventoryScenarios.unfavorable.result.inventoryProjection
.productSchedules[0].purchaseSchedule[0].amount,
'9200.00',
);
const inactiveInventorySchedule = compareBusinessScenarios({
...scenarioPlan,
inventoryPurchaseSchedule: [{ amount: '', leadMonths: 0 }],
useInventoryPurchaseSchedule: false,
});
assert.equal(
inactiveInventorySchedule.base.inventoryProjection.totalPurchases,
'40000.00',
);
const report = exportBusinessPlanCsv(
scenarioPlan,
'2026-10-02T10:00:00.000Z',
);
assert.match(report, /Comparação de cenários/);
assert.match(report, /Existências finais/);
assert.match(report, /Falta de stock/);
assert.match(report, /business-plan-v17/);
assert.match(report, /Favorável/);
assert.match(report, /Desfavorável/);
assert.throws(
() =>
exportBusinessPlanCsv({
...assumptions,
scenarioAdjustments: {
favorable: {
unitVolumePercent: '10',
unitPricePercent: '',
unitCostPercent: '',
operatingCostsPercent: '',
cashCollectionsPercent: '',
},
unfavorable: {
unitVolumePercent: '',
unitPricePercent: '',
unitCostPercent: '',
operatingCostsPercent: '',
cashCollectionsPercent: '',
},
},
}),
/Complete ou limpe/,
);
assert.throws(
() => compareBusinessScenarios(assumptions),
/Indique os pressupostos/,
);
assert.throws(
() =>
compareBusinessScenarios({
...assumptions,
scenarioAdjustments: {
favorable: {
unitVolumePercent: '-101',
unitPricePercent: '0',
unitCostPercent: '0',
operatingCostsPercent: '0',
cashCollectionsPercent: '0',
},
unfavorable: {
unitVolumePercent: '0',
unitPricePercent: '0',
unitCostPercent: '0',
operatingCostsPercent: '0',
cashCollectionsPercent: '0',
},
},
}),
/redução não pode exceder/,
);
});