feat(irpc): reconcile CBF16 cessation and documented prior quotas in QA

This commit is contained in:
2026-10-26 15:16:45 +02:00
parent ba83afe7d8
commit 992c97ad4e
7 changed files with 143 additions and 15 deletions
File diff suppressed because one or more lines are too long
@@ -0,0 +1,41 @@
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 {validateSpecial} from '../../resources/js/lib/irpc-special.ts';
import {fraction} from '../../resources/js/lib/fiscal-rational.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
const read=name=>JSON.parse(fs.readFileSync(new URL('./fixtures/'+name,import.meta.url),'utf8'));
const val=v=>v?Number(v.numerator)/Number(v.denominator):null;
function accepted(){const d=read('irpc-l7-preserved.json').acceleration,f=read('irpc-l7-start-preserved.json');d.business_plan_snapshot=f.plan;d.modelo_a_snapshot=saveNormalBook(d,f.book);return{d,b:normalBook(d)};}
function setup(period='cessation',prior=true){
const{d,b}=accepted(),months=period==='annual'?12:6,p=JSON.parse(d.business_plan_snapshot);p.plan.assetDepreciation='120';d.business_plan_snapshot=JSON.stringify(p);
if(period==='annual')delete b.periodsFx;else Object.assign(b.periodsFx,{kind:'cessation',from:'2026-01-01',to:'2026-06-30',reference:'QA cessação documentada30Junho2026;meses completos sem transmissão presumida'});
b.schedules.interest.amount=String(months*20);
Object.assign(b.depreciation.assets[0],{start:prior?'2025-01-01':'2026-01-15',timing:'months',accounting:String(months*10),previousAccounting:prior?'150':'0',previousFiscal:prior?'100':'0',priorAvailable:prior?'50':'0',previousReference:prior?'QA quotas contabilísticas150,fiscais100,parcela50 temporariamente tributada;sem encargos permanentemente excluídos':'QA ausência declarada de quotas anteriores;activo entrou em2026',exitState:'absent',exitDate:''});
for(const part of[b.special.expenses,b.special.cooperative,b.special.acceleration])Object.assign(part.qualification,{from:'2026-01-01',to:period==='annual'?'2026-12-31':'2026-06-30',periodReference:period==='annual'?'QA ano civil2026':b.periodsFx.reference});
delete b.special.acceleration.monthly.startPeriod;
b.special.acceleration.monthly.continuation={version:1,period,reference:'QA CBF16 vigente no sujeito/projecto e período2026 declarado;sem cumulação',historyReference:'QA activo novo2025/2026,método constante e duodécimos desde entrada;quotas anteriores reconciliadas no mapaL6,não recalculadas',monthlyFromStart:'yes'};return{d,b};
}
test('accepted12 result,key and fullhistory remain exact without continuation',()=>{const{d,b}=accepted();assert.equal(normalCurrent(d,b),true);assert.deepEqual(calculateNormal(d,b),b.result.model);assert.deepEqual(normalBook(d).history,b.history);});
test('cessation reuses6 months without assuming assetexit;50 normal,75 accelerated capped60,additional10',()=>{const{d,b}=setup('cessation',false),asset=JSON.stringify(b.depreciation),period=JSON.stringify(b.periodsFx),m=calculateNormal(d,b),s=m.special,a=s.acceleration.monthly.assets[0];assert.equal(m.version,'IRPC-normal-adjustments/13');assert.equal(a.months,6);assert.equal(a.exit.state,'absent');assert.equal(val(a.normalQuota),50);assert.equal(val(a.acceleratedQuota),75);assert.equal(val(a.acceleratedDeduction),60);assert.equal(val(a.additionalDeduction),10);assert.equal(val(m.taxableProfit),3130);assert.equal(val(m.colecta),1001.6);assert.equal(val(s.taxableProfit),3120);assert.equal(val(s.taxableBase),3120);assert.equal(val(s.colecta),998.4);assert.equal(JSON.stringify(b.depreciation),asset);assert.equal(JSON.stringify(b.periodsFx),period);assert.equal(s.finalBalance.payable,null);});
test('cessation with priorbook150,fiscal100,available50 recovers15 once and adds only25 to normaldeduction',()=>{const{d,b}=setup(),m=calculateNormal(d,b),s=m.special,a=s.acceleration.monthly.assets[0];assert.equal(a.months,6);assert.equal(val(a.normalDeduction),50);assert.equal(val(a.acceleratedDeduction),75);assert.equal(val(a.prior.normalUsed),0);assert.equal(val(a.prior.acceleratedUsed),15);assert.equal(val(a.prior.normalRemaining),50);assert.equal(val(a.prior.acceleratedRemaining),35);assert.equal(val(a.prior.normalFiscalRemaining),850);assert.equal(val(a.prior.acceleratedFiscalRemaining),825);assert.equal(val(s.acceleration.additionalDeduction),25);assert.equal(val(s.taxableProfit),3105);assert.equal(val(s.taxableBase),3105);assert.equal(val(s.colecta),993.6);assert.equal(b.depreciation.assets[0].priorAvailable,'50');assert.equal(b.depreciation.assets[0].previousFiscal,'100');});
test('annual2026 continuation preserves2025 actualbalances and records30priorused within150 deduction,additional50',()=>{const{d,b}=setup('annual'),m=calculateNormal(d,b),s=m.special,a=s.acceleration.monthly.assets[0];assert.equal(a.months,12);assert.equal(val(a.normalQuota),100);assert.equal(val(a.acceleratedQuota),150);assert.equal(val(a.prior.acceleratedUsed),30);assert.equal(val(a.prior.acceleratedRemaining),20);assert.equal(val(s.acceleration.additionalDeduction),50);assert.equal(val(m.taxableProfit),6260);assert.equal(val(s.taxableProfit),6210);assert.equal(val(s.colecta),1987.2);assert.deepEqual(s.acceleration.normal.input,s.acceleration.accelerated.input);});
test('documented assetexit excludes eventmonth and keeps exact fractional quotas;cessation date does not edit exit',()=>{const{d,b}=setup();Object.assign(b.depreciation.assets[0],{exitState:'known',exitDate:'2026-06-20'});const a=calculateNormal(d,b).special.acceleration.monthly.assets[0];assert.equal(a.months,5);assert.deepEqual(a.normalQuota,fraction(125n,3n));assert.deepEqual(a.acceleratedQuota,fraction(125n,2n));assert.deepEqual(a.additionalDeduction,fraction(125n,6n));assert.deepEqual(a.prior.acceleratedUsed,fraction(5n,2n));assert.deepEqual(a.prior.acceleratedRemaining,fraction(95n,2n));});
test('remaining fiscalbalance and accounted/priorcaps bind both calculations;available amount is never a second deduction',()=>{const{d,b}=setup();Object.assign(b.depreciation.assets[0],{previousAccounting:'1000',previousFiscal:'980',priorAvailable:'20'});let a=calculateNormal(d,b).special.acceleration.monthly.assets[0];assert.equal(val(a.normalDeduction),20);assert.equal(val(a.acceleratedDeduction),20);assert.equal(val(a.additionalDeduction),0);assert.equal(val(a.prior.acceleratedFiscalRemaining),0);const second=setup(),p=JSON.parse(second.d.business_plan_snapshot);p.plan.assetDepreciation='0';second.d.business_plan_snapshot=JSON.stringify(p);second.b.depreciation.assets[0].accounting='0';a=calculateNormal(second.d,second.b).special.acceleration.monthly.assets[0];assert.equal(val(a.normalDeduction),50);assert.equal(val(a.acceleratedDeduction),50);assert.equal(val(a.additionalDeduction),0);assert.equal(val(a.prior.acceleratedRemaining),0);});
test('missing support or missing priorbalance blocks only dependent CBF16;absence and negative remain explicit',()=>{for(const key of['reference','historyReference','monthlyFromStart']){const{d,b}=setup();b.special.acceleration.monthly.continuation[key]='';const m=calculateNormal(d,b);assert.equal(m.special.acceleration.additionalDeduction,null);assert.notEqual(m.colecta,null);}const missing=setup();missing.b.depreciation.assets[0].previousFiscal='';const pending=calculateNormal(missing.d,missing.b);assert.equal(pending.special.acceleration.additionalDeduction,null);assert.match(pending.depreciation.pending.join(' '),/em falta/);assert.equal(missing.b.depreciation.assets[0].previousFiscal,'');for(const state of['negative','absent']){const{d,b}=setup();if(state==='negative')b.special.acceleration.rows[0].eligible='no';else{b.special.acceleration.rows=[];Object.assign(b.special.acceleration.qualification,{state:'absent',eligible:''});}const s=calculateNormal(d,b).special;assert.equal(val(s.acceleration.additionalDeduction),0);assert.equal(s.acceleration.status,state==='absent'?'absent':'calculated');if(state==='negative')assert.equal(s.acceleration.rows[0].status,'not_applicable');}});
test('otherstartyears,nonmonthlyhistory,transition,cumulation and prioramounts for currentyearentry stay out of scope',()=>{for(const mutate of[b=>b.depreciation.assets[0].start='2024-01-01',b=>b.special.acceleration.monthly.continuation.monthlyFromStart='no',b=>b.periodsFx.kind='transition',b=>b.special.acceleration.noOtherBenefits='no',b=>b.depreciation.assets[0].start='2026-01-01']){const{d,b}=setup();mutate(b);const m=calculateNormal(d,b);assert.equal(m.special.acceleration.additionalDeduction,null);assert.notEqual(m.colecta,null);}});
test('save-recover13 preserves12result and allhistorical balances;recalc idempotent and reports reconciliation without officialconsumption',()=>{const{d,b}=setup(),history=structuredClone(b.history),old=structuredClone(b.result),asset=JSON.stringify(b.depreciation),plan=d.business_plan_snapshot,others=JSON.parse(d.modelo_a_snapshot).irpcWithholdings;d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b));let back=normalBook(JSON.parse(JSON.stringify(d)));assert.equal(normalCurrent(d,back),true);assert.deepEqual(back.history,[...history,old]);assert.equal(old.model.version,'IRPC-normal-adjustments/12');assert.equal(JSON.stringify(back.depreciation),asset);assert.equal(d.business_plan_snapshot,plan);assert.deepEqual(JSON.parse(d.modelo_a_snapshot).irpcWithholdings,others);const html=normalHtml(d);assert.match(html,/2026-01-01–2026-06-30/);assert.match(html,/recuperada CBF16:15.00/);assert.match(html,/saldo anterior aritmético CBF16:35.00/);assert.match(html,/sem consumo de saldos oficiais/);assert.match(html,/colecta sobre a base:993.60/);assert.match(html,/Saldo final a pagar\/recuperar:Indeterminado/);assert.match(JSON.stringify(createProjectDossierReport(d,{selectedSections:new Set(['finance'])})),/quotas contabilísticas anteriores/);d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,back));back=normalBook(d);assert.equal(val(back.result.model.special.acceleration.additionalDeduction),25);assert.equal(back.depreciation.assets[0].priorAvailable,'50');back.depreciation.assets[0].previousFiscal='110';assert.equal(normalCurrent(d,back),false);});
test('invalid or overlapping optionalpreparation cannot replace preservedbooks',()=>{for(const c of[null,{version:2,period:'annual',reference:'QA',historyReference:'QA',monthlyFromStart:'yes'},{version:1,period:'other',reference:'QA',historyReference:'QA',monthlyFromStart:'yes'}]){const{b}=setup();b.special.acceleration.monthly.continuation=c;assert.throws(()=>validateSpecial(b.special),/não recuperável/);}const{b}=setup();b.special.acceleration.monthly.startPeriod={version:1,reference:'QA'};assert.throws(()=>validateSpecial(b.special),/incompatível/);});
test('prepare only compatible new guardedAPI cases with missing,negative and absence variants',()=>{const cases={cessation:setup('cessation',false),prior:setup(),annual:setup('annual'),exit:setup(),missing:setup(),negative:setup(),absent:setup()};Object.assign(cases.exit.b.depreciation.assets[0],{exitState:'known',exitDate:'2026-06-20'});cases.missing.b.special.acceleration.monthly.continuation.historyReference='';cases.negative.b.special.acceleration.rows[0].eligible='no';cases.absent.b.special.acceleration.rows=[];Object.assign(cases.absent.b.special.acceleration.qualification,{state:'absent',eligible:''});fs.mkdirSync('outputs/irpc-l7-cessation-prior',{recursive:true});for(const[name,{d,b}]of Object.entries(cases)){d.modelo_a_snapshot=saveNormalBook(d,recordNormal(d,b,'2026-10-26T18:00:00+02:00'));fs.writeFileSync('outputs/irpc-l7-cessation-prior/'+name+'.json',JSON.stringify(d));fs.writeFileSync('outputs/irpc-l7-cessation-prior/'+name+'.html',normalHtml(d));}});