Identify IRPC income category and retention nature in Portuguese
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-10-25 19:20:44 +02:00
parent 4d6f828230
commit 2b33a1c343
4 changed files with 33 additions and 2 deletions
@@ -0,0 +1,22 @@
{
"status": "directed_presentation_delta_passed",
"base": "4d6f8282302c7646bd7350b8d67695d005feab9a",
"authorization": "Tempo:each calculation identifiesincomeclassification,rateanddefinitive/onaccountnature.",
"changed_runtime": "resources/js/lib/irpc-withholdings.ts",
"sha256_LF": "919a6b7427fcbf67976797358dde0d02a3aba1121bb827b17280b13c0ddc3bd4",
"arithmetic_input_persistence_logic_changed": false,
"verification": {
"presentation": "1newdirectedtest;all6savedcases showcategory/rate/nature inPortuguese;frozendataunchanged",
"affected_export": "1existingexportcase;obsoleteEnglishlabelassertionupdated",
"types": "exit0",
"build": "exit0"
},
"proof_hashes": {
"presentation.txt": "7b348f1e6d64ac0e5cab732175e69fa32154db504e7781c028ced14ec703aaf8",
"export-presentation.txt": "54ada0166f309a216fdbc294bcee98283392b09a2d20a6b2718f2584fb78a133",
"types.txt": "5346ac8a6566c1c38cb0bbda6a029579df58cd78af78e2c4845d9c82b5d1d76c",
"build.txt": "2291bfc0b02957632c92a3a39215c35931a469eba4548473db4de26135468eed"
},
"other_accepted_proofs": "Reused;no new calculationround.",
"active_changes": false
}
+1 -1
View File
@@ -107,5 +107,5 @@ export function recordIrpc(d:Record<string,unknown>,b:IrpcBook,now=new Date().to
return{...b,history:b.result?[...(b.history??[]),structuredClone(b.result)]:b.history,result:{version:irpcWithholdingVersion,key:key(d,b),calculatedAt:now,inputs:structuredClone(b.rows),payments:structuredClone(b.payments),rows,paymentConversions}};
}
const amount=(v:FiscalFraction|null)=>{if(!v)return'Indeterminado';const p=fractionPresentation(v);return(p.approximate?'≈':'')+p.decimal+' MZN';};
export function irpcReportRows(d:Record<string,unknown>):Array<[string,string,string,string]>{try{const b=irpcBook(d),r=b.result;if(!r)return[];return r.rows.map(v=>{const i=r.inputs.find(i=>i.id===v.id)!;return['IRPC — retenções',`${i.documentRef} — ${i.counterparty}`,`${v.status};${i.direction==='paid'?'retida a entregar':'sofrida'};base:${amount(v.base)};taxa:${v.rate??'por corresponder'}%;retenção:${amount(v.tax)};entrega/prova:${amount(v.settled)};saldo:${amount(v.outstanding)};${v.reason}`,`${acquisitionScope(d)};${i.origin};${v.period};evento:${v.eventDate};${v.nature==='on_account'?'Imposto por conta (sem crédito anual automático)':v.nature??'natureza por determinar'};${irpcCurrent(d,b)?'Entradas correspondentes':'Histórico;entradas/origem divergentes'};reconciliação:${i.settlementRef};comprovativos:${i.allocations.map(a=>{const p=r.payments.find(p=>p.id===a.paymentId);return p?`${p.reference}/${p.date}:parcela${a.amount}MZN de${p.amount}${p.currency};${r.paymentConversions?.[p.id]?irpcFxEvidenceLabel(r.paymentConversions[p.id]!):'sem conversão usada'}`:'referência por resolver';}).join(' | ')};${r.calculatedAt};${v.conversion?'Câmbio:'+irpcFxEvidenceLabel(v.conversion)+';':''}${irpcWithholdingSource}`];});}catch(e){return[['IRPC — retenções','Livro não recuperável',String(e),'Originais e histórico preservados.']];}}
export function irpcReportRows(d:Record<string,unknown>):Array<[string,string,string,string]>{try{const b=irpcBook(d),r=b.result;if(!r)return[];return r.rows.map(v=>{const i=r.inputs.find(i=>i.id===v.id)!;return['IRPC — retenções',`${i.documentRef} — ${i.counterparty}`,`${({calculated:'Calculado',indeterminate:'Indeterminado',outside_scope:'Fora deste âmbito'})[v.status]};enquadramento: ${irpcKinds.find(([k])=>k===i.kind)?.[1]??i.kind};${i.direction==='paid'?'retida a entregar':'sofrida'};base:${amount(v.base)};taxa:${v.rate??'por corresponder'}%;retenção:${amount(v.tax)};entrega/prova:${amount(v.settled)};saldo:${amount(v.outstanding)};${v.reason}`,`${acquisitionScope(d)};${i.origin};${v.period};evento:${v.eventDate};${v.nature==='on_account'?'Imposto por conta (sem crédito anual automático)':v.nature==='definitive'?'Retenção definitiva':'Natureza por determinar'};${irpcCurrent(d,b)?'Entradas correspondentes':'Histórico;entradas/origem divergentes'};reconciliação:${i.settlementRef};comprovativos:${i.allocations.map(a=>{const p=r.payments.find(p=>p.id===a.paymentId);return p?`${p.reference}/${p.date}:parcela${a.amount}MZN de${p.amount}${p.currency};${r.paymentConversions?.[p.id]?irpcFxEvidenceLabel(r.paymentConversions[p.id]!):'sem conversão usada'}`:'referência por resolver';}).join(' | ')};${r.calculatedAt};${v.conversion?'Câmbio:'+irpcFxEvidenceLabel(v.conversion)+';':''}${irpcWithholdingSource}`];});}catch(e){return[['IRPC — retenções','Livro não recuperável',String(e),'Originais e histórico preservados.']];}}
export function irpcPreparationHtml(d:Record<string,unknown>):string{const b=irpcBook(d);if(!b.result||!irpcCurrent(d,b))throw Error('Calcule e guarde as entradas actuais antes de preparar.');const esc=(v:unknown)=>String(v).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]!));return'<!doctype html><html lang="pt"><meta charset="utf-8"><title>IRPC — preparação das retenções</title><body><h1>Preparação IRPC — L5</h1><p>Mapa de trabalho;sem submissão,recepção ou aceitação AT. MRFTA19/39:quadro1 retenção;quadro2 mês/ano único por declaração;quadro7 base/taxa/imposto. CER e correspondência das rubricas digitais/moeda electrónica por esclarecer;67(7)remete62(6),mas taxa nova em62(5). Não preencher assinatura ou uso dos serviços. Não consolidar períodos diferentes nem transformar retenções sofridas em imposto a entregar.</p>'+irpcReportRows(d).map(r=>'<section><h2>'+esc(r[1])+'</h2><p>'+esc(r[2])+'</p><small>'+esc(r[3])+'</small></section>').join('')+'<p>Instruções do exemplar:prazo nominal dia20 do mês seguinte à dedução,sem ajuste de calendário/prorrogação ou procedimento2026 presumidos. Juros não calculados. GuiaM/39 anterior preservada,sem actualizar remissões do original.</p></body></html>';}
@@ -0,0 +1,9 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {irpcBook,irpcReportRows,irpcPreparationHtml} from '../../resources/js/lib/irpc-withholdings.ts';
test('each saved calculation identifies income category rate and Portuguese nature without recalculation',()=>{
const d=JSON.parse(fs.readFileSync(new URL('./fixtures/irpc-grouped-preserved.json',import.meta.url),'utf8')),before=JSON.stringify(irpcBook(d)),rows=irpcReportRows(d);
assert.equal(rows.length,6);for(const r of rows){assert.match(r[2],/enquadramento:/);assert.match(r[2],/taxa:(10|20)%/);assert.match(r[3],/Retenção definitiva|Imposto por conta/);}
assert.match(rows[2][2],/Propriedade intelectual\/industrial/);assert.match(rows[3][2],/Serviços de telecomunicações/);assert.match(rows[4][3],/Imposto por conta/);assert.match(rows[0][3],/Retenção definitiva/);assert.equal(JSON.stringify(irpcBook(d)),before);const html=irpcPreparationHtml(d);assert.match(html,/Retenção definitiva/);assert.match(html,/Imposto por conta/);assert.ok(!html.includes(';definitive;'));
});
+1 -1
View File
@@ -22,7 +22,7 @@ test('duplicate document, duplicate proof ID or duplicate reference do not doubl
test('reuse is idempotent, inherits supplier/document/amount, preserves deliberate edits and detects source delta',()=>{const d=draft(),ab=acquisitionBook(d),a={...newAcquisition('L5-supplier','2026-10','MZN'),supplier:'SYNTHETIC saved supplier',documentRef:'SYNTHETIC invoice',documentLine:'1',net:'100000',description:'SaaS'};d.modelo_a_snapshot=saveAcquisitionBook(d,{...ab,rows:[...ab.rows,a]});let b=reuseIrpcSources(d,irpcBook(d));assert.equal(reuseIrpcSources(d,b).rows.length,b.rows.length);let r=b.rows.find(r=>r.source?.id==='acquisition:L5-supplier');assert.equal(r.counterparty,a.supplier);assert.equal(r.documentRef,'SYNTHETIC invoice / linha 1');r.base='90000';assert.equal(reuseIrpcSources(d,b).rows.find(v=>v.id===r.id).base,'90000');b={version:1,rows:[{...row(),id:r.id,source:r.source}],payments:[]};const current=acquisitionBook(d);d.modelo_a_snapshot=saveAcquisitionBook(d,{...current,rows:current.rows.map(v=>v.id===a.id?{...v,net:'120000'}:v)});assert.match(recordIrpc(d,b).result.rows[0].reason,/origem alterado/);});
test('save/recover preserves all preexisting maps, history, context separation and frozen report',()=>{const d=draft(),prior=JSON.parse(d.modelo_a_snapshot),b=recordIrpc(d,book());d.modelo_a_snapshot=saveIrpcBook(d,b);const now=JSON.parse(d.modelo_a_snapshot);for(const[k,v]of Object.entries(prior))assert.deepEqual(now[k],v);assert.deepEqual(irpcBook(d).result,b.result);assert.equal(irpcCurrent(d,b),true);b.rows[0].base='200000';assert.equal(irpcCurrent(d,b),false);assert.match(irpcReportRows({...d,modelo_a_snapshot:saveIrpcBook(d,b)})[0][2],/10000.00/);assert.equal(recordIrpc(d,b).history.length,1);d.context_opportunity='other';assert.equal(irpcBook(d).rows.length,0);});
test('malformed store remains recoverable as explicit error, never overwritten blank',()=>{assert.throws(()=>irpcBook({modelo_a_snapshot:'{"version":1,"books":{},"irpcWithholdings":[]}'}));const d=draft(),b=book();b.rows[0].events=[{kind:'madeup',date:'2026-10-01',reference:'x'}];assert.throws(()=>saveIrpcBook(d,b));});
test('export escapes private inputs, retains period/nature/gaps and excludes submission claims',()=>{const d=draft(),r={...row(),counterparty:'<script>alert(1)</script>'};d.modelo_a_snapshot=saveIrpcBook(d,recordIrpc(d,book(r)));const html=irpcPreparationHtml(d);assert.ok(!html.includes('<script>'));assert.match(html,/&lt;script&gt;/);assert.match(html,/sem submissão/);assert.match(html,/CER/);assert.match(html,/2026-10/);assert.match(html,/definitive/);});
test('export escapes private inputs, retains period/nature/gaps and excludes submission claims',()=>{const d=draft(),r={...row(),counterparty:'<script>alert(1)</script>'};d.modelo_a_snapshot=saveIrpcBook(d,recordIrpc(d,book(r)));const html=irpcPreparationHtml(d);assert.ok(!html.includes('<script>'));assert.match(html,/&lt;script&gt;/);assert.match(html,/sem submissão/);assert.match(html,/CER/);assert.match(html,/2026-10/);assert.match(html,/Retenção definitiva/);});
if(process.env.IRPC_FIXTURE_OUTPUT){const d=draft(),b=recordIrpc(d,{version:1,rows:[{...row(),settlement:'complete',allocations:[{paymentId:'p1',amount:'6000'}]},{...row('commission'),base:'20000',kind:'emoney_commission',direction:'received',settlement:'complete',allocations:[{paymentId:'p2',amount:'1500'}]}],payments:[{id:'p1',kind:'remittance',reference:'SYNTHETIC-AT1',date:'2026-10-20',amount:'6000',currency:'MZN'},{id:'p2',kind:'withholding_proof',reference:'SYNTHETIC-PROOF1',date:'2026-10-20',amount:'1500',currency:'MZN'}]});d.modelo_a_snapshot=saveIrpcBook(d,b);fs.writeFileSync(process.env.IRPC_FIXTURE_OUTPUT,JSON.stringify(d,null,2)+'\n');}
test("allocation changes in another opportunity mark previous result stale only for shared payments",()=>{const d=draft(),p={id:'p',kind:'remittance',reference:'SYNTHETIC-SHARED',date:'2026-10-20',amount:'10000',currency:'MZN'},r={...row(),settlement:'complete',allocations:[{paymentId:'p',amount:'6000'}]},b=recordIrpc(d,{...book(r),payments:[p]});d.modelo_a_snapshot=saveIrpcBook(d,b);assert.equal(irpcCurrent(d,b),true);const ctx=d.context_opportunity;d.context_opportunity='other';d.modelo_a_snapshot=saveIrpcBook(d,book(row('unrelated')));d.context_opportunity=ctx;assert.equal(irpcCurrent(d,b),true);d.context_opportunity='other';d.modelo_a_snapshot=saveIrpcBook(d,{...book({...row('shared'),settlement:'complete',allocations:[{paymentId:'p',amount:'1000'}]}),payments:[p]});d.context_opportunity=ctx;assert.equal(irpcCurrent(d,b),false);assert.match(irpcReportRows(d)[0][3],/Histórico/);});