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, ); });