27 lines
14 KiB
JavaScript
27 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,normalReportRows,normalFinancialSource}from'../../resources/js/lib/irpc-normal.ts';
|
|
import{newNormalDepreciation,newDepreciationAsset,reuseNormalAssets,calculateNormalDepreciation,depreciationCatalog}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);};
|
|
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);});
|
|
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);});
|
|
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);});
|
|
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);});
|
|
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);});
|
|
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);});
|
|
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);});
|
|
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);});
|
|
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);});
|
|
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);});
|
|
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);});
|
|
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);});
|
|
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);});
|
|
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);});
|
|
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);});
|
|
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);});
|
|
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));});
|
|
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);});
|