455 lines
17 KiB
JavaScript
455 lines
17 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 {
|
|
newSpecialInput,
|
|
newQualification,
|
|
newSpecialExpense,
|
|
newAcceleratedAsset,
|
|
} from '../../resources/js/lib/irpc-special.ts';
|
|
import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts';
|
|
const seed = () =>
|
|
JSON.parse(
|
|
fs.readFileSync(
|
|
new URL(
|
|
'./fixtures/irpc-l6-values-preserved.json',
|
|
import.meta.url,
|
|
),
|
|
'utf8',
|
|
),
|
|
);
|
|
const value = (v) => (v ? Number(v.numerator) / Number(v.denominator) : null);
|
|
const qualification = (
|
|
state = 'absent',
|
|
from = '2026-07-01',
|
|
to = '2026-12-31',
|
|
) => ({
|
|
...newQualification(),
|
|
state,
|
|
eligible: state === 'known' ? 'yes' : '',
|
|
from,
|
|
to,
|
|
reference:
|
|
'QA declaração/prova das condições legais no sujeito e parcela,sem inferência territorial',
|
|
periodReference: 'QA norma/exercício2026 e período documentado',
|
|
});
|
|
function setup() {
|
|
const d = seed(),
|
|
b = normalBook(d);
|
|
b.special = newSpecialInput();
|
|
for (const part of [
|
|
b.special.expenses,
|
|
b.special.cooperative,
|
|
b.special.acceleration,
|
|
])
|
|
part.qualification = qualification();
|
|
return { d, b };
|
|
}
|
|
function expenses() {
|
|
const { d, b } = setup();
|
|
b.schedules.rows.push({
|
|
id: 'expense36g',
|
|
rule: 'undocumented',
|
|
amount: '100',
|
|
sourceCode: 'operating_expenses',
|
|
document: 'QA-despesa',
|
|
line: '1',
|
|
reference: 'QA despesa100 efectivamente incluída e não documentada36g',
|
|
fiscalYear: '',
|
|
conditions: 'yes',
|
|
});
|
|
b.profit.topics.expenseCorrections = {
|
|
state: 'known',
|
|
amount: '100',
|
|
reference: 'QA acréscimo36g já conferido,sem outra correcção',
|
|
};
|
|
b.special.expenses.qualification = qualification('known');
|
|
b.special.expenses.rows = [
|
|
{
|
|
...newSpecialExpense('autonomous1'),
|
|
scheduleId: 'expense36g',
|
|
nature: 'undocumented',
|
|
eligible: 'yes',
|
|
reference:
|
|
'QA qualificação legal61(4),não inferida de anexo em falta',
|
|
},
|
|
];
|
|
return { d, b };
|
|
}
|
|
function cooperative() {
|
|
const { d, b } = setup();
|
|
b.special.cooperative = {
|
|
qualification: qualification('known'),
|
|
kind: 'agrarian',
|
|
allIncomeEligible: 'yes',
|
|
withheldIncome: 'absent',
|
|
};
|
|
return { d, b };
|
|
}
|
|
function acceleration() {
|
|
const { d, b } = setup();
|
|
delete b.periodsFx;
|
|
delete b.forms;
|
|
b.schedules.interest.amount = '240';
|
|
Object.assign(b.depreciation.assets[0], {
|
|
category: 'machine',
|
|
description: 'Máquina industrial existente QA',
|
|
cost: '1000',
|
|
rate: '10',
|
|
accounting: '120',
|
|
start: '2026-01-01',
|
|
timing: 'annual',
|
|
});
|
|
for (const part of [
|
|
b.special.expenses,
|
|
b.special.cooperative,
|
|
b.special.acceleration,
|
|
])
|
|
part.qualification = qualification(
|
|
'absent',
|
|
'2026-01-01',
|
|
'2026-12-31',
|
|
);
|
|
b.special.acceleration = {
|
|
qualification: qualification('known', '2026-01-01', '2026-12-31'),
|
|
cbfRequirements: 'yes',
|
|
recognition: 'documented',
|
|
recognitionReference:
|
|
'QA instrumento do projecto/reconhecimento10,pressupostos3/9 e afectação industrial documentados',
|
|
noOtherBenefits: 'yes',
|
|
rows: [
|
|
{
|
|
...newAcceleratedAsset('accelerated1'),
|
|
assetId: b.depreciation.assets[0].id,
|
|
kind: 'industrial_equipment',
|
|
eligible: 'yes',
|
|
reference:
|
|
'QA factura/afectação da máquina ao projecto industrialCBF16',
|
|
},
|
|
],
|
|
};
|
|
return { d, b };
|
|
}
|
|
void test('no optionalL7 keeps exact accepted9 result and key; installed history is not recalculated on read', () => {
|
|
const d = seed(),
|
|
b = normalBook(d),
|
|
old = structuredClone(b.result);
|
|
assert.equal(normalCurrent(d, b), true);
|
|
assert.deepEqual(calculateNormal(d, b), old.model);
|
|
assert.deepEqual(normalBook(d).result, old);
|
|
});
|
|
void test('61(4) reuses100 addback36g and computes35 autonomously without another profit adjustment', () => {
|
|
const { d, b } = expenses(),
|
|
m = calculateNormal(d, b),
|
|
s = m.special;
|
|
assert.equal(m.version, 'IRPC-normal-adjustments/10');
|
|
assert.equal(value(m.taxableProfit), 3230);
|
|
assert.equal(value(m.colecta), 1033.6);
|
|
assert.equal(value(s.taxableProfit), 3230);
|
|
assert.equal(value(s.expenses.rows[0].l6Adjustment), 100);
|
|
assert.equal(value(s.expenses.rows[0].additionalProfitAdjustment), 0);
|
|
assert.equal(value(s.expenses.autonomousTax), 35);
|
|
assert.equal(value(s.combinedColecta), 1068.6);
|
|
assert.equal(s.finalBalance.payable, null);
|
|
});
|
|
void test('missing, absent and notapplicable differ; attachment presence alone never classifies an expense', () => {
|
|
const { d, b } = setup();
|
|
b.special.expenses.qualification = qualification('');
|
|
let s = calculateNormal(d, b).special;
|
|
assert.equal(s.expenses.status, 'indeterminate');
|
|
assert.equal(s.expenses.autonomousTax, null);
|
|
b.special.expenses.qualification = qualification('absent');
|
|
s = calculateNormal(d, b).special;
|
|
assert.equal(s.expenses.status, 'absent');
|
|
assert.equal(value(s.expenses.autonomousTax), 0);
|
|
b.special.expenses.qualification = qualification('not_applicable');
|
|
assert.equal(
|
|
calculateNormal(d, b).special.expenses.status,
|
|
'not_applicable',
|
|
);
|
|
const c = expenses();
|
|
c.b.special.expenses.rows[0].nature = '';
|
|
assert.equal(
|
|
calculateNormal(c.d, c.b).special.expenses.autonomousTax,
|
|
null,
|
|
);
|
|
});
|
|
void test('false condition keeps existing addback and no autonomous tax; contradictory absence cannot erase expense', () => {
|
|
const { d, b } = expenses();
|
|
b.special.expenses.rows[0].eligible = 'no';
|
|
let m = calculateNormal(d, b);
|
|
assert.equal(m.special.expenses.rows[0].status, 'not_applicable');
|
|
assert.equal(value(m.special.expenses.autonomousTax), 0);
|
|
assert.equal(value(m.taxableProfit), 3230);
|
|
b.special.expenses.rows = [];
|
|
b.special.expenses.qualification = qualification('absent');
|
|
m = calculateNormal(d, b);
|
|
assert.equal(m.special.expenses.status, 'indeterminate');
|
|
assert.equal(value(m.taxableProfit), 3230);
|
|
});
|
|
void test('duplicate, unlinked or uncalculated36g cannot doublecharge or manufacture tax', () => {
|
|
for (const mode of [
|
|
'duplicate',
|
|
'unlinked',
|
|
'uncalculated',
|
|
'missingcoverage',
|
|
]) {
|
|
const { d, b } = expenses();
|
|
if (mode === 'duplicate')
|
|
b.special.expenses.rows.push({
|
|
...b.special.expenses.rows[0],
|
|
id: 'duplicate',
|
|
});
|
|
if (mode === 'unlinked')
|
|
b.special.expenses.rows[0].scheduleId = 'not-found';
|
|
if (mode === 'uncalculated') b.schedules.rows[0].conditions = '';
|
|
if (mode === 'missingcoverage') b.special.expenses.rows = [];
|
|
assert.equal(
|
|
calculateNormal(d, b).special.expenses.autonomousTax,
|
|
null,
|
|
mode,
|
|
);
|
|
}
|
|
});
|
|
void test('common qualification rejects wrong period/year or missing identity without blocking independent L6 totals', () => {
|
|
for (const mutate of [
|
|
(b) => (b.special.expenses.qualification.to = '2025-12-31'),
|
|
(b) => (b.special.expenses.qualification.periodReference = ''),
|
|
(b) => (b.base.subjectNuit = ''),
|
|
]) {
|
|
const { d, b } = expenses();
|
|
mutate(b);
|
|
const m = calculateNormal(d, b);
|
|
assert.equal(m.special.expenses.autonomousTax, null);
|
|
if (b.base.subjectNuit) assert.equal(value(m.colecta), 1033.6);
|
|
}
|
|
});
|
|
void test('unknown benefit does not suppress separately calculable autonomous expense or normal reference', () => {
|
|
const { d, b } = expenses();
|
|
b.special.acceleration.qualification = qualification('');
|
|
const m = calculateNormal(d, b);
|
|
assert.equal(value(m.special.expenses.autonomousTax), 35);
|
|
assert.equal(value(m.special.reference.colecta), 1033.6);
|
|
assert.equal(m.special.combinedColecta, null);
|
|
});
|
|
void test('eligible cooperative uses16percent without changing reference32percent or autonomous expense rate', () => {
|
|
const { d, b } = cooperative(),
|
|
m = calculateNormal(d, b);
|
|
assert.equal(value(m.colecta), 1001.6);
|
|
assert.equal(m.special.cooperative.status, 'calculated');
|
|
assert.equal(value(m.special.cooperative.rate), 16);
|
|
assert.equal(value(m.special.colecta), 500.8);
|
|
assert.equal(value(m.special.taxableBase), 3130);
|
|
assert.equal(value(m.special.combinedColecta), 500.8);
|
|
const x = expenses();
|
|
x.b.special.cooperative = structuredClone(b.special.cooperative);
|
|
assert.equal(
|
|
value(calculateNormal(x.d, x.b).special.combinedColecta),
|
|
551.8,
|
|
);
|
|
});
|
|
void test('mixed, withheld, unclassified or false cooperative conditions never grant blanket reduction', () => {
|
|
for (const mutate of [
|
|
(c) => (c.allIncomeEligible = 'no'),
|
|
(c) => (c.withheldIncome = 'known'),
|
|
(c) => (c.kind = 'other'),
|
|
]) {
|
|
const { d, b } = cooperative();
|
|
mutate(b.special.cooperative);
|
|
const m = calculateNormal(d, b);
|
|
assert.equal(m.special.cooperative.status, 'indeterminate');
|
|
assert.equal(m.special.colecta, null);
|
|
assert.equal(value(m.colecta), 1001.6);
|
|
}
|
|
const { d, b } = cooperative();
|
|
b.special.cooperative.qualification.eligible = 'no';
|
|
assert.equal(value(calculateNormal(d, b).special.colecta), 1001.6);
|
|
});
|
|
void test('cooperative treatment does not require mislabelling special rate as L6 general; contradictory absence remains pending', () => {
|
|
const { d, b } = cooperative();
|
|
b.assessment.treatment = 'other';
|
|
let m = calculateNormal(d, b);
|
|
assert.equal(m.colecta, null);
|
|
assert.equal(value(m.special.colecta), 500.8);
|
|
b.special.cooperative.qualification = qualification('absent');
|
|
m = calculateNormal(d, b);
|
|
assert.equal(m.special.cooperative.status, 'indeterminate');
|
|
assert.equal(m.special.colecta, null);
|
|
});
|
|
void test('common period follows declared shortperiod even without schedules; calendar dates cannot replace it', () => {
|
|
const { d, b } = cooperative();
|
|
delete b.schedules;
|
|
b.special.expenses.qualification = qualification(
|
|
'absent',
|
|
'2026-01-01',
|
|
'2026-12-31',
|
|
);
|
|
const m = calculateNormal(d, b);
|
|
assert.equal(m.special.period.from, '2026-07-01');
|
|
assert.equal(m.special.expenses.status, 'indeterminate');
|
|
});
|
|
void test('CBF16 reuses asset and rate10to15 but respects120 accounted cap; additional deduction20 not50', () => {
|
|
const { d, b } = acceleration(),
|
|
input = JSON.stringify(b.depreciation),
|
|
m = calculateNormal(d, b),
|
|
s = m.special;
|
|
assert.equal(s.acceleration.status, 'calculated', s.acceleration.reason);
|
|
assert.equal(value(m.depreciation.assets[0].quota), 100);
|
|
assert.equal(value(s.acceleration.accelerated.assets[0].quota), 150);
|
|
assert.equal(value(s.acceleration.accelerated.assets[0].deduction), 120);
|
|
assert.equal(value(s.acceleration.additionalDeduction), 20);
|
|
assert.equal(value(m.taxableProfit), 6260);
|
|
assert.equal(value(s.taxableProfit), 6240);
|
|
assert.equal(value(s.taxableBase), 6240);
|
|
assert.equal(value(s.colecta), 1996.8);
|
|
assert.equal(JSON.stringify(b.depreciation), input);
|
|
});
|
|
void test('new project building reuses land separation and legal table rate before bounded acceleration', () => {
|
|
const { d, b } = acceleration();
|
|
Object.assign(b.depreciation.assets[0], {
|
|
category: 'commercial_building',
|
|
cost: '5000',
|
|
land: '0',
|
|
rate: '2',
|
|
});
|
|
b.special.acceleration.rows[0].kind = 'new_project_building';
|
|
const m = calculateNormal(d, b);
|
|
assert.equal(m.special.acceleration.status, 'calculated');
|
|
assert.equal(
|
|
value(m.special.acceleration.accelerated.assets[0].quota),
|
|
150,
|
|
);
|
|
assert.equal(value(m.special.acceleration.additionalDeduction), 20);
|
|
});
|
|
void test('lower optional normal quota remains valid L6 but is not treated as legally fixed rate in CBF16', () => {
|
|
const { d, b } = acceleration();
|
|
b.depreciation.assets[0].rate = '5';
|
|
const m = calculateNormal(d, b);
|
|
assert.equal(m.depreciation.assets[0].status, 'calculated');
|
|
assert.equal(m.special.acceleration.additionalDeduction, null);
|
|
assert.match(m.special.acceleration.reason, /quota inferior/);
|
|
});
|
|
void test('location alone, wrong activity, prior quotas, unsupported periods and benefit combinations cannot accelerate', () => {
|
|
for (const mutate of [
|
|
(b) => (b.special.acceleration.cbfRequirements = ''),
|
|
(b) => (b.special.acceleration.recognitionReference = ''),
|
|
(b) => (b.special.acceleration.rows[0].kind = 'other'),
|
|
(b) => (b.depreciation.assets[0].timing = 'months'),
|
|
(b) => (b.depreciation.assets[0].previousAccounting = '1'),
|
|
(b) => (b.special.acceleration.noOtherBenefits = 'no'),
|
|
(b) => {
|
|
b.special.cooperative = {
|
|
qualification: qualification(
|
|
'known',
|
|
'2026-01-01',
|
|
'2026-12-31',
|
|
),
|
|
kind: 'agrarian',
|
|
allIncomeEligible: 'yes',
|
|
withheldIncome: 'absent',
|
|
};
|
|
},
|
|
]) {
|
|
const { d, b } = acceleration();
|
|
d.province_id = 'ZEE-candidate';
|
|
mutate(b);
|
|
const m = calculateNormal(d, b);
|
|
assert.equal(m.special.acceleration.additionalDeduction, null);
|
|
assert.notEqual(m.taxableProfit, null);
|
|
assert.equal(m.special.acceleration.status, 'indeterminate');
|
|
}
|
|
});
|
|
void test('save/recover/report preserves9history and L5/books; repeated calculation never accumulates tax or asset deduction', () => {
|
|
const { d, b } = expenses(),
|
|
old = structuredClone(b.result),
|
|
plan = d.business_plan_snapshot;
|
|
d.modelo_a_snapshot = saveNormalBook(
|
|
d,
|
|
recordNormal(d, b, '2026-10-26T14:00:00+02:00'),
|
|
);
|
|
let 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, plan);
|
|
assert.ok(JSON.parse(d.modelo_a_snapshot).irpcWithholdings);
|
|
const html = normalHtml(d);
|
|
assert.match(html, /imposto autónomo:35/);
|
|
assert.match(html, /novo ajustamento ao lucro:0/);
|
|
assert.match(html, /Saldo final a pagar\/recuperar:Indeterminado/);
|
|
assert.match(
|
|
JSON.stringify(
|
|
createProjectDossierReport(d, {
|
|
selectedSections: new Set(['finance']),
|
|
}),
|
|
),
|
|
/Despesas — imposto autónomo/,
|
|
);
|
|
d.modelo_a_snapshot = saveNormalBook(d, recordNormal(d, back));
|
|
back = normalBook(d);
|
|
assert.equal(value(back.result.model.special.expenses.autonomousTax), 35);
|
|
assert.equal(value(back.result.model.taxableProfit), 3230);
|
|
d.province_id = 'changed';
|
|
assert.equal(normalCurrent(d, normalBook(d)), false);
|
|
assert.throws(() => normalHtml(d), /Calcule/);
|
|
});
|
|
void test('new evidence changes key; malformedqualification or duplicate row ID cannot overwrite book', () => {
|
|
const { d, b } = expenses();
|
|
d.modelo_a_snapshot = saveNormalBook(d, recordNormal(d, b));
|
|
const back = normalBook(d);
|
|
back.special.expenses.rows[0].reference += ' alterado';
|
|
assert.equal(normalCurrent(d, back), false);
|
|
back.special.expenses.rows.push(
|
|
structuredClone(back.special.expenses.rows[0]),
|
|
);
|
|
assert.throws(() => saveNormalBook(d, back), /identificador repetido/);
|
|
});
|
|
void test('report presents context labels and retains provenance internally without dumping raw context snapshots', () => {
|
|
const { d, b } = setup();
|
|
d.province_id_label = 'Província QA';
|
|
d.context_opportunity_snapshot = '{"internal":"QA implementation blob"}';
|
|
d.modelo_a_snapshot = saveNormalBook(d, recordNormal(d, b));
|
|
assert.match(normalHtml(d), /Província: Província QA/);
|
|
assert.doesNotMatch(normalHtml(d), /QA implementation blob/);
|
|
assert.equal(
|
|
normalBook(d).result.model.special.context.context_opportunity_snapshot,
|
|
d.context_opportunity_snapshot,
|
|
);
|
|
});
|
|
void test('write only new QA cases for transactional API and recovered reports', () => {
|
|
const missing = expenses(),
|
|
negative = expenses();
|
|
missing.b.special.expenses.rows[0].nature = '';
|
|
negative.b.special.expenses.rows[0].eligible = 'no';
|
|
const cases = {
|
|
expense: expenses(),
|
|
missing,
|
|
negative,
|
|
absent: setup(),
|
|
cooperative: cooperative(),
|
|
acceleration: acceleration(),
|
|
};
|
|
fs.mkdirSync('outputs/irpc-l7-common', { recursive: true });
|
|
for (const [name, { d, b }] of Object.entries(cases)) {
|
|
d.modelo_a_snapshot = saveNormalBook(
|
|
d,
|
|
recordNormal(d, b, '2026-10-26T14:00:00+02:00'),
|
|
);
|
|
fs.writeFileSync(
|
|
'outputs/irpc-l7-common/' + name + '.json',
|
|
JSON.stringify(d),
|
|
);
|
|
fs.writeFileSync(
|
|
'outputs/irpc-l7-common/' + name + '.html',
|
|
normalHtml(d),
|
|
);
|
|
}
|
|
});
|