Files
Investidor-V2/tests/frontend/irpc-grouped.test.mjs
T

41 lines
14 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {irpcBook,newIrpcRow,recordIrpc,saveIrpcBook,emptyIrpcOrdinaryFacts,irpcCurrent,irpcReportRows,irpcPreparationHtml} from '../../resources/js/lib/irpc-withholdings.ts';
import {convertIrpcAmount,irpcFxSources,updateIrpcFx} from '../../resources/js/lib/irpc-fx.ts';
import {emptyFxSelection,selectFx} from '../../resources/js/lib/iva-fx-selection.ts';
import {currencyEventBook,newCurrencyEvent,saveCurrencyEventBook} from '../../resources/js/lib/iva-currency-events.ts';
const accepted=JSON.parse(fs.readFileSync(new URL('./fixtures/irpc-l5-preserved.json',import.meta.url),'utf8'));
const draft=()=>structuredClone(accepted);
const row=(id='normal')=>({...newIrpcRow(id),counterparty:'SYNTHETIC IRPC counterparty',documentRef:'SYNTHETIC-'+id,base:'100000',kind:'royalty',recipientIrpc:'yes',payerIrpc:'yes',residence:'nonresident',permanentEstablishment:'no',mozambique:'yes',qualificationRef:'SYNTHETIC classified non-digital income,IRPCpayer/recipient,Mozambique source;residence andEP explicitly recorded',specialRelief:'none',eventsComplete:'yes',events:[{kind:'cost',date:'2026-10-02',reference:'SYNTHETIC applicable cost'},{kind:'payment',date:'2026-11-02',reference:'SYNTHETIC payment'}],settlement:'none',settlementRef:'SYNTHETIC no remittance',ordinary:{...emptyIrpcOrdinaryFacts(),nonDigital:'yes',reference:'SYNTHETIC exact category facts;CIRPC62/67'}});
const book=(r=row())=>({version:1,rows:[r],payments:[]});
const calc=r=>recordIrpc(draft(),book(r)).result.rows[0];
const fx=(patch={})=>{const s={...emptyFxSelection(),nature:'commercial',submissionDate:'2026-10-20',bulletinDate:'2026-10-20',bulletinRef:'SYNTHETIC fictional BM bulletin for QA',source:'SYNTHETIC archived QA exemplar',issuer:'Banco de Moçambique',sourceCurrency:'USD',direction:'mzn_per_unit',mean:'64',...patch};return{...s,applied:selectFx(s)};};
test('ordinary nonresident royalties equipment services and nonlisted capitals use20%,not digital10%',()=>{for(const kind of ['royalty','equipment','services','capital']){const r=row();r.kind=kind;if(kind==='capital')r.ordinary.capitalType='unlisted_interest';const v=calc(r);assert.equal(v.status,'calculated');assert.equal(v.rate,20);assert.equal(v.tax.numerator,'20000');assert.equal(v.nature,'definitive');}});
test('resident royalties and ordinary rent are on account;predial nonresident is also on account',()=>{for(const patch of [{kind:'royalty',residence:'resident'},{kind:'rent',residence:'resident'},{kind:'rent',residence:'nonresident'}]){const r={...row(),...patch};r.ordinary.rentOwnPropertyManagement='no';const v=calc(r);assert.equal(v.status,'calculated');assert.equal(v.nature,'on_account');assert.equal(v.tax.numerator,'20000');assert.equal('annualCredit'in v,false);}});
test('identified dispensa branches and unknown facts block only their own income line',()=>{const d=draft(),rs=[{...row('resident-services'),residence:'resident',kind:'services'},{...row('property'),kind:'rent'},{...row('capital'),residence:'resident',kind:'capital',ordinary:{...row().ordinary,capitalType:'unlisted_interest'}},row('independent')];const out=recordIrpc(d,{version:1,rows:rs,payments:[]}).result.rows;assert.ok(out.slice(0,3).every(v=>v.status==='indeterminate'&&v.tax===null));assert.equal(out[3].tax.numerator,'20000');});
test('special categories62(4)a-c retain10% with exact facts,not generic20% or genericinstallation10%',()=>{for(const kind of ['telecom','international_transport','connected_installation','aircraft_maintenance_charter','rural_public_electrification','maritime_fishing_cabotage']){const r={...row(),kind};r.ordinary.specialFacts='yes';const v=calc(r);assert.equal(v.rate,10);assert.equal(v.tax.numerator,'10000');assert.match(v.reason,/62\(4\)/);}const unknown={...row(),kind:'connected_installation'};assert.equal(calc(unknown).tax,null);const generic={...row(),kind:'services'};assert.equal(calc(generic).rate,20);});
test('special62(4) entity condition is distinct from digital noEP/nonattributable branch',()=>{for(const patch of [{residence:'resident'},{permanentEstablishment:'yes',attributable:'no'}]){const r={...row(),kind:'telecom',...patch};r.ordinary.specialFacts='yes';assert.equal(calc(r).tax,null);}});
test('listed debt and other BMsecurities remain distinct;liquidity repos are not assumed',()=>{for(const[capitalType,rate]of [['listed_debt_mz',20],['listed_other_mz',10]]){const r={...row(),kind:'capital'};r.ordinary.capitalType=capitalType;r.ordinary.specialFacts='yes';assert.equal(calc(r).rate,rate);}const resident={...row(),kind:'capital',residence:'resident'};resident.ordinary.capitalType='listed_debt_mz';resident.ordinary.capitalSeatAndManagement='yes';assert.equal(calc(resident).rate,20);assert.equal(calc(resident).nature,'definitive');resident.ordinary.capitalType='liquidity_repo';assert.equal(calc(resident).rate,20);assert.equal(calc(resident).nature,'definitive');});
test('expired2025 agricultural measures do not extend into2026 foreign services/interest',()=>{for(const kind of ['services','capital']){const r={...row(),kind,description:'SYNTHETIC nationalagriculturalcompany / external agricultural financing'};r.ordinary.capitalType='unlisted_interest';const v=calc(r);assert.equal(v.rate,20);assert.match(v.reason,/31\/12\/2025/);}const r=row();r.events[0].date='2025-12-31';assert.equal(calc(r).tax,null);});
test('applied existing BMmean converts base, preserves original and keeps event/date separate',()=>{const r={...row(),currency:'USD',base:'1000',fxSelection:fx()};const v=calc(r);assert.equal(v.base.numerator,'64000');assert.equal(v.tax.numerator,'12800');assert.equal(v.period,'2026-10');assert.equal(v.eventDate,'2026-10-02');assert.equal(v.conversion.date,'2026-10-20');assert.equal(v.conversion.originalAmount,'1000');assert.equal(v.conversion.originalCurrency,'USD');assert.equal(r.base,'1000');assert.equal(r.currency,'USD');});
test('purchase/sale perspective,inverse quote and fiscalexception use deliveredselector',()=>{const opposite=fx({nature:'purchase',relation:'opposite',bulletinPerspective:'SYNTHETIC issuer',buy:'63',sell:'65'});let r={...row(),currency:'USD',base:'100',fxSelection:opposite};assert.equal(calc(r).base.numerator,'6500');const inverse=fx({direction:'units_per_mzn',mean:'0.02'});r.fxSelection=inverse;assert.equal(calc(r).base.numerator,'5000');r.fxSelection=fx({mode:'exception',exceptionRef:'SYNTHETIC identifiedfiscalprovision',exceptionScope:'SYNTHETIC retention scope',exceptionDate:'2026-10-02',exceptionRate:'66'});assert.equal(calc(r).base.numerator,'6600');assert.equal(calc(r).conversion.column,'exception');});
test('unapplied,stale,missingmean,wrongcurrency or undated FX remains indeterminate',()=>{const samples=[undefined,{...fx(),applied:undefined},{...fx(),mean:'65'},{...fx(),sourceCurrency:'EUR'},{...fx(),bulletinDate:'2026-10-19'}];for(const f of samples){const v=calc({...row(),currency:'USD',base:'1000',fxSelection:f});assert.equal(v.tax,null);assert.match(v.reason,/conversão/);}});
test('existing saved FX selections are linked,reused,and changed source marks only linked results stale',()=>{const d=draft(),cb=currencyEventBook(d),s=fx(),f={...newCurrencyEvent('shared-fx','MZN'),fxSelection:s,sourceCurrency:'USD',fxDirection:s.direction,fxRate:s.applied.rate,fxDate:s.applied.date,fxSource:s.applied.source,description:'SYNTHETIC existing FX'};d.modelo_a_snapshot=saveCurrencyEventBook(d,{...cb,rows:[...cb.rows,f]});const source=irpcFxSources(d).find(s=>s.id===f.id),r={...row(),currency:'USD',base:'1000',fxSelection:source.selection,fxLink:{id:source.id,fingerprint:source.fingerprint}},b=recordIrpc(d,book(r));d.modelo_a_snapshot=saveIrpcBook(d,b);assert.equal(irpcCurrent(d,b),true);const current=currencyEventBook(d);d.modelo_a_snapshot=saveCurrencyEventBook(d,{...current,rows:current.rows.map(v=>v.id===f.id?{...v,description:'SYNTHETIC revisedsource'}:v)});assert.equal(irpcCurrent(d,b),false);assert.match(recordIrpc(d,b).result.rows[0].reason,/origem alterada/);const m=recordIrpc(d,book(row('independentMZN')));assert.equal(m.result.rows[0].tax.numerator,'20000');});
test('foreign withholding proof converts available amount once;allocations remainMZN',()=>{const r={...row(),direction:'received',settlement:'complete',settlementRef:'SYNTHETIC provedpartialwithholding',allocations:[{paymentId:'p',amount:'640'}]},p={id:'p',kind:'withholding_proof',reference:'SYNTHETIC-USDproof',date:'2026-10-20',currency:'USD',amount:'10',fxSelection:fx()};const d=draft(),b=recordIrpc(d,{...book(r),payments:[p]});assert.equal(b.result.rows[0].settled.numerator,'640');assert.equal(b.result.rows[0].outstanding.numerator,'19360');assert.equal(b.result.paymentConversions.p.originalAmount,'10');d.modelo_a_snapshot=saveIrpcBook(d,b);assert.match(irpcReportRows(d)[0][3],/SYNTHETIC-USDproof/);assert.match(irpcReportRows(d)[0][3],/Original: 10 USD/);});
test('foreign payment to beneficiary or purported ATremittance is never converted into taxproof',()=>{for(const kind of ['beneficiary','remittance']){const r={...row(),settlement:'complete',allocations:[{paymentId:'p',amount:'640'}]},p={id:'p',kind,reference:'SYNTHETIC-wrongpayment',date:'2026-10-20',currency:'USD',amount:'10',fxSelection:fx()};assert.equal(recordIrpc(draft(),{...book(r),payments:[p]}).result.rows[0].status,'indeterminate');}});
test('converted shared proof cannot exceed availableMZN or change FX between contexts',()=>{const d=draft(),p={id:'p',kind:'withholding_proof',reference:'SYNTHETIC-sharedUSD',date:'2026-10-20',currency:'USD',amount:'10',fxSelection:fx()},r={...row('one'),direction:'received',settlement:'complete',allocations:[{paymentId:'p',amount:'400'}]};d.modelo_a_snapshot=saveIrpcBook(d,recordIrpc(d,{...book(r),payments:[p]}));d.context_opportunity='other';const other={...row('two'),direction:'received',settlement:'complete',allocations:[{paymentId:'p',amount:'300'}]};assert.equal(recordIrpc(d,{...book(other),payments:[p]}).result.rows[0].status,'indeterminate');other.allocations[0].amount='200';assert.equal(recordIrpc(d,{...book(other),payments:[p]}).result.rows[0].status,'calculated');assert.equal(recordIrpc(d,{...book(other),payments:[{...p,fxSelection:fx({mean:'65'})}]}).result.rows[0].status,'indeterminate');});
test('accepted v1MZN10% results remain current and bytepreserved without recomputation',()=>{const d=draft(),b=irpcBook(d),before=JSON.stringify(b);assert.equal(irpcCurrent(d,b),true);assert.equal(JSON.stringify(irpcBook(d)),before);assert.deepEqual(irpcBook({...d,modelo_a_snapshot:saveIrpcBook(d,b)}),b);});
test('new normal/FX result preserves old maps and acceptedresult in history,report frozen after change',()=>{const d=draft(),b=irpcBook(d),old=structuredClone(b.result),s=JSON.parse(d.modelo_a_snapshot);const next=recordIrpc(d,{...b,rows:[...b.rows,{...row('fx'),currency:'USD',base:'1000',fxSelection:fx()}]});d.modelo_a_snapshot=saveIrpcBook(d,next);for(const[k,v]of Object.entries(s))if(k!=='irpcWithholdings')assert.deepEqual(JSON.parse(d.modelo_a_snapshot)[k],v);assert.deepEqual(next.history.at(-1),old);next.rows.at(-1).fxSelection.mean='65';assert.equal(irpcCurrent(d,next),false);d.modelo_a_snapshot=saveIrpcBook(d,next);const report=irpcReportRows(d).at(-1);assert.match(report[2],/12800.00/);assert.match(report[3],/Histórico/);assert.match(report[3],/taxa: 64/);});
test('export keeps currencies,normal/special distinctions,payment refs and externalformalities separate',()=>{const d=draft(),r={...row(),currency:'USD',base:'1000',fxSelection:fx()};d.modelo_a_snapshot=saveIrpcBook(d,recordIrpc(d,book(r)));const h=irpcPreparationHtml(d);assert.match(h,/Original: 1000 USD/);assert.ok(!h.includes("selectionKey"));assert.match(h,/20%/);assert.match(h,/CER/);assert.match(h,/sem submissão/);assert.match(h,/2026-10/);});
if(process.env.IRPC_GROUPED_FIXTURE_OUTPUT){
const d=draft(),old=irpcBook(d),s=fx(),f={...newCurrencyEvent('retention-fx','MZN'),description:'SYNTHETIC existing BMselection for retention',fxSelection:s,sourceCurrency:'USD',fxDirection:s.direction,fxRate:s.applied.rate,fxDate:s.applied.date,fxSource:s.applied.source};const cb=currencyEventBook(d);d.modelo_a_snapshot=saveCurrencyEventBook(d,{...cb,rows:[...cb.rows,f]});const source=irpcFxSources(d).find(v=>v.id===f.id);
const foreign={...row('foreign-royalty'),currency:'USD',base:'1000',fxSelection:source.selection,fxLink:{id:source.id,fingerprint:source.fingerprint},settlement:'complete',settlementRef:'SYNTHETIC IRPC remittance5000',allocations:[{paymentId:'p3',amount:'5000'}]},special={...row('special-telecom'),kind:'telecom',ordinary:{...row().ordinary,specialFacts:'yes'}},resident={...row('resident-royalty'),residence:'resident',base:'50000'},received={...row('received-foreign-royalty'),direction:'received',currency:'USD',base:'100',fxSelection:s,settlement:'complete',settlementRef:'SYNTHETIC proof10USD converted640MZN',allocations:[{paymentId:'p4',amount:'640'}]};
const b=recordIrpc(d,{...old,rows:[...old.rows,foreign,special,resident,received],payments:[...old.payments,{id:'p3',kind:'remittance',reference:'SYNTHETIC-IRPC-AT5000',date:'2026-10-20',amount:'5000',currency:'MZN'},{id:'p4',kind:'withholding_proof',reference:'SYNTHETIC-IRPC-USD10',date:'2026-10-20',amount:'10',currency:'USD',fxSelection:s}]});d.modelo_a_snapshot=saveIrpcBook(d,b);fs.writeFileSync(process.env.IRPC_GROUPED_FIXTURE_OUTPUT,JSON.stringify(d,null,2)+'\n');
}
test("62(3) definitive capital needs exact seat/management facts and is not an annual credit",()=>{const r={...row(),residence:"resident",kind:"capital"};r.ordinary.capitalType="listed_debt_mz";assert.equal(calc(r).tax,null);r.ordinary.capitalSeatAndManagement="yes";assert.equal(calc(r).nature,"definitive");assert.equal(calc(r).rate,20);assert.equal("annualCredit"in calc(r),false);});
test('confirming unchanged FX preserves source link;deliberate input change detaches only that selection',()=>{const input={currency:'USD',fxSelection:fx(),fxLink:{id:'saved',fingerprint:'original'}};const confirmed={...input.fxSelection,applied:selectFx(input.fxSelection)};assert.deepEqual(updateIrpcFx(input,confirmed).fxLink,input.fxLink);assert.equal(updateIrpcFx(input,{...input.fxSelection,mean:'65'}).fxLink,undefined);assert.equal(input.fxSelection.mean,'64');assert.equal(input.fxLink.id,'saved');});