123 lines
27 KiB
TypeScript
123 lines
27 KiB
TypeScript
import {validateMrfta,mrftaReportRows,type MrftaStore} from './irpc-mrfta.ts';
|
||
import {irpcReliefVersion,evaluateIrpcRelief,irpcReliefEvidenceLabel,type IrpcReliefPreparation} from './irpc-relief-preparation.ts';
|
||
import {convertIrpcAmount,irpcFxDependency,irpcFxEvidenceLabel,type IrpcFxAmount,type IrpcFxEvidence} from './irpc-fx.ts';
|
||
import {acquisitionBook,acquisitionScope} from './iva-acquisitions.ts';
|
||
import {salesBook} from './iva-operations.ts';
|
||
import {closureFingerprint} from './iva-closure.ts';
|
||
import {addFractions,decimalFraction,fraction,fractionPresentation,subtractFractions,type FiscalFraction} from './fiscal-rational.ts';
|
||
|
||
export const irpcWithholdingVersion='irpc-L5/1';
|
||
export const irpcWithholdingSource='Lei12/2025,BR248/2025,PDF19/p2180(19):arts5(5/7),20(2)c,62(5)a/b e67(5/7);CIRPC Lei34/2007 arts2,62/67/68,PDF17–19/p768(71–73);Lei19/2013 PDF2/p682(22),62(3/4);Lei20/2022 BR252 p2731/PDF1,62(1–5). Art67(7)remete62(6),enquanto taxa nova consta62(5):divergência formal preservada,sem renumerar o original. MRFTA19/39AT,p1–2. Código2002 revogado não usado. Outros regimes/taxas dependem da correspondência específica,sem zero presumido.';
|
||
type Choice=''|'yes'|'no';
|
||
export type IrpcKind=''|'digital_goods'|'digital_services'|'emoney_commission'|'royalty'|'equipment'|'rent'|'capital'|'services'|'telecom'|'international_transport'|'connected_installation'|'aircraft_maintenance_charter'|'rural_public_electrification'|'maritime_fishing_cabotage'|'other';
|
||
export const irpcKinds:Array<[IrpcKind,string]>=[['','Por identificar'],['digital_goods','Transmissão de bens digitais'],['digital_services','Prestação de serviços digitais'],['emoney_commission','Comissão de agente de moeda electrónica'],['royalty','Propriedade intelectual/industrial'],['equipment','Uso de equipamento'],['rent','Rendimento predial'],['capital','Aplicação de capitais'],['services','Outras prestações/intermediação'],['telecom','Serviços de telecomunicações — artigo62(4)a'],['international_transport','Transportes internacionais — artigo62(4)a'],['connected_installation','Montagem/instalação de equipamentos conexos a telecomunicações/transportes —62(4)a'],['aircraft_maintenance_charter','Manutenção e fretamento de aeronaves —62(4)a'],['rural_public_electrification','Construção/reabilitação de infraestruturas de produção,transporte e distribuição de energia eléctrica,nas zonas rurais,em projectos públicos de electrificação rural —62(4)b'],['maritime_fishing_cabotage','Afretamento de embarcações marítimas para pesca/cabotagem —62(4)c'],['other','Outro enquadramento']];
|
||
export type IrpcEvent={kind:'cost'|'payment'|'maturity'|'availability'|'liquidation'|'quantification';date:string;reference:string};
|
||
export type IrpcPayment=IrpcFxAmount&{id:string;kind:'beneficiary'|'remittance'|'withholding_proof';reference:string;date:string;amount:string;currency:string};
|
||
export type IrpcOrdinaryFacts={nonDigital:Choice;capitalType:''|'unlisted_interest'|'unlisted_dividend'|'listed_debt_mz'|'listed_other_mz'|'liquidity_repo'|'other';rentOwnPropertyManagement:Choice;specialFacts:Choice;capitalSeatAndManagement?:Choice;reference:string};
|
||
export const emptyIrpcOrdinaryFacts=():IrpcOrdinaryFacts=>({nonDigital:'',capitalType:'',rentOwnPropertyManagement:'',specialFacts:'',capitalSeatAndManagement:'',reference:''});
|
||
export type IrpcRow=IrpcFxAmount&{reliefPreparation?:IrpcReliefPreparation;ordinary?:IrpcOrdinaryFacts;id:string;direction:'paid'|'received';counterparty:string;documentRef:string;description:string;base:string;currency:string;origin:'projected'|'effective';kind:IrpcKind;recipientIrpc:Choice;payerIrpc:Choice;residence:''|'resident'|'nonresident';permanentEstablishment:Choice;attributable:Choice;mozambique:Choice;qualificationRef:string;specialRelief:''|'none'|'claimed';reliefRef:string;eventsComplete:Choice;events:IrpcEvent[];settlement:''|'none'|'complete';settlementRef:string;allocations:Array<{paymentId:string;amount:string}>;source?:{id:string;key:string;fingerprint:string};};
|
||
export type IrpcResult={treatment?:'dispensed'|'treaty';reliefRule?:string;id:string;status:'calculated'|'indeterminate'|'outside_scope';reason:string;period:string;eventDate:string;nature:'definitive'|'on_account'|null;rate:number|null;base:FiscalFraction|null;tax:FiscalFraction|null;settled:FiscalFraction|null;outstanding:FiscalFraction|null;direction:IrpcRow['direction'];conversion?:IrpcFxEvidence|null};
|
||
export type IrpcBook=MrftaStore&{version:1;rows:IrpcRow[];payments:IrpcPayment[];result?:{version:string;key:string;calculatedAt:string;inputs:IrpcRow[];payments:IrpcPayment[];rows:IrpcResult[];paymentConversions?:Record<string,IrpcFxEvidence|null>};history?:Array<NonNullable<IrpcBook['result']>>};
|
||
export const newIrpcRow=(id:string):IrpcRow=>({id,direction:'paid',counterparty:'',documentRef:'',description:'',base:'',currency:'MZN',origin:'projected',kind:'',recipientIrpc:'',payerIrpc:'',residence:'',permanentEstablishment:'',attributable:'',mozambique:'',qualificationRef:'',specialRelief:'',reliefRef:'',eventsComplete:'',events:[],settlement:'',settlementRef:'',allocations:[]});
|
||
function store(d:Record<string,unknown>){const s=JSON.parse(String(d.modelo_a_snapshot||'{"version":1,"books":{}}'));if(s.version!==1||!s.books||typeof s.books!=='object'||Array.isArray(s.books))throw Error('Livros fiscais não recuperáveis;originais preservados.');return s;}
|
||
function map(d:Record<string,unknown>):Record<string,IrpcBook>{const m=store(d).irpcWithholdings;if(m===undefined)return{};if(!m||typeof m!=='object'||Array.isArray(m))throw Error('Mapa IRPC não recuperável;originais preservados.');return m;}
|
||
function validate(b:IrpcBook){
|
||
if(!b||b.version!==1||!Array.isArray(b.rows)||!Array.isArray(b.payments)||b.history!==undefined&&!Array.isArray(b.history))throw Error('Livro IRPC não recuperável.');
|
||
validateMrfta(b);
|
||
for(const r of b.rows){const template=newIrpcRow('');if(!r||Object.keys(template).filter(k=>typeof template[k as keyof IrpcRow]==='string').some(k=>typeof r[k as keyof IrpcRow]!=='string')||!['paid','received'].includes(r.direction)||!['projected','effective'].includes(r.origin)||!irpcKinds.some(([k])=>k===r.kind)||!['','resident','nonresident'].includes(r.residence)||!['','none','claimed'].includes(r.specialRelief)||!['','none','complete'].includes(r.settlement)||['recipientIrpc','payerIrpc','permanentEstablishment','attributable','mozambique','eventsComplete'].some(k=>!['','yes','no'].includes(r[k as keyof IrpcRow]as string))||!Array.isArray(r.events)||!Array.isArray(r.allocations)||r.events.some(e=>!e||!['cost','payment','maturity','availability','liquidation','quantification'].includes(e.kind)||typeof e.date!=='string'||typeof e.reference!=='string')||r.allocations.some(a=>!a||typeof a.paymentId!=='string'||typeof a.amount!=='string')||r.source!==undefined&&(!r.source||!['id','key','fingerprint'].every(k=>typeof r.source![k as keyof NonNullable<IrpcRow['source']>]==='string')))throw Error('Entradas IRPC não recuperáveis;sem apagar ou repor vazios.');}
|
||
for(const r of b.rows)if(r.reliefPreparation&&(r.reliefPreparation.rule!==undefined&&!['','cirpc68d','cirpc68f','ptMz12'].includes(r.reliefPreparation.rule)||r.reliefPreparation.facts!==undefined&&(!r.reliefPreparation.facts||typeof r.reliefPreparation.facts!=='object'||Array.isArray(r.reliefPreparation.facts)||Object.values(r.reliefPreparation.facts).some(v=>!['','yes','no'].includes(v)))))throw Error('Condições da dispensa/convenção não recuperáveis.');
|
||
for(const r of b.rows)if(r.reliefPreparation&&(!['dispensation','exemption','treaty'].includes(r.reliefPreparation.kind)||['instrument','article','country','residenceDocument','validFrom','validTo','conditions','evidence'].some(k=>typeof r.reliefPreparation![k as keyof IrpcReliefPreparation]!=='string')))throw Error('Preparação de dispensa/convenção não recuperável;original preservado.');
|
||
for(const r of b.rows)if(r.ordinary&&(['nonDigital','rentOwnPropertyManagement','specialFacts'].some(k=>!['','yes','no'].includes(r.ordinary![k as 'nonDigital'|'rentOwnPropertyManagement'|'specialFacts']))||typeof r.ordinary.reference!=='string'||!['','unlisted_interest','unlisted_dividend','listed_debt_mz','listed_other_mz','liquidity_repo','other'].includes(r.ordinary.capitalType)))throw Error('Factos específicos IRPC não recuperáveis.');
|
||
for(const r of b.rows)if(r.ordinary?.capitalSeatAndManagement!==undefined&&!['','yes','no'].includes(r.ordinary.capitalSeatAndManagement))throw Error('Factos de sede/direcção não recuperáveis.');
|
||
for(const v of [...b.rows,...b.payments])if(v.fxSelection!==undefined&&(!v.fxSelection||typeof v.fxSelection!=='object'||Array.isArray(v.fxSelection))||v.fxLink!==undefined&&(!v.fxLink||typeof v.fxLink.id!=='string'||typeof v.fxLink.fingerprint!=='string'))throw Error('Vínculo/selecção cambial não recuperável;original preservado.');
|
||
for(const p of b.payments)if(!p||!['id','reference','date','amount','currency'].every(k=>typeof p[k as keyof IrpcPayment]==='string')||!['beneficiary','remittance','withholding_proof'].includes(p.kind))throw Error('Pagamento IRPC inválido;originais preservados.');
|
||
if(b.result&&(!Array.isArray(b.result.rows)||!Array.isArray(b.result.inputs)||typeof b.result.key!=='string'||b.result.version!==irpcWithholdingVersion))throw Error('Resultado IRPC não recuperável.');
|
||
if(b.payments.some((p,i)=>p.reference.trim()&&b.payments.some((q,j)=>i!==j&&q.kind===p.kind&&q.reference.trim()===p.reference.trim()&&q.date===p.date&&q.currency===p.currency)))throw Error('Pagamento/prova já registado:reutilize a referência existente em vez de duplicar.');
|
||
if(new Set(b.rows.map(r=>r.id)).size!==b.rows.length||new Set(b.payments.map(p=>p.id)).size!==b.payments.length)throw Error('Identificadores IRPC repetidos.');
|
||
}
|
||
export function irpcBook(d:Record<string,unknown>):IrpcBook{const b=map(d)[acquisitionScope(d)]??{version:1,rows:[],payments:[]};validate(b);return b;}
|
||
export function saveIrpcBook(d:Record<string,unknown>,b:IrpcBook):string{validate(b);return JSON.stringify({...store(d),irpcWithholdings:{...map(d),[acquisitionScope(d)]:b}});}
|
||
export function irpcSources(d:Record<string,unknown>){return[
|
||
...acquisitionBook(d).rows.map(r=>({id:'acquisition:'+r.id,key:closureFingerprint(r),counterparty:r.supplier,documentRef:r.documentRef+(r.documentLine?' / linha '+r.documentLine:''),description:r.description,amount:r.net,currency:acquisitionBook(d).currency,direction:'paid' as const,origin:r.origin})),
|
||
...salesBook(d).rows.map(r=>({id:'sale:'+r.id,key:closureFingerprint(r),counterparty:'',documentRef:r.evidence,description:r.description,amount:r.basis==='net'?r.amount:'',currency:r.sourceCurrency,direction:'received' as const,origin:'projected' as const})),
|
||
];}
|
||
export function reuseIrpcSources(d:Record<string,unknown>,b:IrpcBook):IrpcBook{validate(b);const rows=[...b.rows];for(const s of irpcSources(d))if(!rows.some(r=>r.source?.id===s.id)){const r={...newIrpcRow('irpc:'+s.id),counterparty:s.counterparty,documentRef:s.documentRef,description:s.description,base:s.amount,currency:s.currency,direction:s.direction,origin:s.origin};rows.push({...r,source:{id:s.id,key:s.key,fingerprint:closureFingerprint(r)}});}return{...b,rows};}
|
||
function sourceIssue(d:Record<string,unknown>,r:IrpcRow){if(!r.source)return'';const s=irpcSources(d).find(s=>s.id===r.source!.id);return !s?'Operação de origem não está disponível neste contexto;vínculo e edições conservados.':s.key!==r.source.key?'Documento/operação de origem alterado;compare o histórico e reconcilie a divergência antes de calcular.':'';}
|
||
const date=(s:string)=>/^20\d{2}-(0[1-9]|1[0-2])-\d{2}$/.test(s)&&!Number.isNaN(Date.parse(s))&&new Date(s).toISOString().slice(0,10)===s;
|
||
const positive=(f:FiscalFraction)=>BigInt(f.numerator)>0n;
|
||
export function evaluateIrpc(d:Record<string,unknown>,r:IrpcRow):IrpcResult{
|
||
const v:IrpcResult={id:r.id,status:'indeterminate',reason:'Identifique o rendimento e os intervenientes.',period:'',eventDate:'',nature:null,rate:null,base:null,tax:null,settled:null,outstanding:null,direction:r.direction};
|
||
const issue=sourceIssue(d,r)||irpcFxDependency(d,r);if(issue)return{...v,reason:issue};
|
||
if(r.recipientIrpc==='no')return{...v,status:'outside_scope',reason:'Titular fora de IRPC;sem converter em IRPS ou em isenção.'};
|
||
if(!r.counterparty.trim()||!r.documentRef.trim()||r.recipientIrpc!=='yes'||r.payerIrpc!=='yes'||!r.residence||!r.kind||!r.qualificationRef.trim())return{...v,reason:'Faltam contraparte,documento,qualificação do rendimento,titular IRPC,devedor IRPC ou residência. A via devedor IRPS exige âmbito próprio.'};
|
||
if(r.mozambique!=='yes')return{...v,reason:'Falta correspondência territorial aos arts5/67;ser estrangeiro não resolve incidência nem isenção.'};
|
||
if(r.specialRelief!=='none'&&r.specialRelief!=='claimed')return{...v,reason:'Convenção,isenção ou dispensa invocada/desconhecida:identificar artigo,período,condições e prova;sem taxa reduzida ou zero automáticos.'};
|
||
if(!r.eventsComplete||r.eventsComplete!=='yes'||!r.events.length||r.events.some(e=>!date(e.date)||!e.reference.trim()))return{...v,reason:'Identifique os eventos aplicáveis67(5),incluindo reconhecimento do custo,e confirme a sua completude com suporte;uma factura ou pagamento isolado não fixa o momento.'};
|
||
v.eventDate=r.events.map(e=>e.date).sort()[0];v.period=v.eventDate.slice(0,7);
|
||
if(!v.eventDate.startsWith('2026-'))return{...v,reason:'Primeiro evento aplicável fora de2026;corresponder versão/período próprio,sem deslocar para o pagamento posterior.'};
|
||
try{const converted=convertIrpcAmount(d,r.base,r);v.base=converted.value;v.conversion=converted.evidence;}catch(e){return{...v,reason:'Base do rendimento/conversão por resolver: '+String(e)};}
|
||
if(r.specialRelief==='claimed'){
|
||
const relief=evaluateIrpcRelief(r,v.eventDate);if(!relief.applied)return{...v,reason:relief.reason};
|
||
v.treatment=relief.treatment;v.reliefRule=relief.rule;v.rate=relief.rate;v.nature=relief.treatment==='treaty'?'definitive':'on_account';v.reason=relief.reason;
|
||
v.tax=fraction(BigInt(v.base.numerator)*BigInt(relief.rate),BigInt(v.base.denominator)*100n);v.status='calculated';return v;
|
||
}
|
||
const digital=['digital_goods','digital_services','emoney_commission'].includes(r.kind);
|
||
if(digital){
|
||
if(r.residence==='nonresident'&&r.permanentEstablishment==='no'||r.residence==='nonresident'&&r.permanentEstablishment==='yes'&&r.attributable==='no')v.nature='definitive';
|
||
else return{...v,reason:'Para residente ou rendimento imputável aEP,corresponder concretamente a taxa62(5) com as dispensas68,em especial68(d),antes da liquidação. EP desconhecido permanece indeterminado;90dias/serviço digital não criam ausência deEP automática.'};
|
||
v.rate=10;v.reason='Taxa10%62(5) no âmbito digital/moeda electrónica demonstrado;retenção definitiva67(3). Procedimento67(7)/remissão62(6) e CER permanecem separados.';
|
||
}else{
|
||
const f=r.ordinary;
|
||
if(!f||f.nonDigital!=='yes'||!f.reference.trim())return{...v,reason:'Classifique o rendimento não abrangido pelo ramo digital/moeda electrónica e identifique os factos específicos. Cotejo62/67:Lei34/2007,Lei19/2013 eLei20/2022 agora disponível;sem taxa genérica.'};
|
||
const nonresident=r.residence==='nonresident';
|
||
if(nonresident&&r.permanentEstablishment!=='no'&&!(r.permanentEstablishment==='yes'&&['yes','no'].includes(r.attributable)))return{...v,reason:'Falta identificar EP e imputação;ausência física ou nacionalidade não resolvem este facto.'};
|
||
const attributable=!nonresident||r.permanentEstablishment==='yes'&&r.attributable==='yes';
|
||
v.nature=r.kind==='rent'||attributable?'on_account':'definitive';
|
||
if(['equipment','services'].includes(r.kind)&&v.nature==='on_account')return{...v,reason:'Rendimento67(1)b/g com imposto por conta:dispensa68(d) identificada,por corresponder no ramo de dispensas;não reter20% nem declarar isenção do rendimento.'};
|
||
if(r.kind==='rent'&&f.rentOwnPropertyManagement!=='no')return{...v,reason:'Rendimento predial mantém natureza por conta67(3). Identifique a gestão de imóveis próprios e eventual dispensa68(f)/transparência;apenas este caso depende dessa correspondência.'};
|
||
if(r.kind==='capital'){
|
||
if(!f.capitalType||f.capitalType==='other')return{...v,reason:'Identifique juros/dividendos não cotados,títulos cotadosBM ou permutas de liquidez;62(3/4) não permite taxa única para capitais.'};
|
||
if(r.residence==='resident'&&['listed_debt_mz','liquidity_repo'].includes(f.capitalType)){if(f.capitalSeatAndManagement!=='yes')return{...v,reason:'Art62(3):identifique sede e direcção efectiva nacionais do titular,além da categoria exacta dos títulos/permuta;residência genérica não substitui estes factos.'};v.nature='definitive';}
|
||
if(v.nature==='on_account'&&f.capitalType!=='listed_debt_mz')return{...v,reason:'Capitais com imposto por conta:correspondência específica das dispensas68(a/b/c/g),incluindo titular e operação,permanece separada;sem bloquear capitais definitivos independentes.'};
|
||
if(f.capitalType==='liquidity_repo'&&r.residence!=='resident')return{...v,reason:'Permutas de liquidez62(3)b exigem entidade com sede/direcção efectiva nacional;caso não residente mantém correspondência própria.'};
|
||
}
|
||
const special=['telecom','international_transport','connected_installation','aircraft_maintenance_charter','rural_public_electrification','maritime_fishing_cabotage'].includes(r.kind)||r.kind==='capital'&&f.capitalType==='listed_other_mz';
|
||
if(special){
|
||
if(!nonresident||r.permanentEstablishment!=='no')return{...v,reason:'Taxa especial62(4) refere entidades62(2),não residentes semEP. O caso residente/EP tem correspondência própria;sem copiar10%.'};
|
||
if(f.specialFacts!=='yes')return{...v,reason:'Confirme os factos exactos62(4)a–d:conexão dos equipamentos,projecto público rural,pesca/cabotagem ou título cotadoBM que não é dívida/bilhete62(3)a,conforme categoria. Sem taxa especial por descrição genérica.'};
|
||
v.rate=10;v.reason='Taxa especial10%62(4),redacçãoLei19/2013,conservada emLei20/2022 eLei12/2025;entidade não residente semEP e categoria específica identificadas. Não é a taxa digital62(5).';
|
||
}else{
|
||
if(!['royalty','equipment','rent','capital','services'].includes(r.kind))return{...v,reason:'Natureza fora dos ramos agrupados;identificar artigo e condições próprios,sem taxa por defeito.'};
|
||
v.rate=20;v.reason=(r.kind==='capital'&&r.residence==='resident'&&['listed_debt_mz','liquidity_repo'].includes(f.capitalType)?'Retenção liberatória20%62(3)a/b:juros de dívida/bilhetes cotados ou permuta de liquidez qualificados,com sede e direcção efectiva nacionais. ':'')+'Taxa20%62(1),com62(2/3) quando aplicável;incidência67(1),natureza67(3). Lei20/2022,p2731,confrontada:as medidas agrícolas terminam31/12/2025 e não são transportadas para2026. Sem aplicar10% digital a rendimentos não digitais.';
|
||
}
|
||
}
|
||
v.tax=fraction(BigInt(v.base.numerator)*BigInt(v.rate!),BigInt(v.base.denominator)*100n);v.status='calculated';v.reason+=' Base separada do IVA;entrega/prova e liquidação anual mantêm reconciliação própria.';return v;
|
||
}
|
||
function sharedPayments(d:Record<string,unknown>,b:IrpcBook){const used=b.payments.filter(p=>b.rows.some(r=>r.allocations.some(a=>a.paymentId===p.id)));return Object.entries(map(d)).filter(([ctx])=>ctx!==acquisitionScope(d)).flatMap(([ctx,other])=>{validate(other);const payments=other.payments.filter(q=>used.some(p=>p.reference.trim()===q.reference.trim()&&p.date===q.date&&p.currency===q.currency&&p.kind===q.kind));return payments.length?[[ctx,payments,other.rows.flatMap(r=>r.allocations.filter(a=>payments.some(p=>p.id===a.paymentId)))]]:[];});}
|
||
function key(d:Record<string,unknown>,b:IrpcBook){const fx=[...b.rows,...b.payments].filter(r=>r.currency!=='MZN'&&r.fxLink).map(r=>irpcFxDependency(d,r));return JSON.stringify([irpcWithholdingVersion,acquisitionScope(d),b.rows,b.payments,b.rows.map(r=>sourceIssue(d,r)),sharedPayments(d,b),...(fx.length?[fx]:[]),...(b.rows.some(r=>r.specialRelief==='claimed')?[irpcReliefVersion]:[])]);}
|
||
export function irpcCurrent(d:Record<string,unknown>,b:IrpcBook){return !!b.result&&b.result.key===key(d,b);}
|
||
export function recordIrpc(d:Record<string,unknown>,b:IrpcBook,now=new Date().toISOString()):IrpcBook{
|
||
validate(b);const rows=b.rows.map(r=>evaluateIrpc(d,r));const paymentConversions:Record<string,IrpcFxEvidence|null>={};const allocations=new Map<string,FiscalFraction[]>();
|
||
const fail=(i:number,reason:string)=>{rows[i]={...rows[i],status:'indeterminate',reason,settled:null,outstanding:null};};
|
||
b.rows.forEach((r,i)=>{
|
||
if(rows[i].status!=='calculated')return;
|
||
if(rows[i].treatment==='dispensed'){if(r.allocations.length){fail(i,'Dispensa com entrega/prova associada: reconcilie sem apagar o pagamento nem gerar crédito automático.');return;}rows[i].settled=fraction(0n,1n);rows[i].outstanding=fraction(0n,1n);return;}
|
||
if(r.settlement==='none'&&r.allocations.length){fail(i,'Ausência de entrega/prova contradiz pagamentos associados;conserve e reconcilie.');return;}
|
||
if(!r.settlement||!r.settlementRef.trim()){rows[i].reason+=' Entrega à AT/retenção sofrida ainda não reconciliada;não presumir zero pago.';return;}
|
||
const parts:FiscalFraction[]=[];const seen=new Set<string>();
|
||
for(const a of r.allocations){const p=b.payments.find(p=>p.id===a.paymentId);try{
|
||
if(!p||seen.has(p.id)||p.kind==='remittance'&&p.currency!=='MZN'||!date(p.date)||p.date<rows[i].eventDate||!p.reference.trim()||p.kind!==(r.direction==='paid'?'remittance':'withholding_proof'))throw Error();
|
||
seen.add(p.id);const amount=decimalFraction(a.amount);paymentConversions[p.id]=convertIrpcAmount(d,p.amount,p).evidence;parts.push(amount);allocations.set(p.id,[...(allocations.get(p.id)??[]),amount]);
|
||
}catch(e){fail(i,'Pagamento/prova ausente,incoerente,repetido ou de outra natureza:pagamento ao fornecedor e imposto IVA não são entrega IRPC. '+String(e));return;}}
|
||
const settled=addFractions(parts);if(positive(subtractFractions(settled,rows[i].tax!))){fail(i,'Valor associado excede a retenção calculada;reconcilie excesso sem gerar crédito automático.');return;}
|
||
rows[i].settled=settled;rows[i].outstanding=subtractFractions(rows[i].tax!,settled);
|
||
});
|
||
for(const [id,parts]of allocations){const p=b.payments.find(p=>p.id===id)!;let excess=false;try{const elsewhere:FiscalFraction[]=[];for(const [ctx,other]of Object.entries(map(d))){if(ctx===acquisitionScope(d))continue;validate(other);for(const q of other.payments.filter(q=>q.reference.trim()===p.reference.trim()&&q.date===p.date&&q.kind===p.kind&&q.currency===p.currency)){if(q.amount!==p.amount||q.currency!=='MZN'&&JSON.stringify(q.fxSelection)!==JSON.stringify(p.fxSelection))throw Error('Montantes/conversões divergentes');for(const r of other.rows)for(const a of r.allocations.filter(a=>a.paymentId===q.id))elsewhere.push(decimalFraction(a.amount));}}excess=positive(subtractFractions(addFractions([...parts,...elsewhere]),convertIrpcAmount(d,p.amount,p).value));}catch{excess=true;}if(excess)b.rows.forEach((r,i)=>{if(r.allocations.some(a=>a.paymentId===id))fail(i,'Mesmo pagamento utilizado além do montante disponível;reparta sem duplicação.');});}
|
||
// An identical source document/income cannot become a second tax just by adding a row.
|
||
b.rows.forEach((r,i)=>{if(b.rows.some((s,j)=>j!==i&&(r.source&&s.source?.id===r.source.id||r.documentRef.trim()&&s.documentRef.trim()===r.documentRef.trim()&&s.counterparty.trim()===r.counterparty.trim()&&s.direction===r.direction))){fail(i,'Documento/rendimento repetido neste livro;identifique parcelas próprias antes de somar,sem duplicação.');}});
|
||
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 mrftaReportRows(b,false);return [...r.rows.map((v):[string,string,string,string]=>{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]};tratamento:${v.treatment==='dispensed'?'Dispensa de retenção':v.treatment==='treaty'?'Limite convencional':'Retenção interna'};enquadramento: ${irpcKinds.find(([k])=>k===i.kind)?.[1]??i.kind};${i.direction==='paid'?'retida a entregar':'sofrida'};base:${amount(v.base)};taxa:${v.treatment==='dispensed'?'Dispensa (sem retenção)':(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'};dispensa/convenção:${irpcReliefEvidenceLabel(i)};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}`];}),...mrftaReportRows(b,irpcCurrent(d,b))];}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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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>';}
|