import type {AcquisitionCalculation} from './iva-acquisitions.ts'; import {addFractions,decimalFraction,fraction,subtractFractions,positiveFraction,fractionLabel,fractionPresentation,type FiscalFraction} from './fiscal-rational.ts'; export const closureVersion='iva-closure-ab/1'; export const closureSource='L3-A/B: CIVA21(2/5),Lei13/2016 PDF15/p1426(273),23(1),PDF16/p1426(274);RCIVA12/15,Decreto7/2008 PDF3/p101;ModeloA/VAT Return9c0b99a8,p1–2,quadros8/10/11. Crédito anterior/comunicado19/20;22=max(17−19−20,0);25=max(18+19+20−17,0);24=22+23;reporte integral26=25 quando expressamente escolhido. Sem substituição21,reembolso27 ou restituição13(2). Cabeçalho remete18/19 mas caixas são19/20;divergência conservada,distinta deP00105/06.'; type Choice=''|'yes'|'no'; export type ClosureSource={revision:number;calculatedAt:string;fingerprint:string;opportunity:string;currency:string;period:string;location:string;projected:boolean;debit:FiscalFraction|null;credit:FiscalFraction|null}; export type ClosureCredit={id:string;kind:'opening'|'communication'|'previous';period:string;reference:string;date:string;amount:string;previousId:string;carryKey:string}; export type ClosurePayment={id:string;reference:string;date:string;amount:string}; export type ClosureInput={id:string;period:string;entityRef:string;origin:'projected'|'effective';source:ClosureSource|null;firstDeclaration:Choice;filing:''|'timely'|'late';submissionDate:string;filingRef:string;creditsComplete:Choice;credits:ClosureCredit[];interest:''|'none'|'documented';interestAmount:string;interestRef:string;destination:''|'report'|'refund';refundAmount?:string;payments:ClosurePayment[]}; export type ClosureResult={id:string;period:string;entityRef:string;origin:ClosureInput['origin'];status:'determined'|'indeterminate';reason:string;inputKey:string;financialKey:string;carryKey:string;fields:Record;paid:FiscalFraction|null;outstanding:FiscalFraction|null;overpayment:FiscalFraction|null;source:string}; export type ClosureBook={version:1;rows:ClosureInput[];result?:{calculatedAt:string;key:string;inputs:ClosureInput[];rows:ClosureResult[]};history?:Array>}; const scope=(d:Record)=>String(d.context_opportunity||'general'); const zero=()=>fraction(0n); export function closureFingerprint(v:unknown):string{const raw=JSON.stringify(v);let h=14695981039346656037n;for(let i=0;i):Record{const s=JSON.parse(String(d.modelo_a_snapshot||'{"version":1,"books":{}}'));if(s.version!==1||!s.books)throw Error('Livros fiscais não recuperáveis;originais preservados.');return s;} function maps(d:Record):Record{const v=store(d).ivaClosures;if(v===undefined)return{};if(!v||typeof v!=='object'||Array.isArray(v))throw Error('Mapa de fechos não recuperável;originais preservados.');return v as Record;} function validate(b:ClosureBook):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)||typeof b.result.key!=='string'))throw Error('FechoL3 não recuperável;originais preservados.'); if(b.result?.rows.some(r=>!r||typeof r.id!=='string'||!r.fields||typeof r.fields!=='object'||!['determined','indeterminate'].includes(r.status)||Object.values(r.fields).some(v=>v!==null&&(!v||!/^\d{1,80}$/.test(v.numerator)||!/^\d{1,80}$/.test(v.denominator)||BigInt(v.denominator)===0n))))throw Error('ResultadoL3 não recuperável;originais preservados.'); for(const r of b.rows){if(!r||!['id','period','entityRef','submissionDate','filingRef','interestAmount','interestRef'].every(k=>typeof r[k as keyof ClosureInput]==='string')||!['projected','effective'].includes(r.origin)||!['','yes','no'].includes(r.firstDeclaration)||!['','yes','no'].includes(r.creditsComplete)||!['','timely','late'].includes(r.filing)||!['','none','documented'].includes(r.interest)||!['','report','refund'].includes(r.destination)||r.refundAmount!==undefined&&typeof r.refundAmount!=='string'||!Array.isArray(r.credits)||!Array.isArray(r.payments)||r.credits.some(c=>!c||!['opening','communication','previous'].includes(c.kind)||!['id','period','reference','date','amount','previousId','carryKey'].every(k=>typeof c[k as keyof ClosureCredit]==='string'))||r.payments.some(p=>!p||!['id','reference','date','amount'].every(k=>typeof p[k as keyof ClosurePayment]==='string'))||r.source!==null&&(!r.source||!Number.isInteger(r.source.revision)||!['calculatedAt','fingerprint','opportunity','currency','period','location'].every(k=>typeof r.source![k as keyof ClosureSource]==='string')))throw Error('EntradasL3 não recuperáveis;originais preservados,sem substituir por zero.');} } export function closureBook(d:Record):ClosureBook{const b=maps(d)[scope(d)]??{version:1,rows:[]};validate(b);return b;} export function saveClosureBook(d:Record,b:ClosureBook):string{validate(b);const s=store(d);return JSON.stringify({...s,ivaClosures:{...maps(d),[scope(d)]:b}});} export function closureSources(d:Record):ClosureSource[]{const b=(store(d).books as Record)[scope(d)];return b?[...(b.history??[]),...(b.result?[b.result]:[])].flatMap(calc=>calc.periods.map(p=>({revision:calc.revision,calculatedAt:calc.calculatedAt,fingerprint:closureFingerprint(calc),opportunity:calc.opportunity,currency:calc.currency,period:p.period,location:calc.location,projected:calc.inputs.length===0||calc.inputs.some(r=>r.origin==='projected'),debit:p.exactFields?.['17']??(p.fields['17']===null?null:decimalFraction(p.fields['17'])),credit:p.exactFields?.['18']??(p.fields['18']===null?null:decimalFraction(p.fields['18']))}))).filter((r,i,a)=>a.findIndex(s=>s.fingerprint===r.fingerprint&&s.period===r.period)===i):[];} export function newClosure(id:string,source:ClosureSource|null,entityRef=''):ClosureInput{return{id,period:source?.period??'',entityRef,origin:'projected',source:source?structuredClone(source):null,firstDeclaration:'',filing:'',submissionDate:'',filingRef:'',creditsComplete:'',credits:[],interest:'',interestAmount:'',interestRef:'',destination:'',payments:[]};} export function newClosureCredit(id:string,kind:ClosureCredit['kind']):ClosureCredit{return{id,kind,period:'',reference:'',date:'',amount:'',previousId:'',carryKey:''};} const priorPeriod=(p:string)=>{const d=new Date(p+'-01T00:00:00Z');d.setUTCMonth(d.getUTCMonth()-1);return d.toISOString().slice(0,7);}; const validDate=(d:string)=>/^20\d{2}-(0[1-9]|1[0-2])-\d{2}$/.test(d)&&!Number.isNaN(Date.parse(d))&&new Date(d).toISOString().slice(0,10)===d; const normalize=(s:string)=>s.trim().toLocaleLowerCase(); function creditIdentity(r:ClosureInput,c:ClosureCredit){return closureFingerprint([normalize(r.entityRef),r.origin,c.kind==='previous'?'carry':'document',c.kind==='previous'?c.previousId:normalize(c.reference),c.period]);} function paymentIdentity(r:ClosureInput,p:ClosurePayment){return closureFingerprint([normalize(r.entityRef),r.origin,normalize(p.reference)]);} function inputKey(r:ClosureInput){return closureFingerprint(r);} function financialKey(r:ClosureInput){const{payments:_,...rest}=r;return closureFingerprint(rest);} function otherUses(d:Record,b:ClosureBook):ClosureInput[]{return[...b.rows,...Object.entries(maps(d)).filter(([id])=>id!==scope(d)).flatMap(([,book])=>{validate(book);return book.rows.filter(r=>r.origin==='effective');})];} function duplicateCredits(d:Record,b:ClosureBook):Set{const uses=new Map();for(const r of otherUses(d,b))for(const c of r.credits){const k=creditIdentity(r,c);uses.set(k,(uses.get(k)??0)+1);}return new Set([...uses].filter(([,n])=>n>1).map(([k])=>k));} function substitutionPayments(d:Record):string[]{const m=store(d).ivaSubstitutions as Record;rows:Array<{id:string;status:string;paid:FiscalFraction|null}>};history?:Array<{inputs:Array<{id:string;prior:{entityRef:string;origin:string}|null;payments:ClosurePayment[]}>;rows:Array<{id:string;status:string;paid:FiscalFraction|null}>}>}>|undefined;return Object.entries(m??{}).flatMap(([id,b])=>[...(b.history??[]),...(b.result?[b.result]:[])].flatMap(c=>c.inputs.filter(r=>r.prior&&(id===scope(d)||r.prior.origin==='effective')&&c.rows.some(v=>v.id===r.id&&v.status==='determined'&&v.paid)).flatMap(r=>r.payments.map(p=>closureFingerprint([normalize(r.prior!.entityRef),r.prior!.origin,normalize(p.reference)])))));} function bookKey(d:Record,b:ClosureBook){const sources=closureSources(d);const values:unknown[]=[b.rows,b.rows.map(r=>sources.some(s=>JSON.stringify(s)===JSON.stringify(r.source))),otherUses(d,b).map(r=>[normalize(r.entityRef),r.origin,r.period,r.credits.map(c=>creditIdentity(r,c)),r.payments.map(p=>paymentIdentity(r,p))])];const reserved=substitutionPayments(d),conflicts=b.rows.flatMap(r=>r.payments.map(p=>paymentIdentity(r,p))).filter(k=>reserved.includes(k));if(conflicts.length)values.push(conflicts);return closureFingerprint(values);} export function closureCurrent(d:Record,b:ClosureBook):boolean{return !!b.result&&b.result.key===bookKey(d,b)&&b.result.rows.length===b.rows.length&&b.rows.every(r=>b.result!.rows.filter(x=>x.id===r.id&&x.inputKey===inputKey(r)).length===1);} export function adoptPreviousCredit(c:ClosureCredit,previous:ClosureResult):ClosureCredit{if(previous.status!=='determined'||!previous.fields['26'])throw Error('Fecho anterior indeterminado;sem crédito presumido.');return{...c,kind:'previous',period:previous.period,reference:`Fecho ${previous.id}`,previousId:previous.id,carryKey:previous.carryKey,amount:fractionPresentation(previous.fields['26']).decimal,date:''};} function refundLockConflict(d:Record,row:ClosureInput,fields:Record):boolean{ const books=store(d).ivaRefunds as Record;result?:{inputs:Array<{source:{closureId:string;entityRef:string;origin:string;period:string;credit:FiscalFraction;carry:FiscalFraction;requested:FiscalFraction}|null;stage:string;decision:string;receipts:unknown[]}>};history?:Array<{inputs:Array<{source:{closureId:string;entityRef:string;origin:string;period:string;credit:FiscalFraction;carry:FiscalFraction;requested:FiscalFraction}|null;stage:string;decision:string;receipts:unknown[]}>}>}>|undefined; return Object.entries(books??{}).some(([context,book])=>[...book.rows,...[...(book.history??[]),...(book.result?[book.result]:[])].flatMap(c=>c.inputs)].some(r=>r.source&&(context===scope(d)||r.source.origin==='effective')&&r.source.closureId===row.id&&normalize(r.source.entityRef)===normalize(row.entityRef)&&r.source.origin===row.origin&&r.source.period===row.period&&(r.stage==='submitted'||r.decision!=='pending'||r.receipts.length>0)&&[['25',r.source.credit],['26',r.source.carry],['27',r.source.requested]].some(([f,v])=>JSON.stringify(fields[f as string])!==JSON.stringify(v)))); } function effectiveCarryAlreadyUsed(d:Record,row:ClosureInput,fields:Record):boolean{ if(row.origin!=='effective'||row.destination!=='refund')return false; const books=maps(d),own=books[scope(d)];if(!own)return false; const prior=[...(own.history??[]),...(own.result?[own.result]:[])].flatMap(c=>c.rows).filter(r=>r.id===row.id&&r.origin==='effective'&&r.fields['26']&&fields['26']&&BigInt(subtractFractions(r.fields['26'],fields['26']).numerator)>0n); return prior.some(p=>Object.values(books).some(b=>[...(b.history??[]),...(b.result?[b.result]:[])].some(c=>c.inputs.some(input=>input.origin==='effective'&&normalize(input.entityRef)===normalize(row.entityRef)&&input.period>row.period&&input.credits.some(credit=>credit.kind==='previous'&&credit.previousId===p.id&&credit.carryKey===p.carryKey)&&c.rows.some(r=>r.id===input.id&&r.status==='determined'))))); } export function recordClosure(d:Record,b:ClosureBook,now=new Date().toISOString()):ClosureBook{ validate(b);if(new Set(b.rows.map(r=>r.id)).size!==b.rows.length)throw Error('Identidade de fecho repetida.'); const contexts=b.rows.map(r=>JSON.stringify([normalize(r.entityRef),r.origin,r.period]));if(new Set(contexts).size!==contexts.length)throw Error('Mesmo âmbito/período repetido neste cenário;sem duplicar o fecho.'); const duplicate=duplicateCredits(d,b),puses=new Map();for(const r of otherUses(d,b))for(const p of r.payments){const k=paymentIdentity(r,p);puses.set(k,(puses.get(k)??0)+1);} const results:ClosureResult[]=[]; for(const row of [...b.rows].sort((a,z)=>a.period.localeCompare(z.period))){ const fields:Record=Object.fromEntries(['17','18','19','20','21','22','23','24','25','26','27'].map(f=>[f,null])); const r:ClosureResult={id:row.id,period:row.period,entityRef:row.entityRef,origin:row.origin,status:'indeterminate',reason:'',inputKey:inputKey(row),financialKey:financialKey(row),carryKey:'',fields,paid:null,outstanding:null,overpayment:null,source:closureSource}; const stop=(reason:string)=>{r.reason=reason;results.push(r);}; if(!/^2026-(0[1-9]|1[0-2])$/.test(row.period)||!row.entityRef.trim()||d.currency!=='MZN'){stop('Identifique âmbito fiscal e período2026/MZN;sem consolidar oportunidades automaticamente.');continue;} if(row.origin==='effective'&&otherUses(d,b).some(other=>other!==row&&other.origin==='effective'&&normalize(other.entityRef)===normalize(row.entityRef)&&other.period===row.period)){stop('Mesmo âmbito fiscal/período efectivo reservado noutra oportunidade recuperada;reconcilie a declaração única,sem duplicar ou consolidar dossiers silenciosamente.');continue;} const source=closureSources(d).find(s=>JSON.stringify(s)===JSON.stringify(row.source)); if(!source||source.period!==row.period||source.opportunity!==scope(d)||source.currency!=='MZN'||!source.debit||!source.credit){stop('ResultadoL2 congelado correspondente ausente ou17/18 indeterminados;sem recalcular nem presumir impostozero.');continue;} fields['17']=source.debit;fields['18']=source.credit; if(BigInt(source.debit.numerator)<0n||BigInt(source.credit.numerator)<0n||BigInt(source.debit.numerator)>0n&&BigInt(source.credit.numerator)>0n){stop('Sinais17/18 incompatíveis;resolver origemL2.');continue;} if(row.origin==='effective'&&source.projected){stop('OrigemL2 contém pressupostos projectados;não os apresentar como declaração efectiva.');continue;} if(row.firstDeclaration!=='yes'){stop('Este percursoA/B exige primeira declaração;substituição21 permaneceL3-C.');continue;} if(!validDate(row.submissionDate)||row.submissionDate.slice(0,7)row.period||c.kind!=='communication'&&c.period===row.period||duplicate.has(creditIdentity(row,c))){invalid='Crédito sem origem/período aplicável ou já utilizado/reservado noutra linha/âmbito recuperado;sem utilização duplicada.';break;} if(c.kind==='previous'){ const p=results.find(p=>p.id===c.previousId);if(!p||p.status!=='determined'||p.period!==priorPeriod(row.period)||p.entityRef.trim().toLowerCase()!==row.entityRef.trim().toLowerCase()||p.origin!==row.origin||p.period!==c.period||p.carryKey!==c.carryKey||!p.fields['26']){invalid='Fecho anterior alterado,indeterminado,fora do mês imediatamente anterior ou de outro âmbito;não adoptar novo crédito silenciosamente.';break;}credits19.push(p.fields['26']); }else{ let v:FiscalFraction;try{v=decimalFraction(c.amount);}catch{invalid='Montante de crédito em falta/inválido;não substituir porzero.';break;} if(c.kind==='opening'&&c.period!==priorPeriod(row.period)){invalid='Saldo de abertura exige referência ao saldo disponível do mês imediatamente anterior;origem mais antiga deve ser reconciliada nesse saldo.';break;} if(c.kind==='opening'&&b.rows.some(p=>p.id!==row.id&&p.entityRef.trim().toLowerCase()===row.entityRef.trim().toLowerCase()&&p.origin===row.origin&&p.period===priorPeriod(row.period))){invalid='Há fecho do período anterior neste livro;reutilize26 expressamente em vez de introduzir outra abertura.';break;} if(c.kind==='communication'&&(!validDate(c.date)||c.date>row.submissionDate)){invalid='Crédito20 exige comunicação recebida,referência e data não posterior à entrega declarada;pedido não é comunicação.';break;} (c.kind==='opening'?credits19:credits20).push(v); } } if(invalid){stop(invalid);continue;} fields['19']=addFractions(credits19);fields['20']=addFractions(credits20); const balance=subtractFractions(addFractions([source.credit,fields['19'],fields['20']]),source.debit); fields['22']=positiveFraction(fraction(-BigInt(balance.numerator),BigInt(balance.denominator)));fields['25']=positiveFraction(balance); if(row.interest==='none'&&row.interestRef.trim())fields['23']=zero(); else if(row.interest==='documented'&&row.interestRef.trim()){try{fields['23']=decimalFraction(row.interestAmount);}catch{/* Preserve unknown. */}} fields['24']=fields['23']?addFractions([fields['22'],fields['23']]):null; if(row.destination==='report'){fields['26']=fields['25'];fields['27']=zero();} else if(row.destination==='refund'){ let amount:FiscalFraction;try{amount=decimalFraction(row.refundAmount??'');}catch{stop('Identifique a parcela destinada ao pedido;sem montante presumido.');continue;} if(BigInt(amount.numerator)===0n||BigInt(subtractFractions(fields['25'],amount).numerator)<0n){stop('Parcela do pedido deve ser positiva e não exceder o crédito25;sem dupla utilização.');continue;} fields['27']=amount;fields['26']=subtractFractions(fields['25'],amount);r.source+=' D:afectação expressa26+27=25,sem concessão/recebimento;CIVA21 eModeloA11.'; }else{stop('Escolha reporte integral ou parcela para preparação de reembolsoD;sem destino presumido.');continue;} if(refundLockConflict(d,row,fields)){stop('Crédito ligado a pedido apresentado/decisão/recebimento;conserve a afectação e reconcilie o procedimento,sem o voltar a reportar ou alterar silenciosamente.');continue;} if(effectiveCarryAlreadyUsed(d,row,fields)){stop('Reporte efectivo já utilizado numa declaração posterior recuperada;prepare o pedido sobre o saldo disponível do período reconciliado,sem reafectar retroactivamente crédito consumido.');continue;} if(!fields['24']){stop('Juros23 por identificar:indique montante documentado/pressuposto ou inexistência expressa com motivo;sem taxa presumida.');continue;} r.status='determined';r.carryKey=closureFingerprint([r.id,r.financialKey,fields['26'],scope(d)]); const payments:FiscalFraction[]=[]; for(const p of row.payments){if(!p.reference.trim()||!validDate(p.date)||(puses.get(paymentIdentity(row,p))??0)>1||substitutionPayments(d).includes(paymentIdentity(row,p))){invalid='Pagamento sem referência/data ou repetido noutro período/declaração;conserve o documento e reconcilie a imputação.';break;}try{payments.push(decimalFraction(p.amount));}catch{invalid='Montante de pagamento por identificar;sem baixa presumida.';break;}} if(invalid){stop(invalid);continue;} r.paid=addFractions(payments);r.outstanding=positiveFraction(subtractFractions(fields['24'],r.paid));r.overpayment=positiveFraction(subtractFractions(r.paid,fields['24'])); r.status='determined';r.carryKey=closureFingerprint([r.id,r.financialKey,fields['26'],scope(d)]);r.reason='Saldo calculado, pagamentos registados e crédito transportado distintos. Pagamento não altera22/25/26;excedente pago não cria crédito20 sem comunicação.21 não aplicável à primeira declaração;'+(row.destination==='refund'?'26/27 separados pela afectação;27 é parcela destinada a pedidoD,sem concessão nem recebimento. ':'27zero pela opção expressa de reporte integral. ')+(row.origin==='projected'?'Pressupostos de simulação,sem declaração ou pagamento efectuados pelo portal.':'Dados efectivos declarados;registo não certifica cobrança nem submissãoAT.');results.push(r); } const result={calculatedAt:now,key:bookKey(d,b),inputs:structuredClone(b.rows),rows:results};return{...b,...(b.result?{history:[...(b.history??[]),structuredClone(b.result)]}:{}),result}; } export function closureReportRows(d:Record):Array<[string,string,string,string]>{ try{ const b=closureBook(d);if(!b.rows.length)return[]; const hasRefund=b.result?.rows.some(r=>r.fields['27']&&BigInt(r.fields['27'].numerator)>0n); const description=hasRefund?closureSource.replace('Sem substituição21,reembolso27 ou restituição13(2).','ExtensãoD:26+27=25;27é parcela destinada a pedido,não decisão/recebimento. Substituição21 e restituição13(2) mantêm percursos próprios.'):closureSource; const out:Array<[string,string,string,string]>=[['IVA mensal','L3-A/B — fecho por período',closureCurrent(d,b)?'Resultado correspondente às entradas':'Histórico preservado;rever entradas/origens antes de utilizar',description]]; for(const r of b.result?.rows??[]){ const input=b.result?.inputs.find(x=>x.id===r.id),source=input?.source; const context=`${r.entityRef};${scope(d)};MZN;${r.origin==='projected'?'Pressuposto de simulação':'Dados efectivos declarados'};versão guardada${b.result!.calculatedAt}`; out.push(['IVA mensal',`Fecho A/B — ${r.period}`,Object.entries(r.fields).map(([f,v])=>`${f}:${v?fractionPresentation(v).decimal:f==='21'&&input?.firstDeclaration==='yes'?'Não aplicável (primeira declaração)':'Indeterminado'}`).join('; '),`${context};${r.reason}`]); const credits=(input?.credits??[]).map(c=>`${c.kind==='communication'?'Comunicação20':c.kind==='previous'?'Transporte19':'Abertura19'}: ${c.reference};origem${c.period};montante apresentado${c.amount}MZN${c.date?';recebida'+c.date:''}`).join(' | ')||'Sem créditos registados'; const payments=(input?.payments??[]).map(p=>`${p.reference};${p.date};${p.amount}MZN`).join(' | ')||'Sem pagamentos registados'; out.push(['IVA mensal',`Pagamentos e reporte — ${r.period}`,`Calculado24:${r.fields['24']?fractionLabel(r.fields['24']):'Indeterminado'};pagamentos registados:${r.paid?fractionLabel(r.paid):'Indeterminado'};por liquidar:${r.outstanding?fractionLabel(r.outstanding):'Indeterminado'};excedente pago:${r.overpayment?fractionLabel(r.overpayment):'Indeterminado'};transportado26:${r.fields['26']?fractionLabel(r.fields['26']):'Indeterminado'}`,`${context};OrigemL2:revisão${source?.revision??'por identificar'},${source?.period??''},${source?.location??''},${source?.calculatedAt??''};${credits};${payments};entrega${input?.submissionDate??''},${input?.filingRef??''};juros:${input?.interestRef??''}`]); } return out; }catch{return[['IVA mensal','L3-A/B','Fecho guardado não recuperável','Originais preservados;sem crédito ou pagamento presumidos.']];} }