Files
Investidor-V2/resources/js/lib/iva-refund.ts
T

57 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {assessFiscalClaim} from './fiscal-claim-events.ts';
import {closureBook,closureCurrent,closureFingerprint} from './iva-closure.ts';
import {subtractFractions,fraction,fractionPresentation,type FiscalFraction} from './fiscal-rational.ts';
export const refundSource='L3-D:CIVA21(5/6/7),Lei13/2016 PDF15–16,p1426(273–274);Lei22/2022 PDF3–4;Lei10/2025 PDF13/p2180(13),21(7a/8revogado/10/13/15). ModeloA/VAT Return9c0b99a8,p2/quadro11:26+27=25. Pedido,decisão e recebimento separados. Formalidades doRegulamento78/2017/30/2022/52/2025 não automatizadas;sem concessão automática.';
type Choice=''|'yes'|'no';
export type RefundSource={closureId:string;period:string;entityRef:string;origin:'projected'|'effective';allocationKey:string;calculatedAt:string;credit:FiscalFraction;carry:FiscalFraction;requested:FiscalFraction};
export type RefundReceipt={id:string;reference:string;date:string;amount:string};
export type RefundInput={id:string;source:RefundSource|null;ground:''|'four_months'|'high_credit'|'cessation';originPeriod:string;entitlementDate:string;cessationDate:string;groundRef:string;factsConfirmed:Choice;oldestFirst:Choice;stage:'preparation'|'submitted';requestReference:string;requestDate:string;decision:'pending'|'approved'|'partial'|'rejected';decisionReference:string;decisionDate:string;decisionIssuer:string;approvedAmount:string;receipts:RefundReceipt[]};
export type RefundResult={id:string;status:'determined'|'indeterminate';reason:string;inputKey:string;eligibility:'compatible'|'not_demonstrated'|'incompatible';requested:FiscalFraction|null;reported:FiscalFraction|null;approved:FiscalFraction|null;received:FiscalFraction|null;awaitingReceipt:FiscalFraction|null;notGranted:FiscalFraction|null;source:string};
export type RefundCalculation={calculatedAt:string;key:string;inputs:RefundInput[];rows:RefundResult[]};
export type RefundBook={version:1;rows:RefundInput[];result?:RefundCalculation;history?:RefundCalculation[]};
const scope=(d:Record<string,unknown>)=>String(d.context_opportunity||'general'),norm=(s:string)=>s.trim().toLocaleLowerCase();
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 period=(s:string)=>/^20\d{2}-(0[1-9]|1[0-2])$/.test(s);
const months=(a:string,b:string)=>(Number(b.slice(0,4))-Number(a.slice(0,4)))*12+Number(b.slice(5))-Number(a.slice(5));
function store(d:Record<string,unknown>):Record<string,unknown>{const s=JSON.parse(String(d.modelo_a_snapshot||'{"version":1,"books":{}}'));if(s.version!==1||!s.books)throw Error('Livros não recuperáveis;originais preservados.');return s;}
function maps(d:Record<string,unknown>):Record<string,RefundBook>{const m=store(d).ivaRefunds;if(m===undefined)return{};if(!m||typeof m!=='object'||Array.isArray(m))throw Error('Mapa de reembolsos não recuperável;sem o substituir.');return m as Record<string,RefundBook>;}
function validate(b:RefundBook):void{if(!b||b.version!==1||!Array.isArray(b.rows)||b.history!==undefined&&!Array.isArray(b.history)||b.result!==undefined&&(!Array.isArray(b.result.rows)||!Array.isArray(b.result.inputs)))throw Error('Livro de reembolsos não recuperável;originais preservados.');for(const r of b.rows)if(!r||!['id','originPeriod','entitlementDate','cessationDate','groundRef','requestReference','requestDate','decisionReference','decisionDate','decisionIssuer','approvedAmount'].every(k=>typeof r[k as keyof RefundInput]==='string')||!['','four_months','high_credit','cessation'].includes(r.ground)||!['','yes','no'].includes(r.factsConfirmed)||!['','yes','no'].includes(r.oldestFirst)||!['preparation','submitted'].includes(r.stage)||!['pending','approved','partial','rejected'].includes(r.decision)||!Array.isArray(r.receipts)||r.receipts.some(p=>!p||!['id','reference','date','amount'].every(k=>typeof p[k as keyof RefundReceipt]==='string')))throw Error('EntradasD não recuperáveis;sem montantes presumidos.');}
export function refundBook(d:Record<string,unknown>):RefundBook{const b=maps(d)[scope(d)]??{version:1,rows:[]};validate(b);return b;}
export function saveRefundBook(d:Record<string,unknown>,b:RefundBook):string{validate(b);const s=store(d);return JSON.stringify({...s,ivaRefunds:{...maps(d),[scope(d)]:b}});}
export function refundSources(d:Record<string,unknown>):RefundSource[]{const b=closureBook(d);if(!closureCurrent(d,b))return[];return(b.result?.rows??[]).flatMap(r=>{const p=r.fields['27'],c=r.fields['25'],carry=r.fields['26'];if(r.status!=='determined'||!p||!c||!carry||BigInt(p.numerator)<=0n)return[];const dependentUnreconciled=b.rows.some(input=>input.credits.some(credit=>credit.kind==='previous'&&credit.previousId===r.id)&&!b.result!.rows.some(result=>result.id===input.id&&result.status==='determined'));if(dependentUnreconciled)return[];return[{closureId:r.id,period:r.period,entityRef:r.entityRef,origin:r.origin,allocationKey:closureFingerprint([r.id,r.entityRef,r.origin,r.period,c,carry,p]),calculatedAt:b.result!.calculatedAt,credit:c,carry,requested:p}];});}
export function newRefund(id:string,source:RefundSource|null):RefundInput{return{id,source:source?structuredClone(source):null,ground:'',originPeriod:'',entitlementDate:'',cessationDate:'',groundRef:'',factsConfirmed:'',oldestFirst:'',stage:'preparation',requestReference:'',requestDate:'',decision:'pending',decisionReference:'',decisionDate:'',decisionIssuer:'',approvedAmount:'',receipts:[]};}
// A new calculation timestamp alone does not change an identical allocation.
function available(d:Record<string,unknown>,r:RefundInput):boolean{return !!r.source&&refundSources(d).some(s=>s.allocationKey===r.source!.allocationKey&&s.closureId===r.source!.closureId&&s.entityRef===r.source!.entityRef&&s.origin===r.source!.origin&&s.period===r.source!.period&&JSON.stringify([s.credit,s.carry,s.requested])===JSON.stringify([r.source!.credit,r.source!.carry,r.source!.requested]));}
function identity(r:RefundInput):string{return JSON.stringify([norm(r.source?.entityRef??''),r.source?.origin,r.source?.period,r.source?.closureId]);}
function uses(d:Record<string,unknown>,b:RefundBook):RefundInput[]{return[...b.rows,...Object.entries(maps(d)).filter(([id])=>id!==scope(d)).flatMap(([,v])=>{validate(v);return v.rows.filter(r=>r.source?.origin==='effective');})];}
function receiptIdentity(r:RefundInput,p:RefundReceipt):string{return JSON.stringify([norm(r.source?.entityRef??''),r.source?.origin,norm(p.reference)]);}
function past(d:Record<string,unknown>,b:RefundBook):RefundInput[]{return Object.entries({...maps(d),[scope(d)]:b}).flatMap(([context,v])=>[...(v.history??[]),...(v.result?[v.result]:[])].flatMap(c=>c.inputs.filter(r=>context===scope(d)||r.source?.origin==='effective')));}
function key(d:Record<string,unknown>,b:RefundBook):string{const all=uses(d,b),old=past(d,b);return closureFingerprint([b.rows,b.rows.map(r=>available(d,r)),all.map(r=>[r.id,identity(r),r.receipts.map(p=>receiptIdentity(r,p))]),b.rows.map(r=>old.filter(v=>v.id!==r.id&&(v.stage==='submitted'||v.decision!=='pending'||v.receipts.length)).some(v=>identity(v)===identity(r)||r.receipts.some(p=>v.receipts.some(q=>receiptIdentity(v,q)===receiptIdentity(r,p)))))]);}
export function refundCurrent(d:Record<string,unknown>,b:RefundBook):boolean{return!!b.result&&b.result.key===key(d,b)&&b.result.rows.length===b.rows.length&&b.rows.every(r=>b.result!.rows.some(v=>v.id===r.id&&v.inputKey===closureFingerprint(r)));}
export function recordRefund(d:Record<string,unknown>,b:RefundBook,now=new Date().toISOString()):RefundBook{
validate(b);if(new Set(b.rows.map(r=>r.id)).size!==b.rows.length)throw Error('Identidade de pedido repetida.');const all=uses(d,b),old=past(d,b);
const rows=b.rows.map(row=>{
const r:RefundResult={id:row.id,status:'indeterminate',reason:'',inputKey:closureFingerprint(row),eligibility:'not_demonstrated',requested:null,reported:null,approved:null,received:null,awaitingReceipt:null,notGranted:null,source:refundSource};const stop=(reason:string)=>({...r,reason});const s=row.source;
if(!s||!available(d,row)||d.currency!=='MZN'||!/^2026-(0[1-9]|1[0-2])$/.test(s.period))return stop('Escolha afectação26/27 actual e recuperável do mesmo período2026/MZN;crédito já reportado não é pedido disponível.');
if(all.filter(v=>identity(v)===identity(row)).length>1||old.some(v=>v.id!==row.id&&identity(v)===identity(row)&&(v.stage==='submitted'||v.decision!=='pending'||v.receipts.length)))return stop('Parcela já ligada a outro pedido no âmbito recuperado;sem pedir ou consumir duas vezes o mesmo crédito.');
r.requested=s.requested;r.reported=s.carry;
let eligibilityReason='';
if(!row.ground||!period(row.originPeriod)||row.originPeriod>s.period||!date(row.entitlementDate)||row.entitlementDate.slice(0,7)!==row.originPeriod||!row.groundRef.trim()||row.factsConfirmed!=='yes'||row.oldestFirst!=='yes')eligibilityReason='Fundamento/origem/nascimento do crédito,suporte ou consideração dos créditos mais antigos ainda não demonstrados;sem direito presumido.';
if(!date(row.requestDate)||row.requestDate.slice(0,7)<s.period||!row.requestReference.trim())return stop('Identifique referência e data do pedido ou preparação;separar da decisão.');
if(!eligibilityReason){
const lastYear=Number(row.entitlementDate.slice(0,4))+10,lastMonth=Number(row.entitlementDate.slice(5,7)),lastDay=Math.min(Number(row.entitlementDate.slice(8)),new Date(Date.UTC(lastYear,lastMonth,0)).getUTCDate());const anniversary=`${lastYear}-${String(lastMonth).padStart(2,'0')}-${String(lastDay).padStart(2,'0')}`;
const above=(n:bigint)=>BigInt(subtractFractions(s.credit,fraction(n)).numerator)>0n;
if(row.requestDate<row.entitlementDate||row.requestDate>anniversary){r.eligibility='incompatible';eligibilityReason='Datas declaradas incompatíveis com nascimento do direito/limite10anos de21(15),Lei10/2025;conservar documentos e identificar eventual excepção.';}
else if(row.ground==='four_months'&&(months(row.originPeriod,s.period)<4||!above(100000n))||row.ground==='high_credit'&&!above(500000n)||row.ground==='cessation'&&(!date(row.cessationDate)||row.cessationDate>row.requestDate)){r.eligibility='incompatible';eligibilityReason='Factos declarados não demonstram21(6),quatro meses/crédito superior100000;21(7a),superior500000;ou21(7b),cessação identificada.';}
else r.eligibility='compatible';
}
const events=assessFiscalClaim(row,s.requested,p=>{const id=receiptIdentity(row,p);return all.flatMap(v=>v.receipts.filter(q=>receiptIdentity(v,q)===id)).length>1||old.some(v=>v.id!==row.id&&v.receipts.some(q=>receiptIdentity(v,q)===id));});Object.assign(r,events);
if(events.status==='indeterminate')return r;
if(row.decision==='pending'){if(eligibilityReason){r.status='indeterminate';return stop(eligibilityReason+' Pedido/estado conservados;formalidades regulamentares não automatizadas.');}r.reason=row.stage==='preparation'?'Pedido em preparação;parcela separada do reporte,sem submissão,decisão ou recebimento.':'Pedido apresentado fora doInvesteMoz;decisão e recebimento ainda não identificados.';return r;}
r.reason=(eligibilityReason?'Decisão externa documentada registada independentemente da avaliação automática do fundamento: '+eligibilityReason+' ':'')+'Pedido,decisão e recebimento separados;parcela27 não é reutilizada em26. Parte não deferida/indeferimento não libertam crédito automaticamente;eventual retorno exige procedimento documentado e reconciliação,sem duplicar20. Recebimento é registo declarado,sem execução bancária ou alteração silenciosa de caixa. Formalidades e suspensões não automatizadas;150dias não são promessa de recebimento.';return r;
});
const result={calculatedAt:now,key:key(d,b),inputs:structuredClone(b.rows),rows};return{...b,...(b.result?{history:[...(b.history??[]),structuredClone(b.result)]}:{}),result};
}
export function refundReportRows(d:Record<string,unknown>):Array<[string,string,string,string]>{try{const b=refundBook(d);if(!b.rows.length)return[];const out:Array<[string,string,string,string]>=[['IVA mensal','L3-D — reembolso geral',refundCurrent(d,b)?'Resultado correspondente às entradas':'Histórico preservado;rever entradas/afectação',refundSource]],fmt=(v:FiscalFraction|null|undefined)=>v?fractionPresentation(v).decimal:'Indeterminado';for(const r of b.result?.rows??[]){const input=b.result!.inputs.find(v=>v.id===r.id),s=input?.source;out.push(['IVA mensal',`Reembolso — ${s?.period??'por identificar'}`,`Fundamento:${r.eligibility==='compatible'?'Compatível com factos declarados':r.eligibility==='incompatible'?'Divergência nos factos — ver suporte':'Não demonstrado automaticamente'};Pedido27:${fmt(r.requested)};reporte26 separado:${fmt(r.reported)};reconhecido:${fmt(r.approved)};recebido:${fmt(r.received)};aguarda recebimento:${fmt(r.awaitingReceipt)};não deferido:${fmt(r.notGranted)}`,`${s?.entityRef??''};${scope(d)};MZN;${s?.origin==='effective'?'Dados efectivos declarados':'Pressuposto de simulação'};${input?.stage==='submitted'?'Apresentado fora doInvesteMoz':'Em preparação'};pedido ${input?.requestReference??''},${input?.requestDate??''};origem${input?.originPeriod??''},nascimento${input?.entitlementDate??''};${input?.groundRef??''};${r.reason}`]);out.push(['IVA mensal',`Decisão e recebimento — ${s?.period??''}`,`Decisão:${input?.decision==='approved'?'Deferido':input?.decision==='partial'?'Deferimento parcial':input?.decision==='rejected'?'Indeferido':'Pendente'};documento${input?.decisionReference??''};${input?.decisionDate??''};${input?.decisionIssuer??''};recebimentos:${input?.receipts.map(p=>`${p.reference};${p.date};${p.amount}MZN`).join(' | ')||'nenhum registado'}`,`Afectação do fecho${s?.closureId??''},versão${s?.calculatedAt??''};25:${fmt(s?.credit)}=26:${fmt(s?.carry)}+27:${fmt(s?.requested)};âmbito apenas desta parcela;versão guardada${b.result!.calculatedAt};${refundSource}`]);}return out;}catch{return[['IVA mensal','L3-D','Pedido não recuperável','Originais preservados;sem concessão/recebimento presumidos.']];}}