358 lines
14 KiB
JavaScript
358 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 { fraction } from '../../resources/js/lib/fiscal-rational.ts';
|
||
import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts';
|
||
const read = (name) =>
|
||
JSON.parse(
|
||
fs.readFileSync(new URL('./fixtures/' + name, 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-start-preserved.json');
|
||
d.business_plan_snapshot = f.plan;
|
||
d.modelo_a_snapshot = saveNormalBook(d, f.book);
|
||
return { d, b: normalBook(d) };
|
||
}
|
||
function setup(period = 'cessation', prior = true) {
|
||
const { d, b } = accepted(),
|
||
months = period === 'annual' ? 12 : 6,
|
||
p = JSON.parse(d.business_plan_snapshot);
|
||
p.plan.assetDepreciation = '120';
|
||
d.business_plan_snapshot = JSON.stringify(p);
|
||
if (period === 'annual') delete b.periodsFx;
|
||
else
|
||
Object.assign(b.periodsFx, {
|
||
kind: 'cessation',
|
||
from: '2026-01-01',
|
||
to: '2026-06-30',
|
||
reference:
|
||
'QA cessação documentada30Junho2026;meses completos sem transmissão presumida',
|
||
});
|
||
b.schedules.interest.amount = String(months * 20);
|
||
Object.assign(b.depreciation.assets[0], {
|
||
start: prior ? '2025-01-01' : '2026-01-15',
|
||
timing: 'months',
|
||
accounting: String(months * 10),
|
||
previousAccounting: prior ? '150' : '0',
|
||
previousFiscal: prior ? '100' : '0',
|
||
priorAvailable: prior ? '50' : '0',
|
||
previousReference: prior
|
||
? 'QA quotas contabilísticas150,fiscais100,parcela50 temporariamente tributada;sem encargos permanentemente excluídos'
|
||
: 'QA ausência declarada de quotas anteriores;activo entrou em2026',
|
||
exitState: 'absent',
|
||
exitDate: '',
|
||
});
|
||
for (const part of [
|
||
b.special.expenses,
|
||
b.special.cooperative,
|
||
b.special.acceleration,
|
||
])
|
||
Object.assign(part.qualification, {
|
||
from: '2026-01-01',
|
||
to: period === 'annual' ? '2026-12-31' : '2026-06-30',
|
||
periodReference:
|
||
period === 'annual'
|
||
? 'QA ano civil2026'
|
||
: b.periodsFx.reference,
|
||
});
|
||
delete b.special.acceleration.monthly.startPeriod;
|
||
b.special.acceleration.monthly.continuation = {
|
||
version: 1,
|
||
period,
|
||
reference:
|
||
'QA CBF16 vigente no sujeito/projecto e período2026 declarado;sem cumulação',
|
||
historyReference:
|
||
'QA activo novo2025/2026,método constante e duodécimos desde entrada;quotas anteriores reconciliadas no mapaL6,não recalculadas',
|
||
monthlyFromStart: 'yes',
|
||
};
|
||
return { d, b };
|
||
}
|
||
|
||
void test('accepted12 result,key and fullhistory remain exact without continuation', () => {
|
||
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('cessation reuses6 months without assuming assetexit;50 normal,75 accelerated capped60,additional10', () => {
|
||
const { d, b } = setup('cessation', false),
|
||
asset = JSON.stringify(b.depreciation),
|
||
period = JSON.stringify(b.periodsFx),
|
||
m = calculateNormal(d, b),
|
||
s = m.special,
|
||
a = s.acceleration.monthly.assets[0];
|
||
assert.equal(m.version, 'IRPC-normal-adjustments/13');
|
||
assert.equal(a.months, 6);
|
||
assert.equal(a.exit.state, 'absent');
|
||
assert.equal(val(a.normalQuota), 50);
|
||
assert.equal(val(a.acceleratedQuota), 75);
|
||
assert.equal(val(a.acceleratedDeduction), 60);
|
||
assert.equal(val(a.additionalDeduction), 10);
|
||
assert.equal(val(m.taxableProfit), 3130);
|
||
assert.equal(val(m.colecta), 1001.6);
|
||
assert.equal(val(s.taxableProfit), 3120);
|
||
assert.equal(val(s.taxableBase), 3120);
|
||
assert.equal(val(s.colecta), 998.4);
|
||
assert.equal(JSON.stringify(b.depreciation), asset);
|
||
assert.equal(JSON.stringify(b.periodsFx), period);
|
||
assert.equal(s.finalBalance.payable, null);
|
||
});
|
||
|
||
void test('cessation with priorbook150,fiscal100,available50 recovers15 once and adds only25 to normaldeduction', () => {
|
||
const { d, b } = setup(),
|
||
m = calculateNormal(d, b),
|
||
s = m.special,
|
||
a = s.acceleration.monthly.assets[0];
|
||
assert.equal(a.months, 6);
|
||
assert.equal(val(a.normalDeduction), 50);
|
||
assert.equal(val(a.acceleratedDeduction), 75);
|
||
assert.equal(val(a.prior.normalUsed), 0);
|
||
assert.equal(val(a.prior.acceleratedUsed), 15);
|
||
assert.equal(val(a.prior.normalRemaining), 50);
|
||
assert.equal(val(a.prior.acceleratedRemaining), 35);
|
||
assert.equal(val(a.prior.normalFiscalRemaining), 850);
|
||
assert.equal(val(a.prior.acceleratedFiscalRemaining), 825);
|
||
assert.equal(val(s.acceleration.additionalDeduction), 25);
|
||
assert.equal(val(s.taxableProfit), 3105);
|
||
assert.equal(val(s.taxableBase), 3105);
|
||
assert.equal(val(s.colecta), 993.6);
|
||
assert.equal(b.depreciation.assets[0].priorAvailable, '50');
|
||
assert.equal(b.depreciation.assets[0].previousFiscal, '100');
|
||
});
|
||
|
||
void test('annual2026 continuation preserves2025 actualbalances and records30priorused within150 deduction,additional50', () => {
|
||
const { d, b } = setup('annual'),
|
||
m = calculateNormal(d, b),
|
||
s = m.special,
|
||
a = s.acceleration.monthly.assets[0];
|
||
assert.equal(a.months, 12);
|
||
assert.equal(val(a.normalQuota), 100);
|
||
assert.equal(val(a.acceleratedQuota), 150);
|
||
assert.equal(val(a.prior.acceleratedUsed), 30);
|
||
assert.equal(val(a.prior.acceleratedRemaining), 20);
|
||
assert.equal(val(s.acceleration.additionalDeduction), 50);
|
||
assert.equal(val(m.taxableProfit), 6260);
|
||
assert.equal(val(s.taxableProfit), 6210);
|
||
assert.equal(val(s.colecta), 1987.2);
|
||
assert.deepEqual(
|
||
s.acceleration.normal.input,
|
||
s.acceleration.accelerated.input,
|
||
);
|
||
});
|
||
|
||
void test('documented assetexit excludes eventmonth and keeps exact fractional quotas;cessation date does not edit exit', () => {
|
||
const { d, b } = setup();
|
||
Object.assign(b.depreciation.assets[0], {
|
||
exitState: 'known',
|
||
exitDate: '2026-06-20',
|
||
});
|
||
const a = calculateNormal(d, b).special.acceleration.monthly.assets[0];
|
||
assert.equal(a.months, 5);
|
||
assert.deepEqual(a.normalQuota, fraction(125n, 3n));
|
||
assert.deepEqual(a.acceleratedQuota, fraction(125n, 2n));
|
||
assert.deepEqual(a.additionalDeduction, fraction(125n, 6n));
|
||
assert.deepEqual(a.prior.acceleratedUsed, fraction(5n, 2n));
|
||
assert.deepEqual(a.prior.acceleratedRemaining, fraction(95n, 2n));
|
||
});
|
||
|
||
void test('remaining fiscalbalance and accounted/priorcaps bind both calculations;available amount is never a second deduction', () => {
|
||
const { d, b } = setup();
|
||
Object.assign(b.depreciation.assets[0], {
|
||
previousAccounting: '1000',
|
||
previousFiscal: '980',
|
||
priorAvailable: '20',
|
||
});
|
||
let a = calculateNormal(d, b).special.acceleration.monthly.assets[0];
|
||
assert.equal(val(a.normalDeduction), 20);
|
||
assert.equal(val(a.acceleratedDeduction), 20);
|
||
assert.equal(val(a.additionalDeduction), 0);
|
||
assert.equal(val(a.prior.acceleratedFiscalRemaining), 0);
|
||
const second = setup(),
|
||
p = JSON.parse(second.d.business_plan_snapshot);
|
||
p.plan.assetDepreciation = '0';
|
||
second.d.business_plan_snapshot = JSON.stringify(p);
|
||
second.b.depreciation.assets[0].accounting = '0';
|
||
a = calculateNormal(second.d, second.b).special.acceleration.monthly
|
||
.assets[0];
|
||
assert.equal(val(a.normalDeduction), 50);
|
||
assert.equal(val(a.acceleratedDeduction), 50);
|
||
assert.equal(val(a.additionalDeduction), 0);
|
||
assert.equal(val(a.prior.acceleratedRemaining), 0);
|
||
});
|
||
|
||
void test('missing support or missing priorbalance blocks only dependent CBF16;absence and negative remain explicit', () => {
|
||
for (const key of ['reference', 'historyReference', 'monthlyFromStart']) {
|
||
const { d, b } = setup();
|
||
b.special.acceleration.monthly.continuation[key] = '';
|
||
const m = calculateNormal(d, b);
|
||
assert.equal(m.special.acceleration.additionalDeduction, null);
|
||
assert.notEqual(m.colecta, null);
|
||
}
|
||
const missing = setup();
|
||
missing.b.depreciation.assets[0].previousFiscal = '';
|
||
const pending = calculateNormal(missing.d, missing.b);
|
||
assert.equal(pending.special.acceleration.additionalDeduction, null);
|
||
assert.match(pending.depreciation.pending.join(' '), /em falta/);
|
||
assert.equal(missing.b.depreciation.assets[0].previousFiscal, '');
|
||
for (const state of ['negative', 'absent']) {
|
||
const { d, b } = setup();
|
||
if (state === 'negative')
|
||
b.special.acceleration.rows[0].eligible = 'no';
|
||
else {
|
||
b.special.acceleration.rows = [];
|
||
Object.assign(b.special.acceleration.qualification, {
|
||
state: 'absent',
|
||
eligible: '',
|
||
});
|
||
}
|
||
const s = calculateNormal(d, b).special;
|
||
assert.equal(val(s.acceleration.additionalDeduction), 0);
|
||
assert.equal(
|
||
s.acceleration.status,
|
||
state === 'absent' ? 'absent' : 'calculated',
|
||
);
|
||
if (state === 'negative')
|
||
assert.equal(s.acceleration.rows[0].status, 'not_applicable');
|
||
}
|
||
});
|
||
|
||
void test('otherstartyears,nonmonthlyhistory,transition,cumulation and prioramounts for currentyearentry stay out of scope', () => {
|
||
for (const mutate of [
|
||
(b) => (b.depreciation.assets[0].start = '2024-01-01'),
|
||
(b) =>
|
||
(b.special.acceleration.monthly.continuation.monthlyFromStart =
|
||
'no'),
|
||
(b) => (b.periodsFx.kind = 'transition'),
|
||
(b) => (b.special.acceleration.noOtherBenefits = 'no'),
|
||
(b) => (b.depreciation.assets[0].start = '2026-01-01'),
|
||
]) {
|
||
const { d, b } = setup();
|
||
mutate(b);
|
||
const m = calculateNormal(d, b);
|
||
assert.equal(m.special.acceleration.additionalDeduction, null);
|
||
assert.notEqual(m.colecta, null);
|
||
}
|
||
});
|
||
|
||
void test('save-recover13 preserves12result and allhistorical balances;recalc idempotent and reports reconciliation without officialconsumption', () => {
|
||
const { d, b } = setup(),
|
||
history = structuredClone(b.history),
|
||
old = structuredClone(b.result),
|
||
asset = JSON.stringify(b.depreciation),
|
||
plan = d.business_plan_snapshot,
|
||
others = 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/12');
|
||
assert.equal(JSON.stringify(back.depreciation), asset);
|
||
assert.equal(d.business_plan_snapshot, plan);
|
||
assert.deepEqual(JSON.parse(d.modelo_a_snapshot).irpcWithholdings, others);
|
||
const html = normalHtml(d);
|
||
assert.match(html, /2026-01-01–2026-06-30/);
|
||
assert.match(html, /recuperada CBF16:15.00/);
|
||
assert.match(html, /saldo anterior aritmético CBF16:35.00/);
|
||
assert.match(html, /sem consumo de saldos oficiais/);
|
||
assert.match(html, /colecta sobre a base:993.60/);
|
||
assert.match(html, /Saldo final a pagar\/recuperar:Indeterminado/);
|
||
assert.match(
|
||
JSON.stringify(
|
||
createProjectDossierReport(d, {
|
||
selectedSections: new Set(['finance']),
|
||
}),
|
||
),
|
||
/quotas contabilísticas anteriores/,
|
||
);
|
||
d.modelo_a_snapshot = saveNormalBook(d, recordNormal(d, back));
|
||
back = normalBook(d);
|
||
assert.equal(
|
||
val(back.result.model.special.acceleration.additionalDeduction),
|
||
25,
|
||
);
|
||
assert.equal(back.depreciation.assets[0].priorAvailable, '50');
|
||
back.depreciation.assets[0].previousFiscal = '110';
|
||
assert.equal(normalCurrent(d, back), false);
|
||
});
|
||
|
||
void test('invalid or overlapping optionalpreparation cannot replace preservedbooks', () => {
|
||
for (const c of [
|
||
null,
|
||
{
|
||
version: 2,
|
||
period: 'annual',
|
||
reference: 'QA',
|
||
historyReference: 'QA',
|
||
monthlyFromStart: 'yes',
|
||
},
|
||
{
|
||
version: 1,
|
||
period: 'other',
|
||
reference: 'QA',
|
||
historyReference: 'QA',
|
||
monthlyFromStart: 'yes',
|
||
},
|
||
]) {
|
||
const { b } = setup();
|
||
b.special.acceleration.monthly.continuation = c;
|
||
assert.throws(() => validateSpecial(b.special), /não recuperável/);
|
||
}
|
||
const { b } = setup();
|
||
b.special.acceleration.monthly.startPeriod = {
|
||
version: 1,
|
||
reference: 'QA',
|
||
};
|
||
assert.throws(() => validateSpecial(b.special), /incompatível/);
|
||
});
|
||
|
||
void test('prepare only compatible new guardedAPI cases with missing,negative and absence variants', () => {
|
||
const cases = {
|
||
cessation: setup('cessation', false),
|
||
prior: setup(),
|
||
annual: setup('annual'),
|
||
exit: setup(),
|
||
missing: setup(),
|
||
negative: setup(),
|
||
absent: setup(),
|
||
};
|
||
Object.assign(cases.exit.b.depreciation.assets[0], {
|
||
exitState: 'known',
|
||
exitDate: '2026-06-20',
|
||
});
|
||
cases.missing.b.special.acceleration.monthly.continuation.historyReference =
|
||
'';
|
||
cases.negative.b.special.acceleration.rows[0].eligible = 'no';
|
||
cases.absent.b.special.acceleration.rows = [];
|
||
Object.assign(cases.absent.b.special.acceleration.qualification, {
|
||
state: 'absent',
|
||
eligible: '',
|
||
});
|
||
fs.mkdirSync('outputs/irpc-l7-cessation-prior', { recursive: true });
|
||
for (const [name, { d, b }] of Object.entries(cases)) {
|
||
d.modelo_a_snapshot = saveNormalBook(
|
||
d,
|
||
recordNormal(d, b, '2026-10-26T18:00:00+02:00'),
|
||
);
|
||
fs.writeFileSync(
|
||
'outputs/irpc-l7-cessation-prior/' + name + '.json',
|
||
JSON.stringify(d),
|
||
);
|
||
fs.writeFileSync(
|
||
'outputs/irpc-l7-cessation-prior/' + name + '.html',
|
||
normalHtml(d),
|
||
);
|
||
}
|
||
});
|