346 lines
14 KiB
JavaScript
346 lines
14 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 { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts';
|
||
const read = (n) =>
|
||
JSON.parse(
|
||
fs.readFileSync(new URL('./fixtures/' + n, import.meta.url), 'utf8'),
|
||
);
|
||
const val = (v) => (v ? Number(v.numerator) / Number(v.denominator) : null);
|
||
function accepted() {
|
||
const d = read('irpc-l7-preserved.json').acceleration,
|
||
f = read('irpc-l7-continuation-preserved.json');
|
||
d.business_plan_snapshot = f.plan;
|
||
d.modelo_a_snapshot = saveNormalBook(d, f.book);
|
||
return { d, b: normalBook(d) };
|
||
}
|
||
function setup(expense = false) {
|
||
const { d, b } = accepted();
|
||
const i = b.special;
|
||
Object.assign(i.cooperative, {
|
||
kind: 'agrarian',
|
||
allIncomeEligible: 'yes',
|
||
withheldIncome: 'absent',
|
||
});
|
||
Object.assign(i.cooperative.qualification, {
|
||
state: 'known',
|
||
eligible: 'yes',
|
||
reference:
|
||
'QA cooperativa agrária12,base ordinária integralmente elegível;sem rendimentos sujeitos a retenção',
|
||
});
|
||
i.acceleration.rows[0].kind = 'agroindustrial_equipment';
|
||
i.combinations = {
|
||
version: 1,
|
||
reference:
|
||
'QA mesmo sujeito/projecto/período;reduçãoCIRPC12 eCBF16 genérico,efeitos em etapas distintas',
|
||
specificState: 'absent',
|
||
specificReference:
|
||
'QA sem estatuto/benefício específicoCBF20–48 neste projecto/período;CBF13',
|
||
specificException: '',
|
||
otherState: 'absent',
|
||
otherReference:
|
||
'QA sem outros benefícios além dos tratamentosL7 registados',
|
||
};
|
||
if (expense) {
|
||
const old = normalBook(read('irpc-l7-preserved.json').expense);
|
||
b.schedules.rows.push(structuredClone(old.schedules.rows[0]));
|
||
b.profit.topics.expenseCorrections = structuredClone(
|
||
old.profit.topics.expenseCorrections,
|
||
);
|
||
i.expenses = structuredClone(old.special.expenses);
|
||
Object.assign(i.expenses.qualification, {
|
||
from: b.periodsFx.from,
|
||
to: b.periodsFx.to,
|
||
});
|
||
}
|
||
return { d, b };
|
||
}
|
||
|
||
void test('accepted13 model,key and allhistorical balances reproduce exactly without optionalcombination', () => {
|
||
const { d, b } = accepted();
|
||
assert.equal(normalCurrent(d, b), true);
|
||
assert.deepEqual(calculateNormal(d, b), b.result.model);
|
||
assert.deepEqual(normalBook(d).history, b.history);
|
||
});
|
||
|
||
void test('qualified cooperative12 and genericCBF16 combine in order,additional25 once then16percent,prior15 already included', () => {
|
||
const { d, b } = setup(),
|
||
assets = JSON.stringify(b.depreciation),
|
||
m = calculateNormal(d, b),
|
||
s = m.special,
|
||
c = s.combinations;
|
||
assert.equal(m.version, 'IRPC-normal-adjustments/14');
|
||
assert.equal(c.status, 'compatible');
|
||
assert.equal(val(m.taxableProfit), 3130);
|
||
assert.equal(val(m.colecta), 1001.6);
|
||
assert.equal(val(s.acceleration.additionalDeduction), 25);
|
||
assert.equal(val(s.taxableProfit), 3105);
|
||
assert.equal(val(s.taxableBase), 3105);
|
||
assert.equal(val(s.cooperative.rate), 16);
|
||
assert.equal(val(s.colecta), 496.8);
|
||
assert.equal(val(s.combinedColecta), 496.8);
|
||
assert.equal(val(c.assetClaims[0].acceleratedDeduction), 75);
|
||
assert.equal(val(c.assetClaims[0].priorRecoveredIncluded), 15);
|
||
assert.equal(val(c.assetClaims[0].additionalDeduction), 25);
|
||
assert.equal(JSON.stringify(b.depreciation), assets);
|
||
assert.equal(s.finalBalance.payable, null);
|
||
assert.doesNotMatch(s.acceleration.source, /Sem cumulação[.]/);
|
||
});
|
||
|
||
void test('autonomousexpense35percent is not halved by cooperative rate or added again to the taxableprofit', () => {
|
||
const { d, b } = setup(true),
|
||
m = calculateNormal(d, b),
|
||
s = m.special;
|
||
assert.equal(val(m.taxableProfit), 3230);
|
||
assert.equal(val(s.taxableProfit), 3205);
|
||
assert.equal(val(s.taxableBase), 3205);
|
||
assert.equal(val(s.expenses.rows[0].l6Adjustment), 100);
|
||
assert.equal(val(s.expenses.rows[0].additionalProfitAdjustment), 0);
|
||
assert.equal(val(s.expenses.autonomousTax), 35);
|
||
assert.equal(val(s.colecta), 512.8);
|
||
assert.equal(val(s.combinedColecta), 547.8);
|
||
assert.equal(val(s.combinations.limits.autonomousRate), 35);
|
||
});
|
||
|
||
void test('specificCBF22 with generic16 and declaredabsence of exception is incompatible,neither benefit automatically selected', () => {
|
||
const { d, b } = setup();
|
||
Object.assign(b.special.combinations, {
|
||
specificState: 'known',
|
||
specificReference:
|
||
'QA CBF22 benefício específico infra-estruturas declarado no mesmo projecto/período',
|
||
specificException: 'absent',
|
||
});
|
||
const input = structuredClone(b.special),
|
||
m = calculateNormal(d, b),
|
||
s = m.special;
|
||
assert.equal(s.combinations.status, 'incompatible');
|
||
assert.equal(s.acceleration.additionalDeduction, null);
|
||
assert.equal(s.taxableProfit, null);
|
||
assert.equal(s.colecta, null);
|
||
assert.equal(s.combinedColecta, null);
|
||
assert.equal(val(m.colecta), 1001.6);
|
||
assert.deepEqual(b.special, input);
|
||
assert.match(s.pending.join(' '), /IncompatibilidadeCBF4\/13/);
|
||
});
|
||
|
||
void test('claimedexception,unknownspecificpresence,otherbenefit or missingsupport remain indeterminate without provisionalrate', () => {
|
||
for (const patch of [
|
||
{
|
||
specificState: 'known',
|
||
specificReference:
|
||
'QA excepção invocada,artigo/limite não correspondidos',
|
||
specificException: 'claimed',
|
||
},
|
||
{ specificState: '' },
|
||
{
|
||
otherState: 'known',
|
||
otherReference: 'QA outro benefício fora dos ramos calculados',
|
||
},
|
||
{ reference: '' },
|
||
{ specificReference: '' },
|
||
{ otherReference: '' },
|
||
]) {
|
||
const { d, b } = setup();
|
||
Object.assign(b.special.combinations, patch);
|
||
const m = calculateNormal(d, b);
|
||
assert.equal(m.special.combinations.status, 'indeterminate');
|
||
assert.equal(m.special.colecta, null);
|
||
assert.equal(val(m.colecta), 1001.6);
|
||
}
|
||
});
|
||
|
||
void test('missingcooperativeperiod or mixedincome blocks its dependenttotal without choosing eligibility from location', () => {
|
||
for (const patch of [
|
||
{ allIncomeEligible: 'no' },
|
||
{ withheldIncome: 'known' },
|
||
]) {
|
||
const { d, b } = setup();
|
||
Object.assign(b.special.cooperative, patch);
|
||
d.province_id = 'QA-ZEE-address';
|
||
const s = calculateNormal(d, b).special;
|
||
assert.equal(s.colecta, null);
|
||
assert.match(s.cooperative.reason, /mistas\/retidas/);
|
||
}
|
||
const { d, b } = setup();
|
||
b.special.cooperative.qualification.from = '2026-02-01';
|
||
assert.equal(calculateNormal(d, b).special.colecta, null);
|
||
});
|
||
|
||
void test('duplicateassetqualification cannot duplicate CBF16 deduction;remainingfiscal20 cap keepsadditionalzero', () => {
|
||
const { d, b } = setup();
|
||
b.special.acceleration.rows.push({
|
||
...b.special.acceleration.rows[0],
|
||
id: 'same-asset-again',
|
||
});
|
||
let s = calculateNormal(d, b).special;
|
||
assert.equal(s.acceleration.additionalDeduction, null);
|
||
assert.match(s.acceleration.reason, /Activo repetido/);
|
||
const cap = setup();
|
||
Object.assign(cap.b.depreciation.assets[0], {
|
||
previousAccounting: '1000',
|
||
previousFiscal: '980',
|
||
priorAvailable: '20',
|
||
});
|
||
s = calculateNormal(cap.d, cap.b).special;
|
||
assert.equal(val(s.acceleration.additionalDeduction), 0);
|
||
assert.equal(val(s.combinations.assetClaims[0].acceleratedDeduction), 20);
|
||
assert.equal(val(s.combinations.assetClaims[0].remainingFiscal), 0);
|
||
});
|
||
|
||
void test('negativecooperative or absentCBF16 retains the other independentlyqualified treatment', () => {
|
||
const negative = setup();
|
||
negative.b.special.cooperative.qualification.eligible = 'no';
|
||
let s = calculateNormal(negative.d, negative.b).special;
|
||
assert.equal(s.cooperative.status, 'not_applicable');
|
||
assert.equal(val(s.colecta), 993.6);
|
||
const absent = setup();
|
||
absent.b.special.acceleration.rows = [];
|
||
Object.assign(absent.b.special.acceleration.qualification, {
|
||
state: 'absent',
|
||
eligible: '',
|
||
});
|
||
s = calculateNormal(absent.d, absent.b).special;
|
||
assert.equal(s.acceleration.status, 'absent');
|
||
assert.equal(val(s.acceleration.additionalDeduction), 0);
|
||
assert.equal(val(s.colecta), 500.8);
|
||
});
|
||
|
||
void test('save-recover14 appends13 verbatim,leaves inputbalances/plan/otherbooks unchanged,and report explains order and no double deduction', () => {
|
||
const { d, b } = setup(true),
|
||
old = structuredClone(b.result),
|
||
history = structuredClone(b.history),
|
||
assets = JSON.stringify(b.depreciation),
|
||
plan = d.business_plan_snapshot,
|
||
other = JSON.parse(d.modelo_a_snapshot).irpcWithholdings;
|
||
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, [...history, old]);
|
||
assert.equal(old.model.version, 'IRPC-normal-adjustments/13');
|
||
assert.equal(JSON.stringify(back.depreciation), assets);
|
||
assert.equal(d.business_plan_snapshot, plan);
|
||
assert.deepEqual(JSON.parse(d.modelo_a_snapshot).irpcWithholdings, other);
|
||
const html = normalHtml(d);
|
||
assert.match(html, /Combinação permitida no recorte/);
|
||
assert.match(html, /parcela anterior já incluída na dedução CBF16:15.00/);
|
||
assert.match(html, /única diferença adicional:25.00/);
|
||
assert.match(html, /imposto autónomo independente:35.00/);
|
||
assert.match(html, /colecta combinada:547.80/);
|
||
assert.match(html, /Saldo final a pagar\/recuperar:Indeterminado/);
|
||
assert.match(
|
||
JSON.stringify(
|
||
createProjectDossierReport(d, {
|
||
selectedSections: new Set(['finance']),
|
||
}),
|
||
),
|
||
/Cumulações — qualificação/,
|
||
);
|
||
d.modelo_a_snapshot = saveNormalBook(d, recordNormal(d, back));
|
||
back = normalBook(d);
|
||
assert.equal(val(back.result.model.special.combinedColecta), 547.8);
|
||
assert.equal(back.depreciation.assets[0].priorAvailable, '50');
|
||
back.special.combinations.otherState = 'known';
|
||
assert.equal(normalCurrent(d, back), false);
|
||
});
|
||
|
||
void test('positive unclassified L6benefit is preserved and blocks another deduction until reconciled;zero base never offsets autonomous35', () => {
|
||
const extra = setup();
|
||
extra.b.base.benefits = {
|
||
state: 'known',
|
||
amount: '100',
|
||
reference: 'QA dedução já declarada sem correspondência à combinação',
|
||
};
|
||
let m = calculateNormal(extra.d, extra.b);
|
||
assert.equal(m.special.combinations.status, 'indeterminate');
|
||
assert.equal(m.special.colecta, null);
|
||
assert.notEqual(m.taxableProfit, null);
|
||
assert.equal(extra.b.base.benefits.amount, '100');
|
||
assert.match(m.special.pending.join(' '), /já registado emL6/);
|
||
const loss = setup(true),
|
||
p = JSON.parse(loss.d.business_plan_snapshot);
|
||
p.plan.products[0].unitPrice = '10';
|
||
loss.d.business_plan_snapshot = JSON.stringify(p);
|
||
loss.b.schedules.currentLossReference =
|
||
'QA perda própria do período reconciliada;sem transporte oficial automático';
|
||
m = calculateNormal(loss.d, loss.b);
|
||
assert.ok(val(m.special.taxableProfit) < 0);
|
||
assert.equal(val(m.special.taxableBase), 0);
|
||
assert.equal(val(m.special.colecta), 0);
|
||
assert.equal(val(m.special.expenses.autonomousTax), 35);
|
||
assert.equal(val(m.special.combinedColecta), 35);
|
||
});
|
||
|
||
void test('malformedoptionalcombination cannot overwrite acceptedbooks', () => {
|
||
for (const value of [
|
||
null,
|
||
{ version: 2 },
|
||
{
|
||
version: 1,
|
||
reference: 'QA',
|
||
specificState: 'unknown',
|
||
specificReference: 'QA',
|
||
specificException: '',
|
||
otherState: 'absent',
|
||
otherReference: 'QA',
|
||
},
|
||
]) {
|
||
const { b } = setup();
|
||
b.special.combinations = value;
|
||
assert.throws(() => validateSpecial(b.special), /não recuperável/);
|
||
}
|
||
});
|
||
|
||
void test('prepare only new guardedAPI cases;accepted13 proofs remain separate', () => {
|
||
const cases = {
|
||
combined: setup(),
|
||
autonomous: setup(true),
|
||
incompatible: setup(),
|
||
missing: setup(),
|
||
exception: setup(),
|
||
negative: setup(),
|
||
absent: setup(),
|
||
};
|
||
Object.assign(cases.incompatible.b.special.combinations, {
|
||
specificState: 'known',
|
||
specificReference: 'QA CBF22 no mesmo projecto/período',
|
||
specificException: 'absent',
|
||
});
|
||
cases.missing.b.special.combinations.specificReference = '';
|
||
Object.assign(cases.exception.b.special.combinations, {
|
||
specificState: 'known',
|
||
specificReference:
|
||
'QA excepção expressa invocada sem correspondência de artigo/limites',
|
||
specificException: 'claimed',
|
||
});
|
||
cases.negative.b.special.cooperative.qualification.eligible = 'no';
|
||
cases.absent.b.special.acceleration.rows = [];
|
||
Object.assign(cases.absent.b.special.acceleration.qualification, {
|
||
state: 'absent',
|
||
eligible: '',
|
||
});
|
||
fs.mkdirSync('outputs/irpc-l7-combinations', { 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-combinations/' + name + '.json',
|
||
JSON.stringify(d),
|
||
);
|
||
fs.writeFileSync(
|
||
'outputs/irpc-l7-combinations/' + name + '.html',
|
||
normalHtml(d),
|
||
);
|
||
}
|
||
});
|