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

339 lines
12 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import {
buildLoanSchedule,
calculateBreakEven,
calculateBudget,
calculateFinancialTools,
calculateFundingGap,
calculateLoanPayment,
exportFinancialToolsCsv,
restoreFinancialToolsSnapshot,
serializeFinancialToolsSnapshot,
} from '../../resources/js/lib/financial-calculators.ts';
function cents(value) {
const [whole, fraction = ''] = value.split('.');
return BigInt(whole) * 100n + BigInt(fraction.padEnd(2, '0'));
}
void test('financial tools snapshot persists editable calculations and rejects malformed data', () => {
const data = {
budgetPeriod: 'Mensal',
loan: {
principal: '100000',
rate: '18',
months: '24',
rateType: 'nominal',
periodMonths: '1',
gracePeriods: '0',
upfrontFee: '0',
recurringFee: '0',
},
breakEven: { fixed: '50000', price: '1000', variable: '600' },
funding: { total: '1000000', secured: '250000' },
incomes: [{ label: 'Vendas', amount: '200000' }],
expenses: [{ label: 'Renda', amount: '15000' }],
};
const snapshot = serializeFinancialToolsSnapshot(data);
assert.deepEqual(restoreFinancialToolsSnapshot(snapshot), data);
assert.equal(restoreFinancialToolsSnapshot('{"version":2}'), null);
assert.equal(restoreFinancialToolsSnapshot('x'.repeat(30001)), null);
assert.equal(
restoreFinancialToolsSnapshot(
serializeFinancialToolsSnapshot({ ...data, incomes: [] }),
),
null,
);
assert.equal(
restoreFinancialToolsSnapshot(
serializeFinancialToolsSnapshot({
...data,
incomes: Array.from({ length: 51 }, () => ({
label: 'x',
amount: '1',
})),
}),
),
null,
);
});
void test('loan payment handles zero interest exactly', () => {
assert.deepEqual(calculateLoanPayment('1200.00', '0', 12), {
monthlyPayment: '100.00',
totalPaid: '1200.00',
status: 'estimated',
});
});
void test('loan payment computes a reproducible amortized estimate', () => {
assert.deepEqual(calculateLoanPayment('1000.00', '12', 12), {
monthlyPayment: '88.85',
totalPaid: '1066.19',
status: 'estimated',
});
});
void test('break-even rounds required units upward and rejects nonpositive margin', () => {
assert.deepEqual(calculateBreakEven('100.00', '30.00', '20.00'), {
contributionPerUnit: '10.00',
breakEvenUnits: '10',
breakEvenRevenue: '300.00',
status: 'estimated',
});
assert.throws(() => calculateBreakEven('10', '20', '20'), /superior/);
});
void test('an invalid indicator does not hide independent financial calculations', () => {
const result = calculateFinancialTools({
loan: {
principal: '1200',
annualRatePercent: '0',
termMonths: 12,
periodMonths: 1,
rateType: 'nominal',
},
breakEven: {
fixedCosts: '100',
unitPrice: '20',
variableCostPerUnit: '20',
},
funding: { totalInvestment: '5000', securedFunding: '1000' },
incomes: ['500'],
expenses: ['600'],
});
assert.equal(result.loan.value?.schedule[0].totalPayment, '100.00');
assert.match(result.breakEven.error ?? '', /superior/);
assert.equal(result.funding.value?.gap, '4000.00');
assert.equal(result.budget.value?.operatingBalance, '-100.00');
});
void test('funding gap distinguishes zero from a negative surplus', () => {
assert.deepEqual(calculateFundingGap('100.00', '120.00'), {
gap: '-20.00',
reconciles: false,
status: 'estimated',
});
});
void test('invalid precision and negative inputs are rejected', () => {
assert.throws(() => calculateFundingGap('-1', '0'), /decimal/);
assert.throws(() => calculateLoanPayment('1.001', '0', 1), /casas/);
});
void test('loan schedule supports nominal rates, interest-only grace and separate fees', () => {
const result = buildLoanSchedule({
principal: '1200.00',
annualRatePercent: '12',
termMonths: 4,
periodMonths: 1,
rateType: 'nominal',
gracePeriods: 2,
upfrontFee: '10',
recurringFee: '2',
});
assert.equal(result.periodicRatePercent, '1.000000');
assert.equal(result.netDisbursed, '1190.00');
assert.equal(result.schedule[0].interest, '12.00');
assert.equal(result.schedule[0].principal, '0.00');
assert.equal(result.schedule[0].fee, '2.00');
assert.equal(result.schedule.at(-1).closingBalance, '0.00');
assert.equal(
result.schedule.reduce((sum, row) => sum + cents(row.principal), 0n),
120000n,
);
assert.equal(
result.schedule.reduce(
(sum, row) => sum + cents(row.totalPayment),
0n,
) + cents(result.upfrontFee),
cents(result.totalPaid),
);
for (const row of result.schedule) {
assert.equal(
cents(row.openingBalance) - cents(row.principal),
cents(row.closingBalance),
);
}
});
void test('loan schedule reconciles exact cents and adjusts the final installment', () => {
const result = buildLoanSchedule({
principal: '1000.01',
annualRatePercent: '7.123456',
termMonths: 7,
periodMonths: 1,
rateType: 'nominal',
});
assert.equal(result.periodicRatePercent, '0.593621');
assert.equal(result.totalInterest, '23.88');
assert.equal(result.totalPaid, '1023.89');
assert.equal(result.schedule.at(-1).closingBalance, '0.00');
assert.equal(
result.schedule.reduce((sum, row) => sum + cents(row.principal), 0n),
100001n,
);
assert.equal(
result.schedule.reduce((sum, row) => sum + cents(row.totalPayment), 0n),
cents(result.totalPaid),
);
});
void test('effective annual rate is converted to the selected repayment period', () => {
const result = buildLoanSchedule({
principal: '1000',
annualRatePercent: '12',
termMonths: 12,
periodMonths: 12,
rateType: 'effective',
});
assert.equal(result.periodicRatePercent, '12.000000');
assert.equal(result.schedule[0].interest, '120.00');
assert.equal(result.totalPaid, '1120.00');
const quarterly = buildLoanSchedule({
principal: '1000',
annualRatePercent: '12',
termMonths: 12,
periodMonths: 3,
rateType: 'effective',
});
assert.equal(quarterly.periodicRatePercent, '2.873734');
assert.equal(quarterly.schedule.at(-1).closingBalance, '0.00');
});
void test('loan schedules reconcile across rates, periodicities and grace periods', () => {
for (const periodMonths of [1, 2, 3, 4, 6, 12]) {
for (const rateType of ['nominal', 'effective']) {
for (const annualRatePercent of ['0', '8.500000', '24']) {
for (const gracePeriods of [0, 2]) {
const result = buildLoanSchedule({
principal: '12345.67',
annualRatePercent,
termMonths: periodMonths * 12,
periodMonths,
rateType,
gracePeriods,
upfrontFee: '15.25',
recurringFee: '2.37',
});
assert.equal(result.schedule.length, 12);
assert.equal(result.schedule.at(-1).closingBalance, '0.00');
assert.equal(
result.schedule.reduce(
(sum, row) => sum + cents(row.principal),
0n,
),
1_234_567n,
);
for (const row of result.schedule) {
assert.equal(
cents(row.openingBalance) - cents(row.principal),
cents(row.closingBalance),
);
assert.ok(cents(row.closingBalance) >= 0n);
}
assert.equal(
result.schedule.reduce(
(sum, row) => sum + cents(row.totalPayment),
0n,
) + cents(result.upfrontFee),
cents(result.totalPaid),
);
assert.equal(
cents(result.totalPaid),
1_234_567n +
cents(result.totalInterest) +
cents(result.totalFees),
);
}
}
}
}
});
void test('budget preserves an operating deficit and rejects invalid values', () => {
assert.deepEqual(calculateBudget(['100.00', '25.50'], ['140.00']), {
incomeTotal: '125.50',
expenseTotal: '140.00',
operatingBalance: '-14.50',
status: 'estimated',
});
assert.throws(() => calculateBudget(['0'], ['-1']), /não negativo/);
});
void test('budget distinguishes absent income or expense rows from explicit zero', () => {
assert.throws(() => calculateBudget([], ['0']), /pelo menos uma linha/);
assert.throws(() => calculateBudget(['0'], []), /pelo menos uma linha/);
assert.deepEqual(calculateBudget(['0'], ['0']), {
incomeTotal: '0.00',
expenseTotal: '0.00',
operatingBalance: '0.00',
status: 'estimated',
});
});
void test('loan terms reject invalid periodicity, excessive grace and commission larger than principal', () => {
const base = {
principal: '1000',
annualRatePercent: '0',
termMonths: 12,
periodMonths: 1,
rateType: 'nominal',
};
assert.throws(
() => buildLoanSchedule({ ...base, periodMonths: 5 }),
/Periodicidade/,
);
assert.throws(
() => buildLoanSchedule({ ...base, gracePeriods: 12 }),
/carência/,
);
assert.throws(
() => buildLoanSchedule({ ...base, upfrontFee: '1001' }),
/exceder o capital/,
);
});
void test('financial tools CSV records reproducible assumptions, periods, formulas and matching results', () => {
const csv = exportFinancialToolsCsv({
currency: 'MZN',
generatedAt: '2026-10-02T10:00:00.000Z',
budgetPeriod: 'Mensal',
loan: {
principal: '1200',
annualRatePercent: '12',
termMonths: 12,
periodMonths: 1,
rateType: 'nominal',
gracePeriods: 0,
upfrontFee: '10',
recurringFee: '2',
},
breakEven: {
fixedCosts: '1000',
unitPrice: '30',
variableCostPerUnit: '20',
},
funding: { totalInvestment: '5000', securedFunding: '3000' },
incomes: [
{ label: '=HYPERLINK("https://example.invalid")', amount: '2500' },
],
expenses: [{ label: 'Operação, mensal', amount: '1800' }],
});
assert.match(csv, /^\uFEFF/);
assert.match(csv, /financial-tools-v1/);
assert.match(csv, /valores iniciais sao exemplos demonstrativos/);
assert.match(csv, /2026-10-02T10:00:00\.000Z/);
assert.match(csv, /Período do orçamento e dos custos fixos,Mensal/);
assert.match(csv, /saldo decrescente/i);
assert.match(csv, /Saldo operacional,,700\.00/);
assert.match(csv, /Unidades de equilíbrio,100/);
assert.match(csv, /Diferença por financiar,2000\.00/);
assert.match(csv, /"'=HYPERLINK/);
assert.match(csv, /"Operação, mensal"/);
assert.match(csv, /não inclui impostos/);
});