import {pecCase} from './helpers/irpc-pec-case.mjs'; import test from 'node:test'; import assert from 'node:assert/strict'; import {annualBook,newAnnualBook,newAnnualPpc,reuseAnnualAssessment,calculateAnnual,recordAnnual,saveAnnualBook,annualCurrent,annualReportRows,annualHtml} from '../../resources/js/lib/irpc-annual-settlement.ts'; import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts'; const value=x=>x?Number(x.numerator)/Number(x.denominator):null; const now='2026-10-27T15:00:00+02:00'; const model=({d,b})=>calculateAnnual(d,b); const indeterminate=c=>{const m=model(c);assert.equal(m.status,'indeterminate',JSON.stringify(m.pending));assert.equal(m.arithmeticBalance,null);assert.equal(m.pecDeduction.deducted,null);assert.equal(m.pecDeduction.remaining,null);return m;}; test('PEC precedes PPC; 1408 - 200 - 500 = 708, no official consumption',()=>{ const m=model(pecCase());assert.deepEqual(m.pending,[]);assert.equal(value(m.pecDeduction.deducted),200);assert.equal(value(m.pecDeduction.afterPec),1208);assert.equal(value(m.payable),708);assert.equal(m.pecDeduction.carryCreated,false);assert.equal(m.officialBalancesConsumed,false); }); test('existing received L5 proof is reused after PEC and cannot become another PEC receipt',()=>{ const c=pecCase('200','500','300');assert.equal(value(model(c).payable),408); Object.assign(c.b.pecPayments.rows[0],{document:'QA-RF-PEC-2026',proofAmount:'300'}); assert.match(indeterminate(c).pending.join(' '),/natureza/); }); test('PEC cap never generates refund; PPC can still generate recoverable balance',()=>{ const a=model(pecCase('2000','0'));assert.equal(value(a.pecDeduction.deducted),1408);assert.equal(value(a.pecDeduction.remaining),592);assert.equal(value(a.arithmeticBalance),0);assert.equal(value(a.recoverable),0); const b=model(pecCase('2000','500'));assert.equal(value(b.recoverable),500);assert.equal(value(b.pecDeduction.remaining),592);assert.equal(value(b.pecDeduction.afterPec),0); }); test('previous use explicit, precise decimals and available portion are respected',()=>{ const c=pecCase('200.25','0');Object.assign(c.b.pecPayments.rows[0],{proofAmount:'300.75',previouslyUsed:'100.50'});assert.equal(value(model(c).payable),1207.75); c.b.pecPayments.rows[0].amount='200.26';indeterminate(c); }); test('absence, known zero and missing PEC are distinct; contradictory rows block',()=>{ const c=pecCase();c.b.pecPayments.rows=[];c.b.pecPayments.state='absent';assert.equal(value(model(c).payable),908); c.b.pecPayments.state='';indeterminate(c);c.b.pecPayments.state='known';indeterminate(c); const z=pecCase('0');assert.equal(value(model(z).payable),908);z.b.pecPayments.rows[0].previouslyUsed='';indeterminate(z); z.b.pecPayments.state='absent';indeterminate(z); }); test('payment proof, unused balance, entity, year and currency cannot be presumed',()=>{ for(const change of[{effective:''},{effective:'no'},{document:''},{reference:''},{priorUseReference:''},{previouslyUsed:''},{previouslyUsed:'201'},{amount:'201'},{amount:'1.001'},{subjectNuit:'999999999'},{year:'2025'},{date:'2025-08-31'},{date:'2026-02-30'},{currency:'USD'}]){const c=pecCase();Object.assign(c.b.pecPayments.rows[0],change);indeterminate(c);} }); test('same physical proof cannot be duplicated, re-dated or relabelled PPC',()=>{ const a=pecCase();a.b.pecPayments.rows.push({...a.b.pecPayments.rows[0],id:'copy',date:'2026-09-01'});assert.match(indeterminate(a).pending.join(' '),/repetido|saldo disponível/); const b=pecCase();Object.assign(b.b.pecPayments.rows[0],{document:b.b.ppc.rows[0].document,proofAmount:'500'});assert.match(indeterminate(b).pending.join(' '),/naturezas|natureza incompatível/); }); test('cross-context allocations respect prior use and changing external claims invalidates current result',()=>{ const c=pecCase('100','0'),r=c.b.pecPayments.rows[0];r.proofAmount='500';r.previouslyUsed='100'; const other=structuredClone(c.b);other.pecPayments.rows[0].amount='300'; const s=JSON.parse(c.d.modelo_a_snapshot);s.irpcAnnual={'opportunity:other':other};c.d.modelo_a_snapshot=JSON.stringify(s); c.d.modelo_a_snapshot=saveAnnualBook(c.d,recordAnnual(c.d,c.b,now));const saved=annualBook(c.d);assert.equal(annualCurrent(c.d,saved),true);assert.equal(value(saved.result.model.payable),1308); const edit=JSON.parse(c.d.modelo_a_snapshot);edit.irpcAnnual['opportunity:other'].pecPayments.rows[0].amount='301';c.d.modelo_a_snapshot=JSON.stringify(edit);assert.equal(annualCurrent(c.d,saved),false);assert.match(indeterminate({...c,b:saved}).pending.join(' '),/saldo disponível/); edit.irpcAnnual['opportunity:other'].pecPayments.rows[0].amount='100';edit.irpcAnnual['opportunity:other'].pecPayments.rows[0].previouslyUsed='0';c.d.modelo_a_snapshot=JSON.stringify(edit);indeterminate({...c,b:saved}); }); test('other contexts cannot change PEC titular, origin or payment nature',()=>{ for(const change of[{subjectNuit:'999999999'},{year:'2025'}]){const c=pecCase('100','0'),other=structuredClone(c.b);other.pecPayments.rows[0].proofAmount='500';Object.assign(other.pecPayments.rows[0],change);const s=JSON.parse(c.d.modelo_a_snapshot);s.irpcAnnual={'opportunity:other':other};c.d.modelo_a_snapshot=JSON.stringify(s);indeterminate(c);} const c=pecCase('100','0'),other=structuredClone(c.b);other.ppc.rows=[{...c.b.pecPayments.rows[0],amount:'50',proofAmount:'100'}];other.ppc.state='known';other.pecPayments.rows=[];other.pecPayments.state='absent';const s=JSON.parse(c.d.modelo_a_snapshot);s.irpcAnnual={'opportunity:other':other};c.d.modelo_a_snapshot=JSON.stringify(s);indeterminate(c); }); test('positive special credits remain scoped and cannot be silently skipped before PEC',()=>{ const c=pecCase();c.b.credits.amount='10';assert.match(indeterminate(c).pending.join(' '),/Créditos/);assert.equal(value(model(c).pecDeduction.available),200); }); test('save/recover/recalculate preserves old annual results and all other fiscal books, plan and reports',()=>{ const c=pecCase(),before=JSON.parse(c.d.modelo_a_snapshot),plan=c.d.business_plan_snapshot; const legacy=structuredClone(c.b);delete legacy.pecPayments;c.d.modelo_a_snapshot=saveAnnualBook(c.d,recordAnnual(c.d,legacy,now));const old=structuredClone(annualBook(c.d).result); c.b={...annualBook(c.d),pecPayments:c.b.pecPayments};c.d.modelo_a_snapshot=saveAnnualBook(c.d,recordAnnual(c.d,c.b,now));const back=annualBook(JSON.parse(JSON.stringify(c.d))); assert.deepEqual(back.history,[old]);assert.equal(value(back.result.model.payable),708);assert.equal(annualCurrent(c.d,back),true);const after=JSON.parse(c.d.modelo_a_snapshot);delete after.irpcAnnual;assert.deepEqual(after,before);assert.equal(c.d.business_plan_snapshot,plan); const html=annualHtml(c.d);assert.match(html,/PEC — dedução limitada/);assert.match(html,/708.00/);assert.match(html,/sem reembolso PEC/); const dossier=createProjectDossierReport(c.d,{selectedSections:new Set(['finance'])});assert.match(JSON.stringify(dossier),/PEC — dedução limitada/); const frozen=structuredClone(back.result);back.pecPayments.rows[0].amount='100';c.d.modelo_a_snapshot=saveAnnualBook(c.d,back);assert.equal(annualCurrent(c.d,back),false);assert.deepEqual(back.result,frozen);assert.match(annualReportRows(c.d).flat().join(' '),/Histórico/);assert.throws(()=>annualHtml(c.d),/fontes actuais/); }); test('malformed PEC extension never replaces legacy snapshots',()=>{ const c=pecCase(),original=c.d.modelo_a_snapshot;c.b.pecPayments.rows[0].previouslyUsed=null;assert.throws(()=>saveAnnualBook(c.d,c.b),/não recuperáveis/);assert.equal(c.d.modelo_a_snapshot,original); });