Files
Investidor-V2/tests/frontend/irpc-normal-period-fx.test.mjs
T

340 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {
normalBook,
normalCurrent,
calculateNormal,
recordNormal,
saveNormalBook,
normalHtml,
normalFinancialSource,
} from '../../resources/js/lib/irpc-normal.ts';
import { newNormalPeriodFx } from '../../resources/js/lib/irpc-normal-period-fx.ts';
import {
emptyFxSelection,
selectFx,
} from '../../resources/js/lib/iva-fx-selection.ts';
import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts';
import { newCurrencyEvent } from '../../resources/js/lib/iva-currency-events.ts';
import { irpcFxSources } from '../../resources/js/lib/irpc-fx.ts';
const seed = () =>
JSON.parse(
fs.readFileSync(
new URL(
'./fixtures/irpc-depreciation-preserved.json',
import.meta.url,
),
'utf8',
),
),
val = (f) => (f ? Number(f.numerator) / Number(f.denominator) : null);
function fx(direction = 'mzn_per_unit', rate = '64') {
const s = {
...emptyFxSelection(),
nature: 'commercial',
submissionDate: '2026-10-26',
bulletinDate: '2026-10-26',
bulletinRef: 'QA pressuposto sintético64; não boletim real',
source: 'https://www.bancomoc.mz/',
sourceCurrency: 'USD',
direction,
mean: rate,
};
return { ...s, applied: selectFx(s) };
}
function setup(currency = 'MZN', kind = 'start') {
const d = seed(),
b = normalBook(d),
p = JSON.parse(d.business_plan_snapshot);
p.plan.currency = currency;
d.business_plan_snapshot = JSON.stringify(p);
b.rows = [];
b.costs = undefined;
b.schedules.rows = [];
b.schedules.interest.amount = currency === 'USD' ? '7680' : '120';
for (const t of Object.values(b.profit.topics)) {
t.state = 'absent';
t.amount = '';
t.reference = 'QA ausência declarada no período seleccionado';
}
b.base.losses = [];
b.base.lossesState = 'absent';
b.base.lossesReference = 'QA ausência de perdas reportadas';
b.depreciation.assets[0].accounting = currency === 'USD' ? '3840' : '60';
b.depreciation.assets[0].cost = currency === 'USD' ? '32000' : '500';
b.depreciation.assets[0].start = '2026-07-01';
b.depreciation.assets[0].timing = 'months';
b.periodsFx = {
...newNormalPeriodFx(),
kind,
from: '2026-07-01',
to: '2026-12-31',
planStart: '2026-01-01',
reference:
'QA início documentado1Julho;correspondência completa dos meses7–12 ao período do sujeito',
mznMapsReference:
'QA activo/quotas/juros e demais mapas reconciliados explicitamente emMZN no período',
conversionReference:
currency === 'USD'
? 'QA pressuposto de simulação64MZN/USD aplicado aos meses seleccionados;sem regra fiscal anual confirmada'
: '',
};
if (currency === 'USD')
b.periodsFx.months = Array.from({ length: 6 }, (_, n) => ({
period: n + 7,
currency: 'USD',
fxSelection: fx(),
}));
return { d, b };
}
void test('installed7 stays current without opt-in; optional8 preserves original result and plan while selecting6 months', () => {
const d = seed(),
old = normalBook(d);
assert.equal(normalCurrent(d, old), true);
const { d: next, b } = setup(),
m = calculateNormal(next, b);
assert.equal(m.version, 'IRPC-normal-adjustments/8');
assert.equal(val(m.accountingProfit), 3120);
assert.equal(val(m.depreciation.assets[0].quota), 50);
assert.equal(val(m.taxableProfit), 3130);
assert.equal(val(m.taxableBase), 3130);
assert.equal(val(m.colecta), 1001.6);
assert.equal(m.assessment.period.from, '2026-07-01');
assert.equal(m.assessment.finalBalance.payable, null);
assert.equal(JSON.parse(next.business_plan_snapshot).plan.periods, 12);
});
void test('monthly conversion reuses delivered selection and direction; originalUSD and assumptions remain reportable', () => {
const { d, b } = setup('USD'),
m = calculateNormal(d, b);
assert.equal(val(m.accountingProfit), 199680);
assert.equal(val(m.depreciation.adjustment), 640);
assert.equal(val(m.taxableProfit), 200320);
assert.equal(val(m.colecta), 64102.4);
assert.equal(m.periodsFx.originalCurrency, 'USD');
assert.equal(m.periodsFx.rows[0].evidence.column, 'mean');
assert.match(m.periodsFx.rule, /simulação/);
const inv = setup('USD');
for (const e of inv.b.periodsFx.months)
e.fxSelection = fx('units_per_mzn', '0.015625');
assert.equal(val(calculateNormal(inv.d, inv.b).colecta), 64102.4);
});
void test('annual plan in another currency and shorterplan aligned to July both reconcile without annualising', () => {
const a = setup('USD');
a.b.periodsFx.kind = 'annual';
a.b.periodsFx.from = '2026-01-01';
a.b.periodsFx.months = Array.from({ length: 12 }, (_, n) => ({
period: n + 1,
currency: 'USD',
fxSelection: fx(),
}));
a.b.schedules.interest.amount = '15360';
a.b.depreciation.assets[0].accounting = '7680';
a.b.depreciation.assets[0].start = '2026-01-01';
assert.equal(val(calculateNormal(a.d, a.b).colecta), 128204.8);
const { d, b } = setup();
const p = JSON.parse(d.business_plan_snapshot);
p.plan.periods = 6;
p.plan.assetDepreciation = '60';
p.plan.depreciationLifeMonths = 6;
d.business_plan_snapshot = JSON.stringify(p);
b.periodsFx.planStart = '2026-07-01';
assert.equal(val(calculateNormal(d, b).colecta), 1001.6);
});
void test('cessation and same-year start/end use documented dates; unsupportedpartial months and contradictions remain pending', () => {
const { d, b } = setup();
b.periodsFx.kind = 'cessation';
b.periodsFx.from = '2026-01-01';
b.periodsFx.to = '2026-06-30';
b.depreciation.assets[0].start = '2026-01-01';
assert.equal(val(calculateNormal(d, b).colecta), 1001.6);
b.periodsFx.kind = 'same_year';
b.periodsFx.from = '2026-04-01';
b.periodsFx.to = '2026-09-30';
assert.equal(val(calculateNormal(d, b).colecta), 1001.6);
for (const patch of [
{ from: '2026-07-15' },
{ to: '2026-12-30' },
{ planStart: '' },
{ from: '2026-01-01', kind: 'start' },
{ to: '2026-02-31' },
{ reference: '' },
{ mznMapsReference: '' },
]) {
const x = setup();
Object.assign(x.b.periodsFx, patch);
assert.equal(calculateNormal(x.d, x.b).colecta, null);
}
});
void test('missingmonth selection,sourcecurrency mismatch and editedapplied rate never become defaultFX', () => {
for (const mutate of [
(i) => i.months.pop(),
(i) => (i.months[0].currency = 'EUR'),
(i) => (i.months[0].fxSelection.mean = '65'),
(i) => (i.conversionReference = ''),
(i) => i.months.push({ ...i.months[0], period: 1 }),
]) {
const { d, b } = setup('USD');
mutate(b.periodsFx);
const m = calculateNormal(d, b);
assert.equal(m.accountingProfit, null);
assert.equal(m.colecta, null);
assert.ok(m.periodsFx.rows.some((r) => r.value));
}
});
void test('fractionalcent conversion blocks dependenttotals while retaining exact source and otherknownmonths', () => {
const { d, b } = setup('USD');
b.periodsFx.months[0].fxSelection = fx('mzn_per_unit', '0.33333333');
const m = calculateNormal(d, b);
assert.equal(m.colecta, null);
assert.ok(
m.periodsFx.pending.some((s) => s.includes('fracção de cêntimo')),
);
assert.equal(m.periodsFx.rows[0].originalCurrency, 'USD');
});
void test('shortperiod priorloss expiry and annualcaps stay explicitlypending; no change to independent arithmetic', () => {
const x = setup();
x.b.base = structuredClone(normalBook(seed()).base);
const m = calculateNormal(x.d, x.b);
assert.equal(val(m.taxableProfit), 3130);
assert.equal(m.taxableBase, null);
assert.ok(m.base.pending.some((s) => s.includes('41(5)')));
const y = setup();
y.b.costs = structuredClone(normalBook(seed()).costs);
assert.equal(calculateNormal(y.d, y.b).colecta, null);
assert.ok(
calculateNormal(y.d, y.b).costs.pending.some((s) =>
s.includes('Limite anual neste período curto'),
),
);
y.b.costs = undefined;
y.b.depreciation.assets[0].timing = 'annual';
assert.equal(calculateNormal(y.d, y.b).colecta, null);
});
void test('save/recovery/report preserves7 history and sourceplan; changed period makes8 historical', () => {
const { d, b } = setup('USD'),
original = d.business_plan_snapshot,
old = structuredClone(b.result);
d.modelo_a_snapshot = saveNormalBook(
d,
recordNormal(d, b, '2026-10-26T10:00:00+02:00'),
);
const back = normalBook(JSON.parse(JSON.stringify(d)));
assert.equal(normalCurrent(d, back), true);
assert.deepEqual(back.history.at(-1), old);
assert.equal(d.business_plan_snapshot, original);
assert.match(normalHtml(d), /64102.40/);
assert.match(normalHtml(d), /regra fiscal anual não confirmada/);
assert.match(
JSON.stringify(
createProjectDossierReport(d, {
selectedSections: new Set(['finance']),
}),
),
/USD/,
);
back.periodsFx.reference += 'changed';
assert.equal(normalCurrent(d, back), false);
});
void test('newcases prepare privateAPI persistence and dossiers including insufficientinformation', () => {
const cases = {
short: setup(),
fx: setup('USD'),
missing: setup('USD'),
loss: setup(),
};
cases.missing.b.periodsFx.months.pop();
cases.loss.b.base = structuredClone(normalBook(seed()).base);
fs.mkdirSync('outputs/irpc-l6-periods-fx', { recursive: true });
for (const [name, { d, b }] of Object.entries(cases)) {
d.modelo_a_snapshot = saveNormalBook(
d,
recordNormal(d, b, '2026-10-26T10:00:00+02:00'),
);
fs.writeFileSync(
'outputs/irpc-l6-periods-fx/' + name + '.json',
JSON.stringify(d),
);
fs.writeFileSync(
'outputs/irpc-l6-periods-fx/' + name + '.html',
normalHtml(d),
);
}
});
void test('different monthrates require MZN map reconciliation; UI source uses converted subset without relabellingUSD credit balances', () => {
const { d, b } = setup('USD');
b.periodsFx.months[0].fxSelection = fx('mzn_per_unit', '65');
assert.equal(calculateNormal(d, b).colecta, null);
b.depreciation.assets[0].accounting = '3850';
b.schedules.interest.amount = '7700';
assert.equal(val(calculateNormal(d, b).colecta), 64272);
d.modelo_a_snapshot = saveNormalBook(d, b);
const source = normalFinancialSource(d);
assert.equal(source.currency, 'MZN');
assert.equal(source.periods, 6);
assert.equal(
source.accountingLines.find((l) => l.code === 'depreciation').total,
'3850.00',
);
assert.deepEqual(source.cashSchedule, []);
});
void test('noncalendar annual mapping cannot bypass existing authorised period and unsupportedshortdates stay pending', () => {
const { d, b } = setup();
b.periodsFx.kind = 'annual';
b.periodsFx.from = '2025-07-01';
b.periodsFx.to = '2026-06-30';
b.periodsFx.planStart = '2025-07-01';
assert.equal(calculateNormal(d, b).colecta, null);
assert.ok(
calculateNormal(d, b).pending.some((s) =>
s.includes('autorizado/comunicado'),
),
);
});
void test('reusedFX link invalidates current result when existing source changes and never changes historical result', () => {
const { d, b } = setup('USD'),
snapshot = JSON.parse(d.modelo_a_snapshot);
snapshot.currencyEvents ??= {};
snapshot.currencyEvents[d.context_opportunity] = {
version: 1,
rows: [
{
...newCurrencyEvent('fx-shared', 'MZN'),
sourceCurrency: 'USD',
fxSelection: fx(),
},
],
};
d.modelo_a_snapshot = JSON.stringify(snapshot);
const linked = irpcFxSources(d)[0];
assert.ok(linked);
b.periodsFx.months[0].fxLink = {
id: linked.id,
fingerprint: linked.fingerprint,
};
b.periodsFx.months[0].fxSelection = linked.selection;
d.modelo_a_snapshot = saveNormalBook(d, recordNormal(d, b));
const book = normalBook(d),
old = structuredClone(book.result);
assert.equal(normalCurrent(d, book), true);
const changed = JSON.parse(d.modelo_a_snapshot);
changed.currencyEvents[d.context_opportunity].rows[0].description =
'New source context';
d.modelo_a_snapshot = JSON.stringify(changed);
assert.equal(normalCurrent(d, normalBook(d)), false);
assert.deepEqual(normalBook(d).result, old);
assert.equal(calculateNormal(d, normalBook(d)).colecta, null);
});
void test('known nondepreciable asset in shortperiod keeps full addback without asking irrelevant timing', () => {
const { d, b } = setup();
Object.assign(b.depreciation.assets[0], {
category: 'nondepreciable',
timing: '',
method: '',
});
const m = calculateNormal(d, b);
assert.equal(val(m.depreciation.adjustment), 60);
assert.equal(val(m.colecta), 1017.6);
});