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

379 lines
15 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,
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');
});