26 lines
14 KiB
JavaScript
26 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,normalFinancialSource}from'../../resources/js/lib/irpc-normal.ts';
|
||
import{newNormalFormPreparation,reuseDirectFormAllocations,normalFormRepresentation,prepareNormalForms}from'../../resources/js/lib/irpc-normal-form-preparation.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-periods-fx-preserved.json',import.meta.url),'utf8')),val=f=>f?Number(f.numerator)/Number(f.denominator):null;
|
||
function setup(){const d=seed(),b=normalBook(d);b.forms={...newNormalFormPreparation(),edition:'accepted_pdf_and_m20a1',reference:'QA PDFM22/Q10–12 e M20A.1/Q4;edições inventariadas distintas',completenessReference:'QA contas completas da oportunidade/sujeito no período;sem outros custos ou proveitos omitidos',allocations:[...reuseDirectFormAllocations(normalFinancialSource(d)),{id:'revenue',sourceCode:'revenue',account:'7.2',amount:'6000',reference:'QA prestação de serviços documentada,sem vendas de bens'},{id:'opex',sourceCode:'operating_expenses',account:'6.3',amount:'600',reference:'QA fornecimentos e serviços de terceiros completos no período'}],incomeTax:{state:'absent',amount:'',placement:'',reference:'QA ausência documentada de imposto contabilizado;não inferir gasto da colecta'},reducedRate:{state:'absent',reference:'QA sujeito/período sem parcela de imposto a taxa reduzida'}};return{d,b};}
|
||
const field=(m,code)=>m.formPreparation.fields.find(f=>f.code===code);
|
||
test('exact representation keeps signedfraction,approximation,zero,missing and parentheses distinct without fiscal rounding',()=>{for(const f of[fraction(1n,3n),fraction(-1n,300n),fraction(123456789012345678901n,100n)]){const r=normalFormRepresentation(f,'whole');assert.equal(r.exact,f.numerator+'/'+f.denominator+' MZN');assert.equal(r.represented,null);assert.equal(r.ready,false);}assert.match(normalFormRepresentation(fraction(1n,3n)).display,/≈/);assert.equal(normalFormRepresentation(fraction(0n)).represented,'0');assert.equal(normalFormRepresentation(null).display,'Indeterminado');assert.equal(normalFormRepresentation(fraction(-20n),'whole',true).represented,'(20)');assert.match(normalFormRepresentation(fraction(1n,2n)).reason,/exacto ao cêntimo/);});
|
||
test('optional9 prepares M20 classifications/netresult and M22 mappedadjustments while preserving all existing totals',()=>{const{d,b}=setup(),before=normalBook(d).result.model,m=calculateNormal(d,b);assert.equal(m.version,'IRPC-normal-adjustments/9');for(const k of['accountingProfit','taxableProfit','taxableBase','colecta'])assert.deepEqual(m[k],before[k]);assert.equal(val(m.formPreparation.netResult),3120);assert.equal(val(field(m,'200').value),3120);assert.equal(field(m,'200').transcriptionReady,false);assert.equal(val(field(m,'213').value),0);assert.equal(val(field(m,'206').value),10);assert.equal(val(field(m,'238').value),3130);assert.equal(val(field(m,'258').value),0);assert.equal(m.formPreparation.reconciliation.matches,true);assert.equal(field(m,'259').application,'not_applicable');assert.equal(field(m,'260').transcriptionReady,true);assert.equal(val(field(m,'302').value),1001.6);assert.equal(field(m,'302').representation.ready,true);assert.equal(field(m,'309–316').value,null);assert.equal(m.assessment.finalBalance.payable,null);});
|
||
test('missingorcontradictory booktax blocks only netresult and its fieldreconciliation; knownnormalprofit/base/colecta preserved',()=>{for(const tax of[{state:'',amount:'',placement:'',reference:''},{state:'absent',amount:'320',placement:'outside',reference:'QA'},{state:'known',amount:'320',placement:'included',reference:'QA'}]){const{d,b}=setup();b.forms.incomeTax=tax;const m=calculateNormal(d,b);assert.equal(m.formPreparation.netResult,null);assert.equal(field(m,'200').value,null);assert.equal(val(field(m,'260').value),3130);assert.equal(val(field(m,'300').value),1001.6);assert.equal(val(m.colecta),1001.6);}});
|
||
test('incomeTaxalreadyincluded reconciles account8.5 and mappedtax without subtracting it again',()=>{const{d,b}=setup();b.forms.incomeTax={state:'known',amount:'320',placement:'included',reference:'QA imposto reconhecido no plano'};b.forms.allocations.find(a=>a.id==='opex').amount='280';b.forms.allocations.push({id:'tax',sourceCode:'operating_expenses',account:'8.5',amount:'320',reference:'QA imposto já no custo de origem'});b.schedules.rows.push({id:'tax',rule:'tax_own',amount:'320',sourceCode:'operating_expenses',document:'QA-tax',line:'1',reference:'QA imposto sobre lucro já incluído',fiscalYear:'',conditions:'yes'});Object.assign(b.profit.topics.expenseCorrections,{state:'known',amount:'320',reference:'QA corresponde ao imposto contabilizado já no resultado'});const m=calculateNormal(d,b);assert.equal(val(m.formPreparation.netResult),3120);assert.equal(val(field(m,'213').value),0);assert.equal(val(m.taxableProfit),3450);assert.equal(field(m,'238').value,null);assert.equal(m.formPreparation.reconciliation.matches,false);assert.ok(m.formPreparation.unmapped.some(e=>e.reason.includes('213')));});
|
||
test('incomplete revenueclassification does not erase directM20 fields or independentM22 calculations',()=>{const{d,b}=setup();b.forms.allocations.find(a=>a.id==='revenue').amount='5999';const m=calculateNormal(d,b);assert.equal(m.formPreparation.m20.find(f=>f.account==='7.2').value,null);assert.equal(val(m.formPreparation.m20.find(f=>f.account==='6.5').value),60);assert.equal(m.formPreparation.netResult,null);assert.equal(val(m.taxableProfit),3130);assert.equal(val(field(m,'300').value),1001.6);});
|
||
test('fractional M20 allocations remain exact; no truncation or rounding changes source or net/taxable totals',()=>{const{d,b}=setup();b.forms.allocations.find(a=>a.id==='opex').amount='300.50';b.forms.allocations.push({id:'other',sourceCode:'operating_expenses',account:'6.8',amount:'299.50',reference:'QA gastos operacionais restantes documentados'});const m=calculateNormal(d,b),f=m.formPreparation.m20.find(f=>f.account==='6.3');assert.deepEqual(f.value,fraction(601n,2n));assert.equal(f.representation.ready,false);assert.equal(f.representation.represented,null);assert.equal(val(m.formPreparation.netResult),3120);assert.equal(val(m.colecta),1001.6);});
|
||
test('lossuses positiveM22 lossfield and M20parentheses; no loss in profitfield and no negative taxablebase',()=>{const{d,b}=setup(),p=JSON.parse(d.business_plan_snapshot);p.plan.products[0].unitPrice='20';b.schedules.currentLossReference='QA perda fiscal corrente reconciliada no sujeito/período,sem reporte automático';d.business_plan_snapshot=JSON.stringify(p);b.forms.allocations.find(a=>a.id==='revenue').amount='1200';const m=calculateNormal(d,b);assert.equal(val(m.taxableProfit),-1670);assert.equal(val(field(m,'259').value),1670);assert.equal(field(m,'260').application,'not_applicable');assert.equal(m.formPreparation.netRepresentation.represented,'(1680)');assert.equal(val(m.taxableBase),0);assert.equal(val(m.colecta),0);});
|
||
test('unmapped priorquota deduction keeps demonstrated effect and existing260/278/300; no guessedblankfield',()=>{const{d,b}=setup(),p=JSON.parse(d.business_plan_snapshot);p.plan.assetDepreciation='48';d.business_plan_snapshot=JSON.stringify(p);Object.assign(b.depreciation.assets[0],{accounting:'24',start:'2025-01-01',previousAccounting:'120',previousFiscal:'80',priorAvailable:'40'});b.forms.allocations.find(a=>a.sourceCode==='depreciation').amount='24';const m=calculateNormal(d,b);assert.equal(val(m.depreciation.adjustment),-26);assert.equal(val(m.colecta),1001.6);assert.ok(m.formPreparation.unmapped.some(e=>e.origin.startsWith('Mapa de amortizações')));assert.equal(field(m,'258').value,null);assert.equal(field(m,'238').value,null);assert.equal(val(field(m,'260').value),3130);assert.equal(m.formPreparation.entries.find(e=>e.origin.startsWith('Mapa de amortizações')).code,null);});
|
||
test('reducedrate unknown or knowninL7 blocks only302,notnormal300 and not finalL8 assumptions',()=>{for(const state of['','known']){const{d,b}=setup();b.forms.reducedRate.state=state;const m=calculateNormal(d,b);assert.equal(field(m,'302').value,null);assert.equal(val(field(m,'300').value),1001.6);assert.equal(val(m.colecta),1001.6);assert.equal(m.assessment.finalBalance.recoverable,null);}});
|
||
test('save/recover/report preserves8history and originalplan and never claims officialrounding or crosseditiontransfer',()=>{const{d,b}=setup(),original=d.business_plan_snapshot,old=structuredClone(b.result);assert.equal(normalCurrent(d,normalBook(d)),true);d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b,'2026-10-26T10:00:00+02:00'));const back=normalBook(JSON.parse(JSON.stringify(d)));assert.equal(normalCurrent(d,back),true);assert.deepEqual(back.history.at(-1),old);assert.equal(d.business_plan_snapshot,original);assert.match(normalHtml(d),/campo213/);assert.match(normalHtml(d),/transporte automático M20/);assert.match(normalHtml(d),/exacto ao cêntimo/);assert.match(JSON.stringify(createProjectDossierReport(d,{selectedSections:new Set(['finance'])})),/campo302/);back.forms.allocations[0].reference+='changed';assert.equal(normalCurrent(d,back),false);});
|
||
test('fivenewcases write privateAPI proof inputs and reports without rerunning accepted functionalsuites',()=>{const cases={mapped:setup(),fractional:setup(),missing:setup(),unmapped:setup(),booktax:setup()};cases.booktax.b.forms.incomeTax={state:'known',amount:'320',placement:'outside',reference:'QA gasto contabilístico exterior ao plano;campo próprio pendente'};cases.fractional.b.forms.allocations.find(a=>a.id==='opex').amount='300.50';cases.fractional.b.forms.allocations.push({id:'other',sourceCode:'operating_expenses',account:'6.8',amount:'299.50',reference:'QA parcela contabilística com centavos'});cases.missing.b.forms.incomeTax={state:'',amount:'',placement:'',reference:''};const u=cases.unmapped,p=JSON.parse(u.d.business_plan_snapshot);p.plan.assetDepreciation='48';u.d.business_plan_snapshot=JSON.stringify(p);Object.assign(u.b.depreciation.assets[0],{accounting:'24',start:'2025-01-01',previousAccounting:'120',previousFiscal:'80',priorAvailable:'40'});u.b.forms.allocations.find(a=>a.sourceCode==='depreciation').amount='24';fs.mkdirSync('outputs/irpc-l6-values-forms',{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-values-forms/'+name+'.json',JSON.stringify(d));fs.writeFileSync('outputs/irpc-l6-values-forms/'+name+'.html',normalHtml(d));}});
|
||
|
||
test('newlyidentified includedtax mismatch blocks only dependent213/reconciliation without revising demonstratedtax',()=>{const{d,b}=setup();b.forms.incomeTax={state:'known',amount:'320',placement:'included',reference:'QA imposto reconhecido no plano'};b.forms.allocations.find(a=>a.id==='opex').amount='280';b.forms.allocations.push({id:'tax',sourceCode:'operating_expenses',account:'8.5',amount:'320',reference:'QA imposto reconhecido no custo da origem'});const m=calculateNormal(d,b);assert.equal(val(m.colecta),1001.6);assert.equal(val(m.formPreparation.netResult),3120);assert.equal(val(field(m,'213').value),0);assert.equal(field(m,'238').value,null);assert.equal(val(field(m,'260').value),3130);assert.ok(m.formPreparation.pending.some(s=>s.includes('tax_own')));});
|
||
test('positive and negative asseteffects remain separate; netdepreciation is never all assignedto206',()=>{const{d,b}=setup(),a=structuredClone(b.depreciation.assets[0]);Object.assign(b.depreciation.assets[0],{cost:'60',accounting:'36'});Object.assign(a,{id:'other',assetKey:'other',document:'QA-other-asset',accounting:'24',start:'2025-01-01',previousAccounting:'120',previousFiscal:'80',priorAvailable:'40'});b.depreciation.assets.push(a);const m=calculateNormal(d,b);assert.equal(val(m.depreciation.adjustment),4);assert.equal(val(field(m,'206').value),30);assert.equal(val(m.taxableProfit),3124);assert.equal(m.formPreparation.unmapped.length,1);assert.equal(val(m.formPreparation.unmapped[0].value),-26);assert.equal(field(m,'238').value,null);});
|
||
test('acceptedcostproof feeds independentM22 labels without applying printedpercentages again',()=>{const d=JSON.parse(fs.readFileSync(new URL('./fixtures/irpc-costs-preserved.json',import.meta.url),'utf8')),b=normalBook(d),m=b.result.model,{b:input}=setup();const mapped=prepareNormalForms(input.forms,m,normalFinancialSource(d));for(const[rule,code]of [['allowance','219'],['representation','220'],['advertising','221'],['credit','209']]){const row=m.costs.rows.find(r=>r.rule===rule);assert.ok(row);const sum=m.costs.rows.filter(r=>r.rule===rule).reduce((s,r)=>s+val(r.adjustment),0);const value=mapped.fields.find(f=>f.code===code).value;if(code!=='209')assert.equal(val(value),sum);else assert.equal(val(value),m.costs.rows.filter(r=>['credit','stock','litigation'].includes(r.rule)).reduce((s,r)=>s+val(r.adjustment),0));}assert.deepEqual(b.result.model,m);});
|
||
|
||
test('absence of booktax cannot erase previouslyidentified ownincomeTax cost; independentknowncolecta remains',()=>{const{d,b}=setup();b.schedules.rows.push({id:'tax',rule:'tax_own',amount:'320',sourceCode:'operating_expenses',document:'QA-tax',line:'1',reference:'QA imposto já incluído',fiscalYear:'',conditions:'yes'});Object.assign(b.profit.topics.expenseCorrections,{state:'known',amount:'320',reference:'QA mapa do imposto próprio'});b.forms.incomeTax={state:'absent',amount:'',placement:'',reference:'QA ausência a conferir'};const m=calculateNormal(d,b);assert.equal(m.formPreparation.netResult,null);assert.ok(m.formPreparation.pending.some(s=>s.includes('contradiz imposto próprio')));assert.equal(val(field(m,'300').value),1104);});
|