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); });