import test from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import { normalBook, normalCurrent, calculateNormal, recordNormal, saveNormalBook, normalHtml, normalFinancialSource, normalReportRows, } from '../../resources/js/lib/irpc-normal.ts'; import { newNormalSchedules, newScheduleRow, newNormalAsset, } from '../../resources/js/lib/irpc-normal-schedules.ts'; import { normalFormFields } from '../../resources/js/lib/irpc-normal-forms.ts'; import { fraction } from '../../resources/js/lib/fiscal-rational.ts'; import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts'; const seed = () => JSON.parse( fs.readFileSync( new URL( './fixtures/irpc-assessment-preserved.json', import.meta.url, ), 'utf8', ), ); function scenario() { const d = seed(), b = normalBook(d), p = JSON.parse(d.business_plan_snapshot); p.plan.monthlyInterestExpense = '20'; p.plan.assetDepreciation = '120'; d.business_plan_snapshot = JSON.stringify(p); b.schedules = newNormalSchedules(); const s = b.schedules; s.period = { kind: 'calendar', from: '2026-01-01', to: '2026-12-31', reference: 'QA doze meses do plano correspondem ao ano civil completo', }; s.interest = { amount: '120', reference: 'QA contrato bancário, exploração, sem relação especial, limites especiais ou capitalização; pagamentos separados', conditions: 'yes', }; for (const [id, rule, amount, sourceCode, fiscalYear] of [ ['thirdparty', 'tax_other', '250', 'operating_expenses', ''], ['gain', 'patrimonial_gain', '200', '', ''], ['cost', 'patrimonial_cost', '50', '', ''], ['timing', 'future_cost', '100', 'operating_expenses', '2027'], ]) s.rows.push({ ...newScheduleRow(id), rule, amount, sourceCode, fiscalYear, document: 'QA-' + id, line: '1', reference: 'QA factos documentados no recorte ordinário, não é operação especial', conditions: 'yes', }); for (const [k, amount] of Object.entries({ expenseCorrections: '250', positivePatrimonial: '200', negativePatrimonial: '50', timing: '100', })) b.profit.topics[k] = { state: 'known', amount, reference: 'QA reconciliação integral do tema com os documentos do mapa', }; b.profit.lines.find((l) => l.code === 'depreciation').state = 'known'; s.assetsReference = 'QA único activo; total contabilístico120 reconciliado com o plano'; s.assets = [ { ...newNormalAsset('server'), category: 'server', description: 'Servidor QA', cost: '500', residual: '0', rate: '20', accounting: '120', previousFiscal: '0', inUseFrom: '2026-01-01', tableReference: 'Decreto72/2013 tabelaII grupo3 item9.1, sem taxa específica da tabelaI', reference: 'QA factura500, activo próprio, vida útil fiscal, sem regras especiais', conditions: 'yes', }, ]; return { d, b }; } const val = (f) => (f ? Number(f.numerator) / Number(f.denominator) : null); void test('new rules reconcile financial interest, patrimonial variations, timing and server quota through profit/base/colecta', () => { const { d, b } = scenario(), m = calculateNormal(d, b); assert.equal(val(m.accountingProfit), 6240); assert.equal(val(m.additions), 220); assert.equal(val(m.schedules.interest), 120); assert.equal(val(m.schedules.depreciation), 20); assert.equal(val(m.schedules.net), 520); assert.equal(val(m.taxableProfit), 6980); assert.equal(val(m.taxableBase), 4680); assert.equal(val(m.colecta), 1497.6); assert.equal(m.tax, null); assert.equal(m.assessment.finalBalance.payable, null); assert.equal(m.schedules.assets[0].fiscal.numerator, '100'); assert.equal(m.version, 'IRPC-normal-adjustments/5'); }); void test('ordinary interest is qualified independently from compensatory interest and cash payments', () => { const { d, b } = scenario(); b.schedules.interest.amount = '119'; const m = calculateNormal(d, b); assert.equal(m.taxableProfit, null); assert.equal(val(m.schedules.net), 520); assert.equal(m.schedules.interest, null); assert.match(m.profit.pending.join(' '), /Juros financeiros/); const c = scenario(); c.b.schedules.rows[0].sourceCode = 'interest_expense'; c.b.schedules.rows[0].amount = '20'; c.b.profit.topics.expenseCorrections.amount = '20'; assert.equal(calculateNormal(c.d, c.b).taxableProfit, null); c.b.schedules.interest.amount = '100'; assert.equal(val(calculateNormal(c.d, c.b).schedules.interest), 100); assert.notEqual(calculateNormal(c.d, c.b).taxableProfit, null); }); void test('missing rule evidence blocks dependent totals while independent amounts remain calculated', () => { const { d, b } = scenario(); b.schedules.rows[0].reference = ''; const m = calculateNormal(d, b); assert.equal(m.taxableProfit, null); assert.equal(m.colecta, null); assert.equal( val(m.schedules.effects.find((e) => e.id === 'gain').effect), 200, ); assert.equal(val(m.schedules.depreciation), 20); assert.equal(val(m.accountingProfit), 6240); }); void test('duplicate documents, over-allocation, wrong source and contradictory topic absence are rejected', () => { for (const mutate of [ (b) => { b.schedules.rows[0].document = b.rows[0].document + ' '; b.schedules.rows[0].line = b.rows[0].line; }, (b) => { b.schedules.rows[0].amount = '1201'; b.profit.topics.expenseCorrections.amount = '1201'; }, (b) => (b.schedules.rows[0].sourceCode = 'revenue'), (b) => (b.profit.topics.positivePatrimonial = { state: 'absent', amount: '', reference: 'QA declared absent', }), ]) { const { d, b } = scenario(); mutate(b); assert.equal(calculateNormal(d, b).taxableProfit, null); } }); void test('capital contributions and distributions outside result are known amounts with zero tax effect, not ordinary gains/costs', () => { const { d, b } = scenario(); b.schedules.rows.find((r) => r.id === 'gain').rule = 'capital_in'; b.schedules.rows.find((r) => r.id === 'cost').rule = 'capital_out'; const m = calculateNormal(d, b); assert.equal(val(m.taxableProfit), 6830); assert.equal( val(m.schedules.effects.find((e) => e.id === 'gain').effect), 0, ); assert.equal( val(m.schedules.effects.find((e) => e.id === 'cost').amount), 50, ); }); void test('timing belongs to documented year; previous years and inconsistent mapping stay pending', () => { for (const y of ['', '2025', '2026']) { const { d, b } = scenario(); b.schedules.rows.find((r) => r.id === 'timing').fiscalYear = y; assert.equal(calculateNormal(d, b).taxableProfit, null); } }); void test('asset quota uses remaining fiscal value; unsupported rates, short-year facts and incomplete reconciliation do not default to accounting', () => { const { d, b } = scenario(); b.schedules.assets[0].previousFiscal = '450'; let m = calculateNormal(d, b); assert.equal(val(m.schedules.assets[0].fiscal), 50); assert.equal(val(m.schedules.depreciation), 70); for (const patch of [ { rate: '25' }, { category: 'other' }, { inUseFrom: '2026-02-01' }, { inUseFrom: '2025-02-31' }, { inUseFrom: '2014-01-01' }, { accounting: '119' }, { conditions: '' }, ]) { const { d, b } = scenario(); Object.assign(b.schedules.assets[0], patch); m = calculateNormal(d, b); assert.equal(m.taxableProfit, null); assert.equal(m.schedules.depreciation, null); } }); void test('known current loss gives zero base/colecta only with documented current-loss treatment and no self-deduction; no official carryforward created', () => { const { d, b } = scenario(), p = JSON.parse(d.business_plan_snapshot); p.plan.monthlyOperatingExpenses = '1000'; d.business_plan_snapshot = JSON.stringify(p); b.base.losses = []; b.base.lossesState = 'absent'; b.base.lossesReference = 'QA ausência expressa de perdas anteriores'; let m = calculateNormal(d, b); assert.equal(val(m.taxableProfit), -3820); assert.equal(m.taxableBase, null); b.schedules.currentLossReference = 'QA perda fiscal corrente apurada, sem dedução de perdas anteriores ou reporte automático'; m = calculateNormal(d, b); assert.equal(val(m.currentLoss), 3820); assert.equal(val(m.taxableBase), 0); assert.equal(val(m.colecta), 0); assert.deepEqual(b.base.losses, []); assert.equal( normalFormFields(m).fields.find((f) => f.code === '269').value .numerator, '3820', ); assert.equal( normalFormFields(m).fields.find((f) => f.code === '270').value, null, ); d.modelo_a_snapshot = saveNormalBook(d, recordNormal(d, b)); fs.mkdirSync('outputs/irpc-l6-sets', { recursive: true }); fs.writeFileSync('outputs/irpc-l6-sets/loss.json', JSON.stringify(d)); }); void test('EP ordinary annual MZN uses its attributable financial result; unsupported currency/year remains case-specific', () => { const { d, b } = scenario(); b.scope = 'ep_general'; b.scopeReference = 'QA único EP em Moçambique, imputação completa e exclusiva de custos/proveitos do EP, CIRPC15(1)c'; assert.equal(val(calculateNormal(d, b).colecta), 1497.6); b.year = '2025'; assert.equal(calculateNormal(d, b).colecta, null); }); void test('form preparation keeps exact fractions and unmapped fields without provisional codes or fabricated final settlement', () => { const { d, b } = scenario(), m = calculateNormal(d, b), f = normalFormFields(m); assert.equal(val(f.fields.find((f) => f.code === '278').value), 4680); assert.equal( f.fields.find((f) => f.code === '300').transcriptionReady, true, ); for (const code of ['200', '302', '309–316']) assert.equal(f.fields.find((f) => f.code === code).value, null); assert.equal(f.m20.status, 'partial'); assert.equal(val(f.m20.fields.find((f) => f.account === '6.5').value), 120); assert.match(f.m20.reason, /campo201.*campo200/); const partial = normalFormFields({ ...m, colecta: fraction(2n, 625n), }).fields.find((f) => f.code === '300'); assert.deepEqual(partial.value, { numerator: '2', denominator: '625' }); assert.equal(partial.transcriptionReady, false); assert.match(partial.reason, /sem arredondamento/); }); void test('installed4 is current without recalculation; new inputs survive save/recovery/history/export/dossier and become stale on edit', () => { const original = seed(), old = normalBook(original); assert.equal(normalCurrent(original, old), true); assert.equal(old.result.model.version, 'IRPC-normal-adjustments/4'); const { d, b } = scenario(), before = JSON.parse(d.modelo_a_snapshot); d.modelo_a_snapshot = saveNormalBook( d, recordNormal(d, b, '2026-10-25T23:50:00Z'), ); const recovered = JSON.parse(JSON.stringify(d)), back = normalBook(recovered); assert.equal(normalCurrent(recovered, back), true); assert.deepEqual(back.history.at(-1), old.result); assert.deepEqual(JSON.parse(d.modelo_a_snapshot).books, before.books); assert.deepEqual( JSON.parse(d.modelo_a_snapshot).irpcWithholdings, before.irpcWithholdings, ); assert.match(normalHtml(d), /1497.6/); assert.match( JSON.stringify( createProjectDossierReport(d, { selectedSections: new Set(['finance']), }), ), /M22 campo 300/, ); assert.match(JSON.stringify(normalReportRows(d)), /M20/); fs.writeFileSync('outputs/irpc-l6-sets/calculated.json', JSON.stringify(d)); fs.writeFileSync('outputs/irpc-l6-sets/preparation.html', normalHtml(d)); back.schedules.rows[0].amount = '251'; recovered.modelo_a_snapshot = saveNormalBook(recovered, back); assert.equal(normalCurrent(recovered, normalBook(recovered)), false); assert.throws(() => normalHtml(recovered)); const missing = normalBook(d); missing.schedules.interest.conditions = ''; d.modelo_a_snapshot = saveNormalBook(d, recordNormal(d, missing)); fs.writeFileSync('outputs/irpc-l6-sets/missing.json', JSON.stringify(d)); }); void test('unknown schema/rules fail without silently replacing saved originals', () => { const { d, b } = scenario(); b.schedules.version = 2; assert.throws(() => saveNormalBook(d, b)); b.schedules.version = 1; b.schedules.rows[0].rule = 'fiction'; assert.throws(() => saveNormalBook(d, b)); }); void test('documented noncalendar EP year preserves12 monthly periods; short periods and absent legal basis stay case-specific', () => { const { d, b } = scenario(); b.scope = 'ep_general'; b.schedules.period = { kind: 'ep_annual', from: '2025-07-01', to: '2026-06-30', reference: 'QA comunicação expressa AT anterior, início e manutenção5exercícios, plano completo do EP', }; b.schedules.assets[0].inUseFrom = '2025-07-01'; let m = calculateNormal(d, b); assert.equal(val(m.colecta), 1497.6); assert.equal(m.assessment.period.from, '2025-07-01'); assert.equal(m.assessment.period.to, '2026-06-30'); d.modelo_a_snapshot = saveNormalBook(d, recordNormal(d, b)); fs.writeFileSync('outputs/irpc-l6-sets/ep.json', JSON.stringify(d)); b.schedules.period.from = '2026-01-01'; assert.equal(calculateNormal(d, b).colecta, null); b.schedules.period.reference = ''; assert.equal(calculateNormal(d, b).taxableProfit, null); }); void test('ordinary tax/undocumented addbacks and current income/cost timing apply their distinct signs without overwriting original accounting', () => { for (const rule of ['tax_own', 'undocumented']) { const { d, b } = scenario(); b.schedules.rows[0].rule = rule; assert.equal(val(calculateNormal(d, b).taxableProfit), 6980); } for (const [rule, delta] of [ ['current_cost', -200], ['current_income', 0], ]) { const { d, b } = scenario(), r = b.schedules.rows.find((r) => r.id === 'timing'); r.rule = rule; r.sourceCode = ''; r.fiscalYear = '2026'; assert.equal(val(calculateNormal(d, b).taxableProfit), 6980 + delta); } const { d, b } = scenario(), r = b.schedules.rows.find((r) => r.id === 'timing'); r.rule = 'future_income'; r.sourceCode = 'revenue'; assert.equal(val(calculateNormal(d, b).taxableProfit), 6780); assert.equal(normalFinancialSource(d).accountingProfitBeforeTax, '6240.00'); });