Files
Investidor-V2/resources/js/lib/irpc-interest.ts
T

70 lines
7.4 KiB
TypeScript

import type {IrpcBook} from './irpc-withholdings.ts';
import {addFractions,decimalFraction,fraction,subtractFractions,type FiscalFraction} from './fiscal-rational.ts';
export type IrpcInterestRule={
annualRate:string;rateSource:string;fiscalBasis:string;validFrom:string;validTo:string;
dayBasis:''|'365'|'366'|'actual_year';dayBasisRef:string;
rounding:''|'total_cent_half_up'|'total_cent_down';roundingRef:string;
retention:''|'withheld'|'not_withheld';occurrenceRef:string;
};
export const newIrpcInterestRule=():IrpcInterestRule=>({annualRate:'',rateSource:'',fiscalBasis:'',validFrom:'',validTo:'',dayBasis:'',dayBasisRef:'',rounding:'',roundingRef:'',retention:'',occurrenceRef:''});
export const irpcInterestVersion='IRPC-interest/1';
export type IrpcInterestSegment={from:string;to:string;days:number;denominator:number;principal:FiscalFraction;interest:FiscalFraction};
export type IrpcInterestCalculation={version:string;rows:Array<{id:string;document:string;segments:IrpcInterestSegment[];exact:FiscalFraction}>;pending:string[];exact:FiscalFraction|null;total:FiscalFraction|null;rule:IrpcInterestRule;dueDate:string;liquidationDate:string;source:string};
const validDate=(s:string)=>/^20\d{2}-\d{2}-\d{2}$/.test(s)&&Number.isFinite(Date.parse(s))&&new Date(s).toISOString().slice(0,10)===s;
const next=(s:string)=>new Date(Date.parse(s)+86400000).toISOString().slice(0,10);
const less=(a:FiscalFraction,b:FiscalFraction)=>BigInt(a.numerator)*BigInt(b.denominator)<BigInt(b.numerator)*BigInt(a.denominator);
const zero=()=>fraction(0n,1n);
export function validateIrpcInterestRule(r:IrpcInterestRule){
if(!r||Object.keys(newIrpcInterestRule()).some(k=>typeof r[k as keyof IrpcInterestRule]!=='string')||!['','365','366','actual_year'].includes(r.dayBasis)||!['','total_cent_half_up','total_cent_down'].includes(r.rounding)||!['','withheld','not_withheld'].includes(r.retention))throw Error('Parâmetros de juros não recuperáveis;originais conservados.');
}
// No fiscal rate, annual day basis or rounding convention is supplied by default.
// Complete references describe the parameters of this case, not a global 2026 rate.
export function calculateIrpcInterest(b:IrpcBook,current:boolean,period:string,dueDate:string,dueReference:string,liquidationDate:string,r:IrpcInterestRule):IrpcInterestCalculation{
validateIrpcInterestRule(r);
const pending:string[]=[],rows:IrpcInterestCalculation['rows']=[];
const result=(exact:FiscalFraction|null=null,total:FiscalFraction|null=null):IrpcInterestCalculation=>({version:irpcInterestVersion,rows,pending,exact,total,rule:structuredClone(r),dueDate,liquidationDate,source:'Decreto9/2008 art35(1/2):dia imediato ao prazo até pagamento/liquidação;25(1):parâmetro na data da liquidação. Taxa,base anual e arredondamento apenas conforme referências do caso. Sem parâmetro fiscal2026 pré-definido,sem MIMO/Prime presumidos. Mora e retenção por conta não efectuada conservam regra própria.'});
if(!current||!b.result)pending.push('Retenções sem cálculo actual.');
if(!/^2026-(0[1-9]|1[0-2])$/.test(period)||!validDate(dueDate)||!dueReference.trim()||!validDate(liquidationDate)||liquidationDate<=dueDate||dueDate<period+'-01')pending.push('Identifique período,prazo documentado e data de liquidação posterior ao prazo;sem prazo/calendário presumidos.');
if(!validDate(r.validFrom)||!validDate(r.validTo)||r.validFrom>r.validTo||liquidationDate<r.validFrom||liquidationDate>r.validTo)pending.push('Parâmetro fora da versão/período aplicável à data de liquidação.');
const labels:Record<string,string>={rateSource:'fonte da taxa anual final',fiscalBasis:'fundamento fiscal aplicável',dayBasisRef:'fonte da base anual',roundingRef:'fonte do arredondamento',occurrenceRef:'documento da retenção/ocorrência'};
for(const[k,label]of Object.entries(labels))if(!r[k as keyof IrpcInterestRule].trim())pending.push('Falta '+label+'.');
if(!r.dayBasis||!r.rounding||!r.retention)pending.push('Regra de base anual,arredondamento e situação da retenção por identificar.');
let rate:FiscalFraction;
try{rate=decimalFraction(r.annualRate);if(!less(zero(),rate))throw Error();}catch{pending.push('Taxa anual final aplicável por identificar;sem zero ou taxa bancária de substituição.');return result();}
if(pending.length)return result();
const selected=b.result!.rows.filter(v=>v.direction==='paid'&&v.period===period&&v.treatment!=='dispensed');
if(!selected.length){pending.push('Sem retenções a entregar seleccionadas para este período.');return result();}
for(const v of selected){
const input=b.result!.inputs.find(i=>i.id===v.id);
if(!input||v.status!=='calculated'||!v.tax||less(v.tax,zero())||v.settled===null||!['none','complete'].includes(input.settlement)||!input.settlementRef.trim()||!v.nature){pending.push(v.id+': retenção/entregas não reconciliadas.');continue;}
if(v.nature==='on_account'&&r.retention!=='withheld'){pending.push(input.documentRef+': imposto por conta não retido exige o termo da declaração anual e responsabilidade correspondentes;fora deste cálculo.');continue;}
let payments:Array<{date:string;amount:FiscalFraction}>=[];
try{
const seen=new Set<string>();
payments=input.allocations.map(a=>{const p=b.result!.payments.find(p=>p.id===a.paymentId);if(!p||seen.has(p.id)||p.kind!=='remittance'||p.currency!=='MZN'||!validDate(p.date)||p.date<v.eventDate||!p.reference.trim()||p.date>liquidationDate)throw Error();seen.add(p.id);const amount=decimalFraction(a.amount);if(!less(zero(),amount))throw Error();return{date:p.date,amount};});
const allocated=addFractions(payments.map(p=>p.amount));if(less(v.tax,allocated)||!isEqual(allocated,v.settled))throw Error();
}catch{pending.push(input.documentRef+': comprovativos incoerentes ou posteriores à data de liquidação;conserve e reconcilie.');continue;}
let principal=subtractFractions(v.tax,addFractions(payments.filter(p=>p.date<=dueDate).map(p=>p.amount))),cursor=next(dueDate);
const segments:IrpcInterestSegment[]=[];
const charge=(to:string)=>{
if(cursor>to||!less(zero(),principal))return;
while(cursor<=to){
const year=Number(cursor.slice(0,4)),yearEnd=year+'-12-31',end=r.dayBasis==='actual_year'&&yearEnd<to?yearEnd:to;
const denominator=r.dayBasis==='actual_year'?(year%4===0&&(year%100!==0||year%400===0)?366:365):Number(r.dayBasis);
const days=(Date.parse(end)-Date.parse(cursor))/86400000+1;
const interest=fraction(BigInt(principal.numerator)*BigInt(rate.numerator)*BigInt(days),BigInt(principal.denominator)*BigInt(rate.denominator)*100n*BigInt(denominator));
segments.push({from:cursor,to:end,days,denominator,principal,interest});cursor=next(end);
}
};
const grouped=new Map<string,FiscalFraction[]>();for(const p of payments.filter(p=>p.date>dueDate))grouped.set(p.date,[...(grouped.get(p.date)??[]),p.amount]);
for(const[day,parts]of [...grouped].sort(([a],[c])=>a.localeCompare(c))){charge(day);principal=subtractFractions(principal,addFractions(parts));cursor=next(day);}
charge(liquidationDate);rows.push({id:v.id,document:input.documentRef,segments,exact:addFractions(segments.map(s=>s.interest))});
}
if(pending.length)return result();
const exact=addFractions(rows.map(r=>r.exact)),scaled=BigInt(exact.numerator)*100n,d=BigInt(exact.denominator);
let cents=scaled/d;if(r.rounding==='total_cent_half_up'&&scaled%d*2n>=d)cents++;
return result(exact,fraction(cents,100n));
}
function isEqual(a:FiscalFraction,b:FiscalFraction){return BigInt(a.numerator)*BigInt(b.denominator)===BigInt(b.numerator)*BigInt(a.denominator);}