57 lines
14 KiB
TypeScript
57 lines
14 KiB
TypeScript
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.']];}}
|