Files
Investidor-V2/tests/frontend/irpc-normal-depreciation.test.mjs
T

566 lines
19 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 {
newNormalDepreciation,
newDepreciationAsset,
reuseNormalAssets,
} from '../../resources/js/lib/irpc-normal-depreciation.ts';
import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts';
const seed = () =>
JSON.parse(
fs.readFileSync(
new URL(
'./fixtures/irpc-costs-preserved.json',
import.meta.url,
),
'utf8',
),
),
val = (f) => (f ? Number(f.numerator) / Number(f.denominator) : null);
function asset(p = {}) {
return {
...newDepreciationAsset('server'),
assetKey: 'asset-server',
description: 'Servidor QA',
document: 'QA-activo-servidor',
line: '1',
category: 'server',
valuation: 'historical_new',
specificRate: 'no',
method: 'constant',
rate: '20',
timing: 'annual',
start: '2026-01-01',
exitState: 'absent',
cost: '500',
residual: '0',
accounting: '120',
previousAccounting: '0',
previousFiscal: '0',
priorAvailable: '0',
previousReference:
'QA ausência de quotas anteriores demonstrada,ou saldos anteriores conforme documentos',
consistentMethod: 'yes',
conditions: 'yes',
reference:
'QA factura/classificação/base histórica/período documentados,sem reavaliações ou regimes especiais',
...p,
};
}
function setup(a = asset(), bookExpense = a.accounting) {
const d = seed(),
b = normalBook(d),
p = JSON.parse(d.business_plan_snapshot);
p.plan.assetDepreciation = bookExpense;
d.business_plan_snapshot = JSON.stringify(p);
b.depreciation = {
...newNormalDepreciation(),
completenessReference:
'QA todos activos/quotas e plano reconciliados no sujeito/exercício2026',
assets: [a],
};
return { d, b };
}
const model = (a, expense) => {
const { d, b } = setup(a, expense);
return calculateNormal(d, b);
};
void test('version7 reuses existing server and replaces20 legacy adjustment exactly once without changing financial result or old maps', () => {
const { d, b } = setup(),
m = calculateNormal(d, b);
assert.equal(val(m.accountingProfit), 6240);
assert.equal(val(m.depreciation.adjustment), 20);
assert.equal(val(m.taxableProfit), 6730);
assert.equal(val(m.taxableBase), 4430);
assert.equal(val(m.colecta), 1417.6);
assert.equal(m.version, 'IRPC-normal-adjustments/7');
assert.equal(val(m.schedules.net), 520);
assert.deepEqual(m.schedules.assets, []);
assert.deepEqual(m.schedules.input.assets, b.schedules.assets);
assert.equal(m.tax, null);
});
void test('documented first-year duodecimos include July and exclude month of exit', () => {
let m = model(asset({ start: '2026-07-15', timing: 'months' }));
assert.equal(m.depreciation.assets[0].months, 6);
assert.equal(val(m.depreciation.assets[0].deduction), 50);
assert.equal(val(m.taxableProfit), 6780);
assert.equal(val(m.taxableBase), 4480);
assert.equal(val(m.colecta), 1433.6);
m = model(
asset({
start: '2026-07-15',
timing: 'months',
exitState: 'known',
exitDate: '2026-10-15',
}),
);
assert.equal(m.depreciation.assets[0].months, 3);
assert.equal(val(m.depreciation.assets[0].deduction), 25);
assert.equal(val(m.colecta), 1441.6);
});
void test('annual first-year option remains distinct from duodecimos; contradictory dates and unknown event are not defaulted', () => {
assert.equal(
val(
model(asset({ start: '2026-07-15' })).depreciation.assets[0]
.deduction,
),
100,
);
for (const p of [
{ exitState: '' },
{ exitState: 'known', exitDate: '' },
{ exitState: 'absent', exitDate: '2026-10-15' },
{ start: '2026-02-31' },
{ exitState: 'known', exitDate: '2025-12-31' },
])
assert.equal(model(asset(p)).colecta, null);
});
void test('declining method uses fiscal remaining basis and coefficient capped by minimum life; result differs from accounting depreciation', () => {
const m = model(
asset({
method: 'declining',
factor: '2',
newTangible: 'yes',
switchToStraight: 'no',
accounting: '240',
}),
);
assert.equal(val(m.depreciation.assets[0].quota), 200);
assert.equal(val(m.depreciation.assets[0].deduction), 200);
assert.equal(val(m.depreciation.adjustment), 40);
assert.equal(val(m.taxableProfit), 6630);
assert.equal(val(m.taxableBase), 4330);
assert.equal(val(m.colecta), 1385.6);
for (const p of [
{ factor: '2.1' },
{ newTangible: 'no' },
{ category: 'furniture', factor: '2' },
{ category: 'commercial_building', factor: '2' },
])
assert.equal(
model(
asset({
method: 'declining',
factor: '2',
newTangible: 'yes',
switchToStraight: 'no',
...p,
}),
).colecta,
null,
);
});
void test('coefficient bands1.5/2/2.5 and half-to-maximum constant-rate recorte are applied by catalog life', () => {
const c = model(
asset({
category: 'computer',
method: 'declining',
factor: '1.5',
newTangible: 'yes',
switchToStraight: 'no',
accounting: '200',
}),
);
assert.equal(val(c.depreciation.assets[0].quota), 187.5);
assert.equal(
model(
asset({
category: 'computer',
method: 'declining',
factor: '2',
newTangible: 'yes',
switchToStraight: 'no',
}),
).colecta,
null,
);
const m = model(
asset({
category: 'laundry_machine',
specificRate: 'yes',
method: 'declining',
factor: '2.5',
newTangible: 'yes',
switchToStraight: 'no',
accounting: '200',
}),
);
assert.equal(val(m.depreciation.assets[0].quota), 156.25);
assert.equal(
val(model(asset({ rate: '10' })).depreciation.assets[0].deduction),
50,
);
assert.equal(model(asset({ rate: '9.99' })).colecta, null);
});
void test('previous taxable accounting quotas are only recovered within current quota and documented available balance', () => {
const m = model(
asset({
start: '2025-01-01',
accounting: '80',
previousAccounting: '120',
previousFiscal: '80',
priorAvailable: '40',
}),
);
assert.equal(val(m.depreciation.assets[0].deduction), 100);
assert.equal(val(m.depreciation.assets[0].priorUsed), 20);
assert.equal(val(m.depreciation.assets[0].priorRemaining), 20);
assert.equal(val(m.depreciation.adjustment), -20);
assert.equal(val(m.taxableProfit), 6730);
assert.equal(m.depreciation.input.assets[0].priorAvailable, '40');
for (const p of [
{ priorAvailable: '' },
{ priorAvailable: '41' },
{ previousFiscal: '121' },
{ previousReference: '' },
])
assert.equal(
model(
asset({
start: '2025-01-01',
accounting: '80',
previousAccounting: '120',
previousFiscal: '80',
priorAvailable: '40',
...p,
}),
).colecta,
null,
);
});
void test('unrecorded quotas are not invented as a deduction; zero previous balance remains explicit', () => {
const m = model(asset({ accounting: '0' }));
assert.equal(val(m.depreciation.assets[0].quota), 100);
assert.equal(val(m.depreciation.assets[0].deduction), 0);
assert.equal(val(m.depreciation.adjustment), 0);
assert.equal(model(asset({ previousAccounting: '' })).colecta, null);
});
void test('article8-2 option requires documented past condition and remaining minimum life,not an automatic switch', () => {
const a = asset({
start: '2022-01-01',
method: 'declining',
factor: '2',
newTangible: 'yes',
switchToStraight: 'yes',
priorFullQuotas: 'yes',
previousAccounting: '500',
previousFiscal: '420',
priorAvailable: '80',
accounting: '0',
}),
m = model(a);
assert.equal(val(m.depreciation.assets[0].quota), 80);
assert.equal(val(m.depreciation.assets[0].deduction), 80);
assert.equal(val(m.depreciation.assets[0].priorUsed), 80);
assert.equal(model({ ...a, priorFullQuotas: '' }).colecta, null);
assert.equal(model({ ...a, priorFullQuotas: 'no' }).colecta, null);
});
void test('specific tableI prevails over generic tableII and known codes are never replaced with a free rate', () => {
const m = model(
asset({
category: 'hotel_decoration',
specificRate: 'yes',
rate: '20',
}),
);
assert.equal(m.depreciation.assets[0].catalog.table, 'I');
assert.equal(m.depreciation.assets[0].catalog.code, '84.2/B');
assert.equal(val(m.depreciation.assets[0].quota), 100);
assert.equal(model(asset({ specificRate: 'yes' })).colecta, null);
assert.equal(model(asset({ category: 'other' })).colecta, null);
assert.equal(
model(asset({ category: 'hotel_decoration', specificRate: 'no' }))
.colecta,
null,
);
assert.equal(model(asset({ valuation: 'used' })).colecta, null);
assert.equal(model(asset({ valuation: '' })).colecta, null);
});
void test('commercial building excludes separately known land; zero land is not presumed when missing', () => {
const m = model(
asset({
category: 'commercial_building',
rate: '2',
cost: '1000',
land: '200',
accounting: '30',
}),
);
assert.equal(val(m.depreciation.assets[0].basis), 800);
assert.equal(val(m.depreciation.assets[0].deduction), 16);
assert.equal(val(m.depreciation.adjustment), 14);
assert.equal(
model(asset({ category: 'commercial_building', rate: '2', land: '' }))
.colecta,
null,
);
});
void test('vehicle ceiling800000 and non-eligible accounting share are separate from prior recoverable quotas', () => {
let m = model(
asset({
category: 'passenger_vehicle',
rate: '25',
vehicleException: 'no',
cost: '900000',
accounting: '225000',
}),
);
assert.equal(val(m.depreciation.assets[0].taxBasis), 800000);
assert.equal(val(m.depreciation.assets[0].deduction), 200000);
assert.equal(val(m.depreciation.adjustment), 25000);
m = model(
asset({
category: 'passenger_vehicle',
rate: '25',
vehicleException: 'no',
cost: '900000',
accounting: '9000',
}),
);
assert.equal(val(m.depreciation.assets[0].eligibleCurrent), 8000);
assert.equal(val(m.depreciation.assets[0].deduction), 8000);
assert.equal(
model(
asset({
category: 'passenger_vehicle',
rate: '25',
vehicleException: '',
}),
).colecta,
null,
);
assert.equal(
model(
asset({
category: 'passenger_vehicle',
rate: '25',
vehicleException: 'no',
cost: '900000',
start: '2025-01-01',
previousAccounting: '225000',
previousFiscal: '200000',
priorAvailable: '25000',
}),
).colecta,
null,
);
});
void test('low value is an explicit individual-asset option,not automatic just because cost is below20000', () => {
const m = model(
asset({ method: 'low_value', individual: 'yes', accounting: '500' }),
);
assert.equal(val(m.depreciation.assets[0].deduction), 500);
assert.equal(val(m.colecta), 1289.6);
assert.equal(
model(asset({ method: 'low_value', individual: 'no' })).colecta,
null,
);
assert.equal(
model(
asset({
category: 'other',
method: 'low_value',
individual: 'yes',
}),
).colecta,
null,
);
assert.equal(
model(
asset({ method: 'low_value', individual: 'yes', cost: '20000.01' }),
).colecta,
null,
);
});
void test('software conditions and explicit nondepreciable category have distinct outcomes', () => {
assert.equal(
model(asset({ category: 'software', rate: '25', limitedRights: '' }))
.colecta,
null,
);
assert.equal(
val(
model(
asset({
category: 'software',
rate: '25',
limitedRights: 'yes',
}),
).depreciation.assets[0].deduction,
),
120,
);
const m = model(
asset({
category: 'nondepreciable',
start: '',
method: '',
previousAccounting: '',
}),
);
assert.equal(val(m.depreciation.assets[0].deduction), 0);
assert.equal(val(m.depreciation.adjustment), 120);
});
void test('maximum life exhaustion excludes current quota; first-use future period is known nondeduction', () => {
const m = model(
asset({
start: '2014-01-01',
previousAccounting: '400',
previousFiscal: '400',
priorAvailable: '0',
}),
);
assert.equal(val(m.depreciation.assets[0].deduction), 0);
assert.match(m.depreciation.assets[0].reason, /vida útil máxima esgotada/);
assert.equal(
val(
model(asset({ start: '2027-01-01' })).depreciation.assets[0]
.deduction,
),
0,
);
assert.equal(
model(asset({ category: 'computer', rate: '25', start: '2018-07-01' }))
.colecta,
null,
);
const clipped = model(
asset({
category: 'computer',
rate: '25',
start: '2018-07-01',
timing: 'months',
}),
);
assert.equal(clipped.depreciation.assets[0].months, 6);
});
void test('duplicate asset/document and unreconciled accounting cannot create a second depreciation adjustment', () => {
const { d, b } = setup();
b.depreciation.assets.push({ ...asset({ id: 'second' }), accounting: '0' });
assert.equal(calculateNormal(d, b).colecta, null);
const c = setup();
c.b.depreciation.assets[0].document = c.b.rows[0].document;
c.b.depreciation.assets[0].line = c.b.rows[0].line;
assert.equal(calculateNormal(c.d, c.b).colecta, null);
assert.equal(model(asset(), '121').colecta, null);
});
void test('one unknown asset keeps independent quotas visible but total profit/base/colecta dependent on reconciliation remains unknown', () => {
const { d, b } = setup(asset({ accounting: '60' }), '120');
b.depreciation.assets.push(
asset({
id: 'second',
assetKey: 'second',
document: 'QA-second',
category: 'other',
accounting: '60',
}),
);
const m = calculateNormal(d, b);
assert.equal(val(m.depreciation.assets[0].deduction), 60);
assert.equal(m.depreciation.assets[1].deduction, null);
assert.equal(m.taxableProfit, null);
assert.equal(m.taxableBase, null);
assert.equal(m.colecta, null);
});
void test('reuse copies declared values without inventing previous accounting/carry data; installed6 stays current and histories survive7', () => {
const d = seed(),
old = normalBook(d);
assert.equal(normalCurrent(d, old), true);
assert.equal(old.result.model.version, 'IRPC-normal-adjustments/6');
const reused = reuseNormalAssets(old.schedules.assets);
assert.equal(reused.assets[0].cost, old.schedules.assets[0].cost);
assert.equal(reused.assets[0].previousAccounting, '');
assert.equal(reused.assets[0].priorAvailable, '');
const { d: next, b } = setup(
asset({ start: '2026-07-15', timing: 'months' }),
),
before = JSON.parse(next.modelo_a_snapshot);
const oldAssets = structuredClone(b.schedules.assets);
next.modelo_a_snapshot = saveNormalBook(
next,
recordNormal(next, b, '2026-10-26T10:00:00+02:00'),
);
const back = normalBook(JSON.parse(JSON.stringify(next)));
assert.equal(normalCurrent(next, back), true);
assert.deepEqual(back.history.at(-1), old.result);
assert.deepEqual(back.schedules.assets, oldAssets);
assert.deepEqual(JSON.parse(next.modelo_a_snapshot).books, before.books);
assert.deepEqual(
JSON.parse(next.modelo_a_snapshot).irpcWithholdings,
before.irpcWithholdings,
);
assert.match(normalHtml(next), /1433.60/);
assert.match(
JSON.stringify(
createProjectDossierReport(next, {
selectedSections: new Set(['finance']),
}),
),
/quota anterior/,
);
const changed = structuredClone(back);
changed.depreciation.assets[0].priorAvailable = '1';
next.modelo_a_snapshot = saveNormalBook(next, changed);
assert.equal(normalCurrent(next, normalBook(next)), false);
assert.throws(() => normalHtml(next));
});
void test('recoverable scenarios generate five API/report proofs without recalculating accepted predecessor suites', () => {
const cases = {
months: setup(asset({ start: '2026-07-15', timing: 'months' })),
declining: setup(
asset({
method: 'declining',
factor: '2',
newTangible: 'yes',
switchToStraight: 'no',
accounting: '240',
}),
),
prior: setup(
asset({
start: '2025-01-01',
accounting: '80',
previousAccounting: '120',
previousFiscal: '80',
priorAvailable: '40',
}),
),
specific: setup(
asset({
category: 'laundry_machine',
specificRate: 'yes',
rate: '12.5',
}),
),
missing: setup(asset({ previousAccounting: '' })),
};
fs.mkdirSync('outputs/irpc-l6-depreciation', { recursive: true });
for (const [name, { d, b }] of Object.entries(cases)) {
d.modelo_a_snapshot = saveNormalBook(
d,
recordNormal(d, b, '2026-10-26T10:00:00+02:00'),
);
fs.writeFileSync(
'outputs/irpc-l6-depreciation/' + name + '.json',
JSON.stringify(d),
);
fs.writeFileSync(
'outputs/irpc-l6-depreciation/' + name + '.html',
normalHtml(d),
);
}
assert.equal(
val(normalBook(cases.specific.d).result.model.colecta),
1429.6,
);
});