267 lines
9.8 KiB
JavaScript
267 lines
9.8 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import {
|
|
normalBook,
|
|
normalCurrent,
|
|
calculateNormal,
|
|
recordNormal,
|
|
saveNormalBook,
|
|
normalHtml,
|
|
} from '../../resources/js/lib/irpc-normal.ts';
|
|
import {
|
|
newNormalBaseInput,
|
|
newNormalLoss,
|
|
calculateNormalBase,
|
|
} from '../../resources/js/lib/irpc-normal-base.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-profit-preserved.json', import.meta.url),
|
|
'utf8',
|
|
),
|
|
);
|
|
const loss = (id, year, assessed, used, requested) => ({
|
|
...newNormalLoss(id),
|
|
year,
|
|
subjectNuit: '123456789',
|
|
assessed,
|
|
previouslyUsed: used,
|
|
requested,
|
|
reference: 'QA saldo fiscal corrigido,sujeito/exercício ' + year,
|
|
kind: 'ordinary',
|
|
annualPeriods: 'yes',
|
|
generalActivity: 'yes',
|
|
balanceConfirmed: 'yes',
|
|
});
|
|
const input = () => ({
|
|
...newNormalBaseInput(),
|
|
subjectNuit: '123456789',
|
|
identityReference: 'QA sujeito do lucro e dos saldos',
|
|
method: 'direct',
|
|
methodReference: 'QA declaração e contabilidade organizada',
|
|
lossesState: 'known',
|
|
lossesReference: 'QA inventário completo dos saldos fiscais do sujeito',
|
|
unchangedObject: 'yes',
|
|
objectReference: 'QA actividade e objecto mantidos ao termo2026',
|
|
losses: [
|
|
loss('old', '2021', '1000', '200', '800'),
|
|
loss('new', '2024', '3000', '500', '1500'),
|
|
],
|
|
benefits: {
|
|
state: 'known',
|
|
amount: '0',
|
|
reference: 'QA deduções/benefícios à base,valor conhecido zero',
|
|
},
|
|
});
|
|
void test('6700 is reused as profit;2300 documented loss use yields4400 taxable base,never tax payable', () => {
|
|
const d = seed(),
|
|
b = normalBook(d);
|
|
b.base = input();
|
|
const m = calculateNormal(d, b);
|
|
assert.equal(m.taxableProfit.numerator, '6700');
|
|
assert.equal(m.base.requestedLosses.numerator, '2300');
|
|
assert.equal(m.base.deductedLosses.numerator, '2300');
|
|
assert.equal(m.taxableBase.numerator, '4400');
|
|
assert.equal(m.tax, null);
|
|
assert.deepEqual(
|
|
m.base.losses.map((l) => l.available.numerator),
|
|
['800', '2500'],
|
|
);
|
|
assert.deepEqual(
|
|
m.base.losses.map((l) => l.remaining.numerator),
|
|
['0', '1000'],
|
|
);
|
|
assert.deepEqual(
|
|
m.base.losses.map((l) => l.lastOrdinaryYear),
|
|
[2026, 2029],
|
|
);
|
|
});
|
|
void test('supported absence of losses and benefits retains6700 base;missing benefit information does not mean zero', () => {
|
|
const i = {
|
|
...input(),
|
|
lossesState: 'absent',
|
|
losses: [],
|
|
unchangedObject: '',
|
|
objectReference: '',
|
|
benefits: {
|
|
state: 'absent',
|
|
amount: '',
|
|
reference: 'QA sem benefício aplicável',
|
|
},
|
|
};
|
|
let m = calculateNormalBase(i, fraction(6700n), '2026');
|
|
assert.equal(m.taxableBase.numerator, '6700');
|
|
i.benefits = { state: '', amount: '', reference: '' };
|
|
m = calculateNormalBase(i, fraction(6700n), '2026');
|
|
assert.equal(m.taxableBase, null);
|
|
assert.equal(m.taxableProfit.numerator, '6700');
|
|
});
|
|
void test('expired ordinary2020 loss cannot be deducted;2021 remains within last ordinary year2026', () => {
|
|
const i = input();
|
|
i.losses = [loss('expired', '2020', '500', '0', '0')];
|
|
i.unchangedObject = '';
|
|
i.objectReference = '';
|
|
let m = calculateNormalBase(i, fraction(6700n), '2026');
|
|
assert.equal(m.losses[0].status, 'excluded');
|
|
assert.equal(m.losses[0].lastOrdinaryYear, 2025);
|
|
assert.equal(m.taxableBase.numerator, '6700');
|
|
i.losses[0].requested = '1';
|
|
assert.equal(
|
|
calculateNormalBase(i, fraction(6700n), '2026').taxableBase,
|
|
null,
|
|
);
|
|
});
|
|
void test('losses cannot exceed available balance or profit;no implicit allocation order or percentage cap', () => {
|
|
const i = input();
|
|
i.losses[0].requested = '801';
|
|
assert.equal(
|
|
calculateNormalBase(i, fraction(6700n), '2026').taxableBase,
|
|
null,
|
|
);
|
|
i.losses = [
|
|
loss('a', '2021', '5000', '0', '5000'),
|
|
loss('b', '2024', '5000', '0', '4000'),
|
|
];
|
|
let m = calculateNormalBase(i, fraction(6700n), '2026');
|
|
assert.equal(m.taxableBase, null);
|
|
assert.match(m.pending.join(' '), /reparta explicitamente/);
|
|
i.losses[0].requested = '2700';
|
|
m = calculateNormalBase(i, fraction(6700n), '2026');
|
|
assert.equal(m.taxableBase.numerator, '0');
|
|
assert.equal(m.deductedLosses.numerator, '6700');
|
|
});
|
|
void test('same taxpayer,annual periods,ordinary origin,reconciled balance and activity facts are specific dependencies', () => {
|
|
for (const patch of [
|
|
{ subjectNuit: '987654321' },
|
|
{ annualPeriods: 'no' },
|
|
{ generalActivity: 'no' },
|
|
{ balanceConfirmed: '' },
|
|
{ kind: 'transfer' },
|
|
{ year: '2026' },
|
|
{ previouslyUsed: '1100' },
|
|
{ requested: '' },
|
|
]) {
|
|
const i = input();
|
|
Object.assign(i.losses[0], patch);
|
|
assert.equal(
|
|
calculateNormalBase(i, fraction(6700n), '2026').taxableBase,
|
|
null,
|
|
);
|
|
}
|
|
});
|
|
void test('changed object explicitly excludes deduction;duplicate annual balances and indirect methods remain pending', () => {
|
|
const i = input();
|
|
i.unchangedObject = 'no';
|
|
i.losses.forEach((l) => (l.requested = '0'));
|
|
let m = calculateNormalBase(i, fraction(6700n), '2026');
|
|
assert.equal(m.taxableBase.numerator, '6700');
|
|
assert.ok(m.losses.every((l) => l.status === 'excluded'));
|
|
i.unchangedObject = 'yes';
|
|
i.losses[1].year = '2021';
|
|
assert.equal(
|
|
calculateNormalBase(i, fraction(6700n), '2026').taxableBase,
|
|
null,
|
|
);
|
|
i.losses[1].year = '2024';
|
|
i.method = 'indirect';
|
|
assert.equal(
|
|
calculateNormalBase(i, fraction(6700n), '2026').taxableBase,
|
|
null,
|
|
);
|
|
});
|
|
void test('positive known benefits are preserved without free subtraction;absence contradictions remain explicit', () => {
|
|
const i = input();
|
|
i.benefits = {
|
|
state: 'known',
|
|
amount: '200',
|
|
reference: 'QA benefício positivo,regra por corresponder',
|
|
};
|
|
let m = calculateNormalBase(i, fraction(6700n), '2026');
|
|
assert.equal(m.input.benefits.amount, '200');
|
|
assert.equal(m.taxableBase, null);
|
|
i.benefits = {
|
|
state: 'absent',
|
|
amount: '200',
|
|
reference: 'QA ausência contraditória',
|
|
};
|
|
assert.equal(
|
|
calculateNormalBase(i, fraction(6700n), '2026').taxableBase,
|
|
null,
|
|
);
|
|
i.benefits = {
|
|
state: 'known',
|
|
amount: '0',
|
|
reference: 'QA zero conhecido',
|
|
};
|
|
i.lossesState = 'absent';
|
|
assert.equal(
|
|
calculateNormalBase(i, fraction(6700n), '2026').taxableBase,
|
|
null,
|
|
);
|
|
});
|
|
void test('unknown or negative current profit is preserved,not transformed to zero or current-year loss carryforward', () => {
|
|
for (const p of [null, fraction(-100n)]) {
|
|
const m = calculateNormalBase(input(), p, '2026');
|
|
assert.equal(m.taxableBase, null);
|
|
assert.deepEqual(m.taxableProfit, p);
|
|
}
|
|
});
|
|
void test('legacy current result and all books survive;new3 history,save/recovery/dossier and repeated simulation do not consume official balances', () => {
|
|
const d = seed(),
|
|
legacy = normalBook(d);
|
|
assert.equal(normalCurrent(d, legacy), true);
|
|
assert.equal(legacy.result.model.version, 'IRPC-normal-adjustments/2');
|
|
const b = { ...legacy, base: input() },
|
|
before = JSON.parse(d.modelo_a_snapshot),
|
|
saved = recordNormal(d, b, '2026-10-25T23:00:00Z');
|
|
assert.deepEqual(saved.history.at(-1), legacy.result);
|
|
assert.equal(saved.result.model.version, 'IRPC-normal-adjustments/3');
|
|
d.modelo_a_snapshot = saveNormalBook(d, saved);
|
|
assert.deepEqual(
|
|
JSON.parse(d.modelo_a_snapshot).irpcWithholdings,
|
|
before.irpcWithholdings,
|
|
);
|
|
const back = JSON.parse(JSON.stringify(d));
|
|
assert.equal(normalCurrent(back, normalBook(back)), true);
|
|
assert.match(normalHtml(back), /matéria colectável simulada:4400/);
|
|
assert.match(normalHtml(back), /imposto a pagar:Indeterminado/);
|
|
assert.match(
|
|
JSON.stringify(
|
|
createProjectDossierReport(back, {
|
|
selectedSections: new Set(['finance']),
|
|
}),
|
|
),
|
|
/4400/,
|
|
);
|
|
const repeated = recordNormal(back, normalBook(back));
|
|
assert.equal(repeated.result.model.taxableBase.numerator, '4400');
|
|
assert.equal(repeated.base.losses[1].previouslyUsed, '500');
|
|
assert.equal(repeated.base.losses[1].requested, '1500');
|
|
const changed = normalBook(back);
|
|
changed.base.losses[0].requested = '700';
|
|
back.modelo_a_snapshot = saveNormalBook(back, changed);
|
|
assert.equal(normalCurrent(back, normalBook(back)), false);
|
|
assert.throws(() => normalHtml(back));
|
|
fs.mkdirSync('outputs/irpc-l6-base', { recursive: true });
|
|
fs.writeFileSync('outputs/irpc-l6-base/calculated.json', JSON.stringify(d));
|
|
fs.writeFileSync('outputs/irpc-l6-base/preparation.html', normalHtml(d));
|
|
const missing = normalBook(d);
|
|
missing.base.benefits = { state: '', amount: '', reference: '' };
|
|
const md = structuredClone(d);
|
|
md.modelo_a_snapshot = saveNormalBook(md, recordNormal(md, missing));
|
|
fs.writeFileSync('outputs/irpc-l6-base/missing.json', JSON.stringify(md));
|
|
});
|
|
void test('malformed future input versions and duplicate IDs fail without resetting originals', () => {
|
|
const d = seed(),
|
|
b = normalBook(d);
|
|
b.base = input();
|
|
b.base.version = 2;
|
|
assert.throws(() => saveNormalBook(d, b));
|
|
b.base.version = 1;
|
|
b.base.losses[1].id = b.base.losses[0].id;
|
|
assert.throws(() => saveNormalBook(d, b));
|
|
});
|