From 2b33a1c343d192f80fb24aac2a27b2d375af5601 Mon Sep 17 00:00:00 2001 From: nala Date: Sun, 25 Oct 2026 19:20:44 +0200 Subject: [PATCH] Identify IRPC income category and retention nature in Portuguese --- .../irpc-l5-presentation-qa-20261025.json | 22 +++++++++++++++++++ resources/js/lib/irpc-withholdings.ts | 2 +- tests/frontend/irpc-presentation.test.mjs | 9 ++++++++ tests/frontend/irpc-withholdings.test.mjs | 2 +- 4 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 docs/investmoz/simulations/irpc-l5-presentation-qa-20261025.json create mode 100644 tests/frontend/irpc-presentation.test.mjs diff --git a/docs/investmoz/simulations/irpc-l5-presentation-qa-20261025.json b/docs/investmoz/simulations/irpc-l5-presentation-qa-20261025.json new file mode 100644 index 00000000..609d0175 --- /dev/null +++ b/docs/investmoz/simulations/irpc-l5-presentation-qa-20261025.json @@ -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 +} diff --git a/resources/js/lib/irpc-withholdings.ts b/resources/js/lib/irpc-withholdings.ts index 9d738bf8..a9162479 100644 --- a/resources/js/lib/irpc-withholdings.ts +++ b/resources/js/lib/irpc-withholdings.ts @@ -107,5 +107,5 @@ export function recordIrpc(d:Record,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):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):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{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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]!));return'IRPC — preparação das retenções

Preparação IRPC — L5

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.

'+irpcReportRows(d).map(r=>'

'+esc(r[1])+'

'+esc(r[2])+'

'+esc(r[3])+'
').join('')+'

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.

';} diff --git a/tests/frontend/irpc-presentation.test.mjs b/tests/frontend/irpc-presentation.test.mjs new file mode 100644 index 00000000..31d3f3c1 --- /dev/null +++ b/tests/frontend/irpc-presentation.test.mjs @@ -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;')); +}); diff --git a/tests/frontend/irpc-withholdings.test.mjs b/tests/frontend/irpc-withholdings.test.mjs index 99717f60..173ad3e2 100644 --- a/tests/frontend/irpc-withholdings.test.mjs +++ b/tests/frontend/irpc-withholdings.test.mjs @@ -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:''};d.modelo_a_snapshot=saveIrpcBook(d,recordIrpc(d,book(r)));const html=irpcPreparationHtml(d);assert.ok(!html.includes(''};d.modelo_a_snapshot=saveIrpcBook(d,recordIrpc(d,book(r)));const html=irpcPreparationHtml(d);assert.ok(!html.includes('