29 lines
12 KiB
JavaScript
29 lines
12 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,normalFinancialSource,normalReportRows} from '../../resources/js/lib/irpc-normal.ts';
|
||
import {newNormalSchedules,newScheduleRow,newNormalAsset,calculateSchedules} from '../../resources/js/lib/irpc-normal-schedules.ts';
|
||
import {normalFormFields} from '../../resources/js/lib/irpc-normal-forms.ts';import {fraction} from '../../resources/js/lib/fiscal-rational.ts';
|
||
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
|
||
const seed=()=>JSON.parse(fs.readFileSync(new URL('./fixtures/irpc-assessment-preserved.json',import.meta.url),'utf8'));
|
||
function scenario(){const d=seed(),b=normalBook(d),p=JSON.parse(d.business_plan_snapshot);p.plan.monthlyInterestExpense='20';p.plan.assetDepreciation='120';d.business_plan_snapshot=JSON.stringify(p);b.schedules=newNormalSchedules();const s=b.schedules;s.period={kind:'calendar',from:'2026-01-01',to:'2026-12-31',reference:'QA doze meses do plano correspondem ao ano civil completo'};
|
||
s.interest={amount:'120',reference:'QA contrato bancário, exploração, sem relação especial, limites especiais ou capitalização; pagamentos separados',conditions:'yes'};
|
||
for(const [id,rule,amount,sourceCode,fiscalYear]of[['thirdparty','tax_other','250','operating_expenses',''],['gain','patrimonial_gain','200','',''],['cost','patrimonial_cost','50','',''],['timing','future_cost','100','operating_expenses','2027']])s.rows.push({...newScheduleRow(id),rule,amount,sourceCode,fiscalYear,document:'QA-'+id,line:'1',reference:'QA factos documentados no recorte ordinário, não é operação especial',conditions:'yes'});
|
||
for(const[k,amount]of Object.entries({expenseCorrections:'250',positivePatrimonial:'200',negativePatrimonial:'50',timing:'100'}))b.profit.topics[k]={state:'known',amount,reference:'QA reconciliação integral do tema com os documentos do mapa'};
|
||
b.profit.lines.find(l=>l.code==='depreciation').state='known';s.assetsReference='QA único activo; total contabilístico120 reconciliado com o plano';s.assets=[{...newNormalAsset('server'),category:'server',description:'Servidor QA',cost:'500',residual:'0',rate:'20',accounting:'120',previousFiscal:'0',inUseFrom:'2026-01-01',tableReference:'Decreto72/2013 tabelaII grupo3 item9.1, sem taxa específica da tabelaI',reference:'QA factura500, activo próprio, vida útil fiscal, sem regras especiais',conditions:'yes'}];return{d,b};}
|
||
const val=f=>f?Number(f.numerator)/Number(f.denominator):null;
|
||
test('new rules reconcile financial interest, patrimonial variations, timing and server quota through profit/base/colecta',()=>{const{d,b}=scenario(),m=calculateNormal(d,b);assert.equal(val(m.accountingProfit),6240);assert.equal(val(m.additions),220);assert.equal(val(m.schedules.interest),120);assert.equal(val(m.schedules.depreciation),20);assert.equal(val(m.schedules.net),520);assert.equal(val(m.taxableProfit),6980);assert.equal(val(m.taxableBase),4680);assert.equal(val(m.colecta),1497.6);assert.equal(m.tax,null);assert.equal(m.assessment.finalBalance.payable,null);assert.equal(m.schedules.assets[0].fiscal.numerator,'100');assert.equal(m.version,'IRPC-normal-adjustments/5');});
|
||
test('ordinary interest is qualified independently from compensatory interest and cash payments',()=>{const{d,b}=scenario();b.schedules.interest.amount='119';const m=calculateNormal(d,b);assert.equal(m.taxableProfit,null);assert.equal(val(m.schedules.net),520);assert.equal(m.schedules.interest,null);assert.match(m.profit.pending.join(' '),/Juros financeiros/);const c=scenario();c.b.schedules.rows[0].sourceCode='interest_expense';c.b.schedules.rows[0].amount='20';c.b.profit.topics.expenseCorrections.amount='20';assert.equal(calculateNormal(c.d,c.b).taxableProfit,null);c.b.schedules.interest.amount='100';assert.equal(val(calculateNormal(c.d,c.b).schedules.interest),100);assert.notEqual(calculateNormal(c.d,c.b).taxableProfit,null);});
|
||
test('missing rule evidence blocks dependent totals while independent amounts remain calculated',()=>{const{d,b}=scenario();b.schedules.rows[0].reference='';const m=calculateNormal(d,b);assert.equal(m.taxableProfit,null);assert.equal(m.colecta,null);assert.equal(val(m.schedules.effects.find(e=>e.id==='gain').effect),200);assert.equal(val(m.schedules.depreciation),20);assert.equal(val(m.accountingProfit),6240);});
|
||
test('duplicate documents, over-allocation, wrong source and contradictory topic absence are rejected',()=>{for(const mutate of[b=>{b.schedules.rows[0].document=b.rows[0].document+' ';b.schedules.rows[0].line=b.rows[0].line;},b=>{b.schedules.rows[0].amount='1201';b.profit.topics.expenseCorrections.amount='1201';},b=>b.schedules.rows[0].sourceCode='revenue',b=>b.profit.topics.positivePatrimonial={state:'absent',amount:'',reference:'QA declared absent'}]){const{d,b}=scenario();mutate(b);assert.equal(calculateNormal(d,b).taxableProfit,null);}});
|
||
test('capital contributions and distributions outside result are known amounts with zero tax effect, not ordinary gains/costs',()=>{const{d,b}=scenario();b.schedules.rows.find(r=>r.id==='gain').rule='capital_in';b.schedules.rows.find(r=>r.id==='cost').rule='capital_out';const m=calculateNormal(d,b);assert.equal(val(m.taxableProfit),6830);assert.equal(val(m.schedules.effects.find(e=>e.id==='gain').effect),0);assert.equal(val(m.schedules.effects.find(e=>e.id==='cost').amount),50);});
|
||
test('timing belongs to documented year; previous years and inconsistent mapping stay pending',()=>{for(const y of['','2025','2026']){const{d,b}=scenario();b.schedules.rows.find(r=>r.id==='timing').fiscalYear=y;assert.equal(calculateNormal(d,b).taxableProfit,null);}});
|
||
test('asset quota uses remaining fiscal value; unsupported rates, short-year facts and incomplete reconciliation do not default to accounting',()=>{const{d,b}=scenario();b.schedules.assets[0].previousFiscal='450';let m=calculateNormal(d,b);assert.equal(val(m.schedules.assets[0].fiscal),50);assert.equal(val(m.schedules.depreciation),70);for(const patch of[{rate:'25'},{category:'other'},{inUseFrom:'2026-02-01'},{inUseFrom:'2025-02-31'},{inUseFrom:'2014-01-01'},{accounting:'119'},{conditions:''}]){const{d,b}=scenario();Object.assign(b.schedules.assets[0],patch);m=calculateNormal(d,b);assert.equal(m.taxableProfit,null);assert.equal(m.schedules.depreciation,null);}});
|
||
test('known current loss gives zero base/colecta only with documented current-loss treatment and no self-deduction; no official carryforward created',()=>{const{d,b}=scenario(),p=JSON.parse(d.business_plan_snapshot);p.plan.monthlyOperatingExpenses='1000';d.business_plan_snapshot=JSON.stringify(p);b.base.losses=[];b.base.lossesState='absent';b.base.lossesReference='QA ausência expressa de perdas anteriores';let m=calculateNormal(d,b);assert.equal(val(m.taxableProfit),-3820);assert.equal(m.taxableBase,null);b.schedules.currentLossReference='QA perda fiscal corrente apurada, sem dedução de perdas anteriores ou reporte automático';m=calculateNormal(d,b);assert.equal(val(m.currentLoss),3820);assert.equal(val(m.taxableBase),0);assert.equal(val(m.colecta),0);assert.deepEqual(b.base.losses,[]);assert.equal(normalFormFields(m).fields.find(f=>f.code==='269').value.numerator,'3820');assert.equal(normalFormFields(m).fields.find(f=>f.code==='270').value,null);d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b));fs.mkdirSync('outputs/irpc-l6-sets',{recursive:true});fs.writeFileSync('outputs/irpc-l6-sets/loss.json',JSON.stringify(d));});
|
||
test('EP ordinary annual MZN uses its attributable financial result; unsupported currency/year remains case-specific',()=>{const{d,b}=scenario();b.scope='ep_general';b.scopeReference='QA único EP em Moçambique, imputação completa e exclusiva de custos/proveitos do EP, CIRPC15(1)c';assert.equal(val(calculateNormal(d,b).colecta),1497.6);b.year='2025';assert.equal(calculateNormal(d,b).colecta,null);});
|
||
test('form preparation keeps exact fractions and unmapped fields without provisional codes or fabricated final settlement',()=>{const{d,b}=scenario(),m=calculateNormal(d,b),f=normalFormFields(m);assert.equal(val(f.fields.find(f=>f.code==='278').value),4680);assert.equal(f.fields.find(f=>f.code==='300').transcriptionReady,true);for(const code of['200','302','309–316'])assert.equal(f.fields.find(f=>f.code===code).value,null);assert.equal(f.m20.status,'partial');assert.equal(val(f.m20.fields.find(f=>f.account==='6.5').value),120);assert.match(f.m20.reason,/campo201.*campo200/);const partial=normalFormFields({...m,colecta:fraction(2n,625n)}).fields.find(f=>f.code==='300');assert.deepEqual(partial.value,{numerator:'2',denominator:'625'});assert.equal(partial.transcriptionReady,false);assert.match(partial.reason,/sem arredondamento/);});
|
||
test('installed4 is current without recalculation; new inputs survive save/recovery/history/export/dossier and become stale on edit',()=>{const original=seed(),old=normalBook(original);assert.equal(normalCurrent(original,old),true);assert.equal(old.result.model.version,'IRPC-normal-adjustments/4');const{d,b}=scenario(),before=JSON.parse(d.modelo_a_snapshot);d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b,'2026-10-25T23:50:00Z'));const recovered=JSON.parse(JSON.stringify(d)),back=normalBook(recovered);assert.equal(normalCurrent(recovered,back),true);assert.deepEqual(back.history.at(-1),old.result);assert.deepEqual(JSON.parse(d.modelo_a_snapshot).books,before.books);assert.deepEqual(JSON.parse(d.modelo_a_snapshot).irpcWithholdings,before.irpcWithholdings);assert.match(normalHtml(d),/1497.6/);assert.match(JSON.stringify(createProjectDossierReport(d,{selectedSections:new Set(['finance'])})),/M22 campo 300/);assert.match(JSON.stringify(normalReportRows(d)),/M20/);fs.writeFileSync('outputs/irpc-l6-sets/calculated.json',JSON.stringify(d));fs.writeFileSync('outputs/irpc-l6-sets/preparation.html',normalHtml(d));back.schedules.rows[0].amount='251';recovered.modelo_a_snapshot=saveNormalBook(recovered,back);assert.equal(normalCurrent(recovered,normalBook(recovered)),false);assert.throws(()=>normalHtml(recovered));const missing=normalBook(d);missing.schedules.interest.conditions='';d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,missing));fs.writeFileSync('outputs/irpc-l6-sets/missing.json',JSON.stringify(d));});
|
||
test('unknown schema/rules fail without silently replacing saved originals',()=>{const{d,b}=scenario();b.schedules.version=2;assert.throws(()=>saveNormalBook(d,b));b.schedules.version=1;b.schedules.rows[0].rule='fiction';assert.throws(()=>saveNormalBook(d,b));});
|
||
|
||
test('documented noncalendar EP year preserves12 monthly periods; short periods and absent legal basis stay case-specific',()=>{const{d,b}=scenario();b.scope='ep_general';b.schedules.period={kind:'ep_annual',from:'2025-07-01',to:'2026-06-30',reference:'QA comunicação expressa AT anterior, início e manutenção5exercícios, plano completo do EP'};b.schedules.assets[0].inUseFrom='2025-07-01';let m=calculateNormal(d,b);assert.equal(val(m.colecta),1497.6);assert.equal(m.assessment.period.from,'2025-07-01');assert.equal(m.assessment.period.to,'2026-06-30');d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b));fs.writeFileSync('outputs/irpc-l6-sets/ep.json',JSON.stringify(d));b.schedules.period.from='2026-01-01';assert.equal(calculateNormal(d,b).colecta,null);b.schedules.period.reference='';assert.equal(calculateNormal(d,b).taxableProfit,null);});
|
||
|
||
test('ordinary tax/undocumented addbacks and current income/cost timing apply their distinct signs without overwriting original accounting',()=>{for(const rule of['tax_own','undocumented']){const{d,b}=scenario();b.schedules.rows[0].rule=rule;assert.equal(val(calculateNormal(d,b).taxableProfit),6980);}for(const [rule,delta]of[['current_cost',-200],['current_income',0]]){const{d,b}=scenario(),r=b.schedules.rows.find(r=>r.id==='timing');r.rule=rule;r.sourceCode='';r.fiscalYear='2026';assert.equal(val(calculateNormal(d,b).taxableProfit),6980+delta);}const{d,b}=scenario(),r=b.schedules.rows.find(r=>r.id==='timing');r.rule='future_income';r.sourceCode='revenue';assert.equal(val(calculateNormal(d,b).taxableProfit),6780);assert.equal(normalFinancialSource(d).accountingProfitBeforeTax,'6240.00');});
|