339 lines
13 KiB
JavaScript
339 lines
13 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 { validateSpecial } from '../../resources/js/lib/irpc-special.ts';
|
|
import { fraction } from '../../resources/js/lib/fiscal-rational.ts';
|
|
import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts';
|
|
const frozen = () =>
|
|
JSON.parse(
|
|
fs.readFileSync(
|
|
new URL('./fixtures/irpc-l7-preserved.json', import.meta.url),
|
|
'utf8',
|
|
),
|
|
);
|
|
const val = (v) => (v ? Number(v.numerator) / Number(v.denominator) : null);
|
|
const q = (state) => ({
|
|
state,
|
|
eligible: state === 'known' ? 'yes' : '',
|
|
from: '2026-01-01',
|
|
to: '2026-12-31',
|
|
reference: 'QA condições/ausência documentadas no sujeito/projecto',
|
|
periodReference: 'QA ano civil2026,CBF16 e Decreto72/2013art9',
|
|
});
|
|
function setup(account = '60', start = '2026-07-15') {
|
|
const d = frozen().acceleration,
|
|
b = normalBook(d),
|
|
p = JSON.parse(d.business_plan_snapshot);
|
|
p.plan.assetDepreciation = account;
|
|
d.business_plan_snapshot = JSON.stringify(p);
|
|
Object.assign(b.depreciation.assets[0], {
|
|
accounting: account,
|
|
start,
|
|
timing: 'months',
|
|
});
|
|
b.special.acceleration.monthly = {
|
|
version: 1,
|
|
reference:
|
|
'QA opção de duodécimos desde a entrada emuso,datas/meses do mesmo activoL6',
|
|
};
|
|
return { d, b };
|
|
}
|
|
void test('accepted10 books remain current and reproduce exact original expense,cooperative and annualasset models;history is read unchanged', () => {
|
|
for (const [name, d] of Object.entries(frozen())) {
|
|
const b = normalBook(d),
|
|
original = structuredClone(b.result);
|
|
assert.equal(normalCurrent(d, b), true, name);
|
|
assert.deepEqual(calculateNormal(d, b), original.model, name);
|
|
assert.deepEqual(normalBook(d).result, original, name);
|
|
}
|
|
});
|
|
void test('explicit monthly11 reuses6 L6months and limits75quota to60accounted,adding only10deduction', () => {
|
|
const { d, b } = setup(),
|
|
original = JSON.stringify(b.depreciation),
|
|
m = calculateNormal(d, b),
|
|
s = m.special,
|
|
a = s.acceleration.monthly.assets[0];
|
|
assert.equal(m.version, 'IRPC-normal-adjustments/11');
|
|
assert.equal(a.months, 6);
|
|
assert.equal(val(a.normalRate), 10);
|
|
assert.equal(val(a.acceleratedRate), 15);
|
|
assert.equal(val(a.normalQuota), 50);
|
|
assert.equal(val(a.acceleratedQuota), 75);
|
|
assert.equal(val(a.normalDeduction), 50);
|
|
assert.equal(val(a.acceleratedDeduction), 60);
|
|
assert.equal(val(a.additionalDeduction), 10);
|
|
assert.equal(val(m.taxableProfit), 6310);
|
|
assert.equal(val(m.colecta), 2019.2);
|
|
assert.equal(val(s.taxableProfit), 6300);
|
|
assert.equal(val(s.taxableBase), 6300);
|
|
assert.equal(val(s.colecta), 2016);
|
|
assert.equal(s.finalBalance.payable, null);
|
|
assert.equal(JSON.stringify(b.depreciation), original);
|
|
});
|
|
void test('month timing comes from L6 including midmonth entry,January,October and December;never dailyproration', () => {
|
|
for (const [start, account, months, normal, accelerated] of [
|
|
['2026-01-01', '120', 12, 100, 150],
|
|
['2026-07-31', '60', 6, 50, 75],
|
|
['2026-10-20', '60', 3, 25, 37.5],
|
|
['2026-12-31', '20', 1, 100 / 12, 12.5],
|
|
]) {
|
|
const { d, b } = setup(account, start),
|
|
m = calculateNormal(d, b),
|
|
a = m.special.acceleration.monthly.assets[0];
|
|
assert.equal(a.months, months);
|
|
assert.equal(val(a.normalQuota), normal);
|
|
assert.equal(val(a.acceleratedQuota), accelerated);
|
|
assert.equal(a.months, m.depreciation.assets[0].months);
|
|
}
|
|
});
|
|
void test('December retains exact fractional difference and report never rounds fiscal amount', () => {
|
|
const { d, b } = setup('20', '2026-12-15'),
|
|
m = calculateNormal(d, b),
|
|
a = m.special.acceleration.monthly.assets[0];
|
|
assert.deepEqual(a.normalQuota, fraction(25n, 3n));
|
|
assert.deepEqual(a.acceleratedQuota, fraction(25n, 2n));
|
|
assert.deepEqual(a.additionalDeduction, fraction(25n, 6n));
|
|
d.modelo_a_snapshot = saveNormalBook(d, recordNormal(d, b));
|
|
assert.match(normalHtml(d), /25\/6/);
|
|
assert.match(normalHtml(d), /≈/);
|
|
});
|
|
void test('accounted caps of0,40,60,100 remain binding and no additional quota is invented', () => {
|
|
for (const [account, normal, accelerated, diff] of [
|
|
['0', 0, 0, 0],
|
|
['40', 40, 40, 0],
|
|
['60', 50, 60, 10],
|
|
['100', 50, 75, 25],
|
|
]) {
|
|
const { d, b } = setup(account),
|
|
s = calculateNormal(d, b).special,
|
|
a = s.acceleration.monthly.assets[0];
|
|
assert.equal(val(a.normalDeduction), normal);
|
|
assert.equal(val(a.acceleratedDeduction), accelerated);
|
|
assert.equal(val(s.acceleration.additionalDeduction), diff);
|
|
}
|
|
});
|
|
void test('new project building reuses land/residual basis and existing2percent table rate with monthlycap', () => {
|
|
const { d, b } = setup('50');
|
|
Object.assign(b.depreciation.assets[0], {
|
|
category: 'commercial_building',
|
|
cost: '5000',
|
|
land: '1000',
|
|
rate: '2',
|
|
});
|
|
b.special.acceleration.rows[0].kind = 'new_project_building';
|
|
const a = calculateNormal(d, b).special.acceleration.monthly.assets[0];
|
|
assert.equal(val(a.taxBasis), 4000);
|
|
assert.equal(val(a.normalQuota), 40);
|
|
assert.equal(val(a.acceleratedQuota), 60);
|
|
assert.equal(val(a.acceleratedDeduction), 50);
|
|
assert.equal(val(a.additionalDeduction), 10);
|
|
});
|
|
void test('other existing assets stay on their normal quota;selected asset is neither copied nor deducted twice', () => {
|
|
const { d, b } = setup('80'),
|
|
other = structuredClone(b.depreciation.assets[0]);
|
|
b.depreciation.assets[0].accounting = '60';
|
|
Object.assign(other, {
|
|
id: 'other',
|
|
assetKey: 'other',
|
|
document: 'QA-other',
|
|
description: 'Mobiliário foraCBF16',
|
|
category: 'furniture',
|
|
cost: '200',
|
|
rate: '10',
|
|
start: '2026-01-01',
|
|
timing: 'annual',
|
|
accounting: '20',
|
|
});
|
|
b.depreciation.assets.push(other);
|
|
const s = calculateNormal(d, b).special;
|
|
assert.equal(s.acceleration.normal.assets.length, 2);
|
|
assert.equal(s.acceleration.accelerated.assets.length, 2);
|
|
assert.deepEqual(
|
|
s.acceleration.normal.assets[1],
|
|
s.acceleration.accelerated.assets[1],
|
|
);
|
|
assert.equal(val(s.acceleration.additionalDeduction), 10);
|
|
});
|
|
void test('missingmonthlysupport blocks only CBF16 totals;normalreference and storedinput are retained', () => {
|
|
const { d, b } = setup();
|
|
b.special.acceleration.monthly.reference = '';
|
|
const m = calculateNormal(d, b);
|
|
assert.equal(m.special.acceleration.status, 'indeterminate');
|
|
assert.equal(m.special.acceleration.additionalDeduction, null);
|
|
assert.equal(val(m.colecta), 2019.2);
|
|
assert.equal(
|
|
m.special.acceleration.monthly.assets[0].acceleratedQuota,
|
|
null,
|
|
);
|
|
assert.match(m.special.acceleration.reason, /Falta suporte/);
|
|
});
|
|
void test('negative condition,declaredabsence and missingqualification have distinct monthly outcomes', () => {
|
|
const negative = setup();
|
|
negative.b.special.acceleration.rows[0].eligible = 'no';
|
|
let s = calculateNormal(negative.d, negative.b).special;
|
|
assert.equal(s.acceleration.rows[0].status, 'not_applicable');
|
|
assert.equal(val(s.acceleration.additionalDeduction), 0);
|
|
assert.equal(s.acceleration.monthly.assets[0].applied, false);
|
|
assert.equal(s.acceleration.monthly.assets[0].acceleratedRate, null);
|
|
for (const state of ['absent', '']) {
|
|
const { d, b } = setup();
|
|
b.special.acceleration.rows = [];
|
|
b.special.acceleration.qualification = q(state);
|
|
b.special.acceleration.monthly.reference = '';
|
|
s = calculateNormal(d, b).special;
|
|
assert.equal(
|
|
s.acceleration.status,
|
|
state === 'absent' ? 'absent' : 'indeterminate',
|
|
);
|
|
assert.equal(
|
|
val(s.acceleration.additionalDeduction),
|
|
state === 'absent' ? 0 : null,
|
|
);
|
|
}
|
|
});
|
|
void test('newmonthlyscope excludes priorquotas,annualoption,exits and earlier/laterentry while retaining L6 reference', () => {
|
|
for (const mutate of [
|
|
(a) => (a.previousAccounting = '1'),
|
|
(a) => (a.timing = 'annual'),
|
|
(a) => {
|
|
a.exitState = 'known';
|
|
a.exitDate = '2026-12-01';
|
|
},
|
|
(a) => (a.start = '2025-07-01'),
|
|
(a) => (a.start = '2027-01-01'),
|
|
]) {
|
|
const { d, b } = setup();
|
|
mutate(b.depreciation.assets[0]);
|
|
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('short fiscalperiod already supported in L6 is not silently included in this annual CBF16 increment', () => {
|
|
const d = JSON.parse(
|
|
fs.readFileSync(
|
|
new URL(
|
|
'./fixtures/irpc-l6-values-preserved.json',
|
|
import.meta.url,
|
|
),
|
|
'utf8',
|
|
),
|
|
),
|
|
b = normalBook(d),
|
|
model = setup().b.special;
|
|
b.special = structuredClone(model);
|
|
for (const part of [
|
|
b.special.expenses,
|
|
b.special.cooperative,
|
|
b.special.acceleration,
|
|
])
|
|
part.qualification = { ...part.qualification, from: '2026-07-01' };
|
|
Object.assign(b.depreciation.assets[0], {
|
|
category: 'machine',
|
|
cost: '1000',
|
|
rate: '10',
|
|
accounting: '60',
|
|
timing: 'months',
|
|
start: '2026-07-01',
|
|
});
|
|
const m = calculateNormal(d, b);
|
|
assert.notEqual(m.colecta, null);
|
|
assert.equal(m.special.acceleration.additionalDeduction, null);
|
|
assert.match(m.special.acceleration.reason, /exercício anual civil2026/);
|
|
});
|
|
void test('monthlyexplicitoption never selects CBF from location;missingeligibility evidence remains pending', () => {
|
|
const { d, b } = setup();
|
|
d.province_id = 'QA-ZEE-candidate';
|
|
b.special.acceleration.recognitionReference = '';
|
|
const m = calculateNormal(d, b);
|
|
assert.equal(m.special.acceleration.additionalDeduction, null);
|
|
assert.notEqual(m.colecta, null);
|
|
assert.match(m.special.acceleration.reason, /reconhecimento10/);
|
|
});
|
|
void test('version11 save-recover preserves10history,originalasset,plan and otherbooks;recalculate stays idempotent', () => {
|
|
const { d, b } = setup(),
|
|
old = structuredClone(b.result),
|
|
asset = JSON.stringify(b.depreciation),
|
|
plan = d.business_plan_snapshot;
|
|
d.modelo_a_snapshot = saveNormalBook(d, recordNormal(d, b));
|
|
let back = normalBook(JSON.parse(JSON.stringify(d)));
|
|
assert.equal(normalCurrent(d, back), true);
|
|
assert.deepEqual(back.history.at(-1), old);
|
|
assert.equal(
|
|
back.history.at(-1).model.version,
|
|
'IRPC-normal-adjustments/10',
|
|
);
|
|
assert.equal(JSON.stringify(back.depreciation), asset);
|
|
assert.equal(d.business_plan_snapshot, plan);
|
|
assert.ok(JSON.parse(d.modelo_a_snapshot).irpcWithholdings);
|
|
const html = normalHtml(d);
|
|
assert.match(html, /duodécimosL6:6/);
|
|
assert.match(html, /diferença adicional:10.00/);
|
|
assert.match(html, /Saldo final a pagar\/recuperar:Indeterminado/);
|
|
assert.match(
|
|
JSON.stringify(
|
|
createProjectDossierReport(d, {
|
|
selectedSections: new Set(['finance']),
|
|
}),
|
|
),
|
|
/CBF16 por duodécimos/,
|
|
);
|
|
d.modelo_a_snapshot = saveNormalBook(d, recordNormal(d, back));
|
|
back = normalBook(d);
|
|
assert.equal(
|
|
val(back.result.model.special.acceleration.additionalDeduction),
|
|
10,
|
|
);
|
|
assert.equal(val(back.result.model.special.colecta), 2016);
|
|
back.depreciation.assets[0].start = '2026-10-01';
|
|
assert.equal(normalCurrent(d, back), false);
|
|
});
|
|
void test('malformed optionalmonthly schema cannot overwrite oldbooks', () => {
|
|
for (const value of [
|
|
null,
|
|
{ version: 2, reference: 'QA' },
|
|
{ version: 1, reference: 12 },
|
|
]) {
|
|
const { b } = setup();
|
|
b.special.acceleration.monthly = value;
|
|
assert.throws(() => validateSpecial(b.special), /não recuperável/);
|
|
}
|
|
});
|
|
void test('newmonthlycases only prepare guardedprivateAPI inputs and reports', () => {
|
|
const cases = {
|
|
monthly: setup(),
|
|
fraction: setup('20', '2026-12-15'),
|
|
missing: setup(),
|
|
negative: setup(),
|
|
absent: setup(),
|
|
};
|
|
cases.missing.b.special.acceleration.monthly.reference = '';
|
|
cases.negative.b.special.acceleration.rows[0].eligible = 'no';
|
|
cases.absent.b.special.acceleration.qualification = q('absent');
|
|
cases.absent.b.special.acceleration.rows = [];
|
|
fs.mkdirSync('outputs/irpc-l7-months', { recursive: true });
|
|
for (const [name, { d, b }] of Object.entries(cases)) {
|
|
d.modelo_a_snapshot = saveNormalBook(
|
|
d,
|
|
recordNormal(d, b, '2026-10-26T16:00:00+02:00'),
|
|
);
|
|
fs.writeFileSync(
|
|
'outputs/irpc-l7-months/' + name + '.json',
|
|
JSON.stringify(d),
|
|
);
|
|
fs.writeFileSync(
|
|
'outputs/irpc-l7-months/' + name + '.html',
|
|
normalHtml(d),
|
|
);
|
|
}
|
|
});
|