44 lines
11 KiB
TypeScript
44 lines
11 KiB
TypeScript
import {acquisitionBook,acquisitionCurrent,type Acquisition} from './iva-acquisitions.ts';
|
||
import {beverageParcel} from './iva-beverage-parcels.ts';
|
||
import {closureFingerprint} from './iva-closure.ts';
|
||
import {assessFiscalClaim,newFiscalClaimEvents,fiscalClaimDate,type FiscalClaimEvents,type FiscalClaimState} from './fiscal-claim-events.ts';
|
||
import {decimalFraction,fractionPresentation,type FiscalFraction} from './fiscal-rational.ts';
|
||
import {beverageRange,beverageRangesOverlap,beverageDeductionEvidence,reconcileBeverageAllocation,type BeverageAllocation} from './iva-beverage-allocation.ts';
|
||
export const beverageRestitutionSource='L3-E:CIVA13(1)(c/d/h),13(2–3),Lei13/2016 PDF12/p1426(270). Bebidas:dedução ou restituição,sem aplicar20(1)(d);não isenção directa. Provisões de pesca costeira excluídas. Linha integral ou parcelas documentadas sem sobreposição;dedução efectiva anterior reconciliada por montante e suporte,sem apagar históricos. Procedimento/formulário específico não automatizado;sem importar limites/prazos do reembolso geral21.';
|
||
export type BeverageRestitutionSource={parcel:string;acquisitionId:string;scope:string;period:string;origin:'projected'|'effective';documentRef:string;documentDate:string;documentLine:string;supplier:string;vat:FiscalFraction;fingerprint:string;operationId:string;operationRef:string;calculatedAt:string};
|
||
export type BeverageRestitutionInput=FiscalClaimEvents&{id:string;source:BeverageRestitutionSource|null;entityRef:string;exclusive:''|'yes'|'no';allocation?:BeverageAllocation};
|
||
export type BeverageRestitutionResult=FiscalClaimState&{id:string;inputKey:string;requested:FiscalFraction|null;source:string};
|
||
type Calc={calculatedAt:string;key:string;inputs:BeverageRestitutionInput[];rows:BeverageRestitutionResult[]};
|
||
export type BeverageRestitutionBook={version:1;rows:BeverageRestitutionInput[];result?:Calc;history?:Calc[]};
|
||
const scope=(d:Record<string,unknown>)=>String(d.context_opportunity||'general'),norm=(s:string)=>s.trim().toLocaleLowerCase();
|
||
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,BeverageRestitutionBook>{const m=store(d).ivaBeverageRestitutions;if(m===undefined)return{};if(!m||typeof m!=='object'||Array.isArray(m))throw Error('Mapa de restituições não recuperável;originais preservados.');return m as Record<string,BeverageRestitutionBook>;}
|
||
function validate(b:BeverageRestitutionBook):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('LivroE não recuperável;originais preservados.');for(const r of b.rows)if(!r||!['id','entityRef','requestReference','requestDate','decisionReference','decisionDate','decisionIssuer','approvedAmount'].every(k=>typeof r[k as keyof BeverageRestitutionInput]==='string')||r.allocation!==undefined&&(!r.allocation||!['from','to','reference','deductionFrom','deductionTo','deductionReference'].every(k=>typeof r.allocation![k as keyof BeverageAllocation]==='string'))||!['','yes','no'].includes(r.exclusive)||!['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 typeof p]==='string')))throw Error('EntradasE não recuperáveis;sem apagar documentos.');}
|
||
export function beverageRestitutionBook(d:Record<string,unknown>):BeverageRestitutionBook{const b=maps(d)[scope(d)]??{version:1,rows:[]};validate(b);return b;}
|
||
export function saveBeverageRestitutionBook(d:Record<string,unknown>,b:BeverageRestitutionBook):string{validate(b);const s=store(d);return JSON.stringify({...s,ivaBeverageRestitutions:{...maps(d),[scope(d)]:b}});}
|
||
export function beverageRestitutionSources(d:Record<string,unknown>):BeverageRestitutionSource[]{const b=acquisitionBook(d);if(!acquisitionCurrent(d,b))return[];return b.rows.flatMap(row=>{const result=b.result!.rows.find(r=>r.id===row.id);if(row.bc?.expense!=='beverages'||row.bc.beveragePath!=='refund'||!result?.reason.startsWith('Via restituição13(2)')||result.deductible!=='0.00'||!result.vat||!row.deductionLink)return[];const vat=decimalFraction(result.vat);if(BigInt(vat.numerator)<=0n)return[];return[{parcel:beverageParcel(row),acquisitionId:row.id,scope:scope(d),period:row.period,origin:row.origin,documentRef:row.documentRef,documentDate:row.documentDate,documentLine:row.documentLine,supplier:row.supplier,vat,fingerprint:closureFingerprint([row,result]),operationId:row.deductionLink.operationId,operationRef:row.deductionLink.allocationRef,calculatedAt:b.result!.calculatedAt}];});}
|
||
export function newBeverageRestitution(id:string,source:BeverageRestitutionSource|null,entityRef=''):BeverageRestitutionInput{return{...newFiscalClaimEvents(),id,source:source?structuredClone(source):null,entityRef,exclusive:''};}
|
||
function sameSource(d:Record<string,unknown>,r:BeverageRestitutionInput):boolean{return!!r.source&&beverageRestitutionSources(d).some(s=>s.scope===r.source!.scope&&s.parcel===r.source!.parcel&&s.fingerprint===r.source!.fingerprint);}
|
||
function allUses(d:Record<string,unknown>,b:BeverageRestitutionBook):BeverageRestitutionInput[]{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 past(d:Record<string,unknown>,b:BeverageRestitutionBook):BeverageRestitutionInput[]{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')&&c.rows.some(v=>v.id===r.id&&v.status==='determined'&&v.requested&&BigInt(v.requested.numerator)>0n))));}
|
||
function committedParcel(r:BeverageRestitutionInput):string{if(!r.source)return '';try{const range=beverageRange(r.source,r.allocation);return closureFingerprint([r.source.parcel,r.source.origin,r.source.vat,range.from,range.to]);}catch{return 'invalid';}}
|
||
const receiptKey=(r:BeverageRestitutionInput,p:FiscalClaimEvents['receipts'][number])=>JSON.stringify([norm(r.entityRef),r.source?.origin,norm(p.reference)]);
|
||
function key(d:Record<string,unknown>,b:BeverageRestitutionBook):string{return closureFingerprint([b.rows,b.rows.map(r=>[sameSource(d,r),r.source?beverageDeductionEvidence(d,r.source):null]),allUses(d,b).map(r=>[r.id,r.source,r.allocation,r.receipts.map(p=>receiptKey(r,p))]),past(d,b).filter(r=>r.stage==='submitted'||r.decision!=='pending'||r.receipts.length).map(r=>[r.id,r.source,r.allocation,r.receipts.map(p=>receiptKey(r,p))])]);}
|
||
export function beverageRestitutionCurrent(d:Record<string,unknown>,b:BeverageRestitutionBook):boolean{return!!b.result&&b.result.key===key(d,b)&&b.rows.length===b.result.rows.length&&b.rows.every(r=>b.result!.rows.some(v=>v.id===r.id&&v.inputKey===closureFingerprint(r)));}
|
||
export function recordBeverageRestitution(d:Record<string,unknown>,b:BeverageRestitutionBook,now=new Date().toISOString()):BeverageRestitutionBook{
|
||
validate(b);if(new Set(b.rows.map(r=>r.id)).size!==b.rows.length)throw Error('IdentidadeE repetida.');const uses=allUses(d,b),history=past(d,b);
|
||
const rows=b.rows.map(row=>{const r:BeverageRestitutionResult={id:row.id,inputKey:closureFingerprint(row),status:'indeterminate',reason:'',requested:null,approved:null,received:null,awaitingReceipt:null,notGranted:null,source:beverageRestitutionSource},stop=(reason:string)=>({...r,reason}),s=row.source;
|
||
if(!s||!sameSource(d,row)||d.currency!=='MZN'||s.scope!==scope(d))return stop('OrigemL1/L2 alterada ou ausente;sem restituição presumida.');
|
||
if(!row.entityRef.trim()||row.exclusive!=='yes')return stop('Identifique entidade e confirme a parcela exclusivamente para restituição;sem dupla utilização.');
|
||
const reconciled=reconcileBeverageAllocation(d,s,row.allocation);if(!reconciled.range)return stop(reconciled.reason);
|
||
const related=(v:BeverageRestitutionInput)=>v.source?.origin===s.origin&&v.source?.parcel===s.parcel;
|
||
const overlaps=(v:BeverageRestitutionInput)=>{try{return beverageRangesOverlap(reconciled.range!,beverageRange(v.source!,v.allocation));}catch{return true;}};
|
||
if(uses.some(v=>v!==row&&related(v)&&overlaps(v))||history.some(v=>related(v)&&(v.stage==='submitted'||v.decision!=='pending'||v.receipts.length)&&(v.id!==row.id?overlaps(v):committedParcel(v)!==committedParcel(row))))return stop('Parcela documental já ligada a pedido ou histórico comprometido;limites sobrepostos ou alterados. Sem duplicar nem libertar IVA por apagar a preparação.');
|
||
r.requested=reconciled.range.amount;if(!fiscalClaimDate(row.requestDate)||row.requestDate<s.documentDate)return stop('Data do pedido/preparação anterior ao documento ou por identificar.');
|
||
const events=assessFiscalClaim(row,reconciled.range.amount,p=>uses.flatMap(v=>v.receipts.filter(q=>receiptKey(v,q)===receiptKey(row,p))).length>1||history.some(v=>v.id!==row.id&&v.receipts.some(q=>receiptKey(v,q)===receiptKey(row,p))));Object.assign(r,events);
|
||
if(r.status==='determined')r.reason+=' Linha13(2) fora da deduçãoL2 e do crédito geralD;pedido não é isenção directa nem concessão automática. Decisão e recebimento registados fora doInvesteMoz;procedimento/formulário específico e prazos não automatizados,sem os importar deD.';return r;
|
||
});
|
||
const result={calculatedAt:now,key:'',inputs:structuredClone(b.rows),rows};const next={...b,...(b.result?{history:[...(b.history??[]),structuredClone(b.result)]}:{}),result};result.key=key(d,next);return next;
|
||
}
|
||
export function beverageRestitutionReportRows(d:Record<string,unknown>):Array<[string,string,string,string]>{try{const b=beverageRestitutionBook(d);if(!b.rows.length)return[];const out:Array<[string,string,string,string]>=[['IVA mensal','L3-E — restituição de bebidas',beverageRestitutionCurrent(d,b)?'Resultado correspondente às entradas':'Histórico preservado;rever origem/parcela',beverageRestitutionSource]],fmt=(v:FiscalFraction|null)=>v?fractionPresentation(v).decimal:'Indeterminado';for(const r of b.result?.rows??[]){const i=b.result!.inputs.find(v=>v.id===r.id),s=i?.source;out.push(['IVA mensal',`Restituição de bebidas — ${s?.period??''}`,`IVA documental:${fmt(s?.vat??null)};parcelaIVA:${fmt(r.requested)};dedução desta parcela:${r.requested?'0pela via exclusivaE':'Indeterminada;ver origem'};reconhecido:${fmt(r.approved)};recebido:${fmt(r.received)};aguarda recebimento:${fmt(r.awaitingReceipt)}`,`${i?.entityRef??''};${scope(d)};${s?.origin==='effective'?'Dados efectivos declarados':'Pressuposto de simulação'};${s?.documentRef??''};${s?.documentDate??''};linha${s?.documentLine??''};operaçãoL1${s?.operationId??''};${s?.operationRef??''};pedido${i?.requestReference??''},${i?.requestDate??''};decisão${i?.decisionReference??''},${i?.decisionDate??''},${i?.decisionIssuer??''};recibos:${i?.receipts.map(p=>`${p.reference};${p.date};${p.amount}MZN`).join(' | ')||'nenhum registado'};${i?.allocation?`repartiçãoIVA[${i.allocation.from},${i.allocation.to}[;suporte${i.allocation.reference};dedução anterior[${i.allocation.deductionFrom},${i.allocation.deductionTo}[;${i.allocation.deductionReference};`:''}${r.reason}`]);}return out;}catch{return[['IVA mensal','L3-E','Restituição não recuperável','Originais preservados;sem dedução/restituição presumidas.']];}}
|