import type {AcquisitionCalculation,Acquisition,AcquisitionResult} from './iva-acquisitions.ts'; import {addFractions,decimalFraction,fraction,fractionLabel,fractionPresentation,subtractFractions,positiveFraction,type FiscalFraction} from './fiscal-rational.ts'; export const regularizationSource='L2-E: CIVA22(4–8),Lei13/2016 PDF16/p1426(274),acerto anual no último período do ano;CIVA51(1/4),PDF21/p1426(279),rectificação da dedução pelo adquirente até ao fim do período seguinte à recepção. Decreto7/2008,RCIVA27,BR16 PDF5/p103,registo individual de activos: data,IVA suportado e deduzido. Caixas ModeloA12/13;divergência de remissões textuais11/12 conservada,sem alterar P001/05–06.'; type Choice=''|'yes'|'no'; export type RegularizationEvent={id:string;kind:'annual'|'purchase_correction';period:string;source:AcquisitionCalculation|null;rowIds:string[];numerator:string;denominator:string;reference:string;receivedDate:string;correctedDeductible:string;reason:string;facts:Record}; export type RegularizationResult={id:string;period:string;status:'determined'|'indeterminate';reason:string;delta:FiscalFraction|null;rawRatio?:FiscalFraction;ratio?:FiscalFraction;details:string;source:string}; type Calculation={version:1;calculatedAt:string;key:string;events:RegularizationResult[]}; export type RegularizationBook={version:1;events:RegularizationEvent[];completePeriods:string[];result?:Calculation;history?:Calculation[]}; type FrozenLine={input:Acquisition;result:AcquisitionResult}; const scope=(draft:Record)=>String(draft.context_opportunity||'general'); function store(draft:Record):Record{const s=JSON.parse(String(draft.modelo_a_snapshot||'{"version":1,"books":{}}'));if(s.version!==1||!s.books)throw Error('Origem fiscal não recuperável;conserve os originais.');return s;} export function validateRegularizationStore(s:Record):void{ if(s.regularizations===undefined)return; if(!s.regularizations||typeof s.regularizations!=='object'||Array.isArray(s.regularizations))throw Error('Mapa de regularizações não recuperável;originais preservados.'); for(const value of Object.values(s.regularizations))assertRegularizationBook(value as RegularizationBook); } function assertRegularizationBook(value:RegularizationBook):void{ const badSource=(s:AcquisitionCalculation|null)=>s!==null&&(!s||typeof s!=='object'||!Array.isArray(s.inputs)||!Array.isArray(s.rows)||!Array.isArray(s.periods)||!Number.isInteger(s.revision)||!['currency','opportunity','calculatedAt','sourceKey'].every(k=>typeof s[k as keyof AcquisitionCalculation]==='string')); if(!value||value.version!==1||!Array.isArray(value.events)||!Array.isArray(value.completePeriods)||value.completePeriods.some(p=>typeof p!=='string')||value.history!==undefined&&!Array.isArray(value.history)||value.events.some(e=>!e||!['annual','purchase_correction'].includes(e.kind)||!['id','period','numerator','denominator','reference','receivedDate','correctedDeductible','reason'].every(k=>typeof e[k as keyof RegularizationEvent]==='string')||!Array.isArray(e.rowIds)||e.rowIds.some(id=>typeof id!=='string')||!e.facts||typeof e.facts!=='object'||Array.isArray(e.facts)||Object.values(e.facts).some(v=>!['','yes','no'].includes(v))||badSource(e.source)))throw Error('Regularizações não recuperáveis;originais preservados,sem substituir por zero.'); if(value.result&&(!Array.isArray(value.result.events)||value.result.version!==1||typeof value.result.key!=='string'||typeof value.result.calculatedAt!=='string'||value.result.events.some(e=>!e||typeof e.id!=='string'||typeof e.period!=='string'||!['determined','indeterminate'].includes(e.status)||e.delta!==null&&(!e.delta||! /^-?\d{1,80}$/.test(e.delta.numerator)||!/^\d{1,80}$/.test(e.delta.denominator)||BigInt(e.delta.denominator)===0n))))throw Error('Resultado de regularizações não recuperável;originais preservados.'); } export function regularizationBook(draft:Record):RegularizationBook{ const value=(store(draft).regularizations as Record|undefined)?.[scope(draft)]; if(!value)return{version:1,events:[],completePeriods:[]}; assertRegularizationBook(value); return value; } export function saveRegularizationBook(draft:Record,book:RegularizationBook):string{const s=store(draft);return JSON.stringify({...s,regularizations:{...(s.regularizations as object??{}),[scope(draft)]:book}});} export function regularizationSources(draft:Record):AcquisitionCalculation[]{const b=(store(draft).books as Record)[scope(draft)];return b?[...(b.history??[]),...(b.result?[b.result]:[])].filter((v,i,a)=>a.findIndex(x=>JSON.stringify(x)===JSON.stringify(v))===i):[];} export function newRegularization(id:string,kind:RegularizationEvent['kind'],source:AcquisitionCalculation|null):RegularizationEvent{return{id,kind,period:source?source.inputs[0]?.period.slice(0,4)+'-12':'',source:source?structuredClone(source):null,rowIds:source?.inputs.filter(r=>kind==='annual'?r.d?.method==='pro_rata':true).map(r=>r.id)??[],numerator:'',denominator:'',reference:'',receivedDate:'',correctedDeductible:'',reason:'',facts:{}};} function frozenLines(e:RegularizationEvent):FrozenLine[]{return e.rowIds.map(id=>({input:e.source!.inputs.find(r=>r.id===id)!,result:e.source!.rows.find(r=>r.id===id)!}));} function sourceAvailable(draft:Record,e:RegularizationEvent):boolean{return !!e.source&®ularizationSources(draft).some(s=>JSON.stringify(s)===JSON.stringify(e.source));} function multiply(a:FiscalFraction,b:FiscalFraction){return fraction(BigInt(a.numerator)*BigInt(b.numerator),BigInt(a.denominator)*BigInt(b.denominator));} const validDate=(s:string)=>/^2026-\d{2}-\d{2}$/.test(s)&&!Number.isNaN(Date.parse(s))&&new Date(s).toISOString().slice(0,10)===s; function identity(e:RegularizationEvent,id:string):string{const r=e.source?.inputs.find(r=>r.id===id);return JSON.stringify([e.kind,r?.supplier.trim().toLowerCase(),r?.documentType,r?.documentRef.trim().toLowerCase(),r?.documentDate,r?.documentLine.trim(),e.kind==='annual'?e.period.slice(0,4):'']);} export function evaluateRegularization(draft:Record,e:RegularizationEvent):RegularizationResult{ const stop=(reason:string):RegularizationResult=>({id:e.id,period:e.period,status:'indeterminate',reason,delta:null,details:'Histórico preservado;sem lançamento automático.',source:regularizationSource}); if(!/^2026-(0[1-9]|1[0-2])$/.test(e.period)||draft.currency!=='MZN'||e.source?.currency!=='MZN'||e.source.opportunity!==scope(draft)||!sourceAvailable(draft,e))return stop('Âmbito2026/MZN e resultado original congelado na mesma oportunidade por identificar;uma edição actual não substitui o resultado original.'); if(!e.rowIds.length||new Set(e.rowIds).size!==e.rowIds.length)return stop('Seleccione parcelas originais distintas.'); const lines=frozenLines(e);if(lines.some(l=>!l.input||!l.result||l.result.deductibleExact===undefined||l.result.eligibility?.status!=='eligible'||!l.input.documentRef.trim()))return stop('Parcela original sem dedução exacta/elegível/documento;não presumir dedução inicial.'); if(!e.reference.trim()||!e.reason.trim()||e.facts.scope!=='yes'||e.facts.recorded!=='yes')return stop('Identifique documento/fonte,motivo,âmbito e montante originalmente deduzido (efectivo ou pressuposto da projecção).'); let delta:FiscalFraction,details:string,rawRatio:FiscalFraction|undefined,ratio:FiscalFraction|undefined; if(e.kind==='annual'){ if(!e.period.endsWith('-12')||lines.some(l=>l.input.period.slice(0,4)!==e.period.slice(0,4)||l.input.d?.method!=='pro_rata'||!l.result.allocationCalculation))return stop('Acerto22(6):apenas pro rata provisório do mesmo ano,no último período;afectação real e18(6) têm regras próprias.'); if(e.facts.annual!=='yes'||e.facts.exclusions!=='yes')return stop('Confirme bases efectivas anuais22(4),semIVA,incluindo fora de campo,e exclusões22(5);não usar apenas vendas do mês.'); if(!/^\d{1,15}(\.\d{1,6})?$/.test(e.numerator)||!/^\d{1,15}(\.\d{1,6})?$/.test(e.denominator))return stop('Bases anuais finais em falta ou inválidas.'); const n=decimalFraction(e.numerator),d=decimalFraction(e.denominator);if(BigInt(d.numerator)===0n)return stop('Denominador anual zero;sem percentagem por defeito.'); rawRatio=fraction(BigInt(n.numerator)*BigInt(d.denominator),BigInt(n.denominator)*BigInt(d.numerator));if(BigInt(rawRatio.numerator)>BigInt(rawRatio.denominator))return stop('Numerador anual excede denominador.'); ratio=fraction((BigInt(rawRatio.numerator)*100n+BigInt(rawRatio.denominator)-1n)/BigInt(rawRatio.denominator),100n); delta=addFractions(lines.map(l=>subtractFractions(multiply(l.result.allocationCalculation!.before,ratio!),l.result.deductibleExact!))); details=lines.map(l=>`${l.input.documentRef}/linha${l.input.documentLine||'única'}:IVA elegível antesD${fractionLabel(l.result.allocationCalculation!.before)},deduzido${fractionLabel(l.result.deductibleExact!)},final${fractionLabel(multiply(l.result.allocationCalculation!.before,ratio!))}`).join('; ')+`;quociente final${fractionLabel(rawRatio)};aplicado${fractionLabel(ratio)}`; }else{ if(lines.length!==1||!validDate(e.receivedDate))return stop('Rectificação51(4):seleccione uma linha e a data de recepção do documento rectificativo.'); const received=e.receivedDate.slice(0,7),next=new Date(e.receivedDate+'T00:00:00Z');next.setUTCMonth(next.getUTCMonth()+1,1);const last=next.toISOString().slice(0,7); if(e.receivedDatelast||e.period0n)return stop('Este percurso51(4) trata anulação/redução da dedução pelo adquirente;acréscimos ou erros têm fundamento/prazo próprios.'); delta=subtractFractions(corrected,lines[0].result.deductibleExact!);details=`${lines[0].input.documentRef};deduzido${fractionLabel(lines[0].result.deductibleExact!)};corrigido${fractionLabel(corrected)};recebido${e.receivedDate};limite${last}`; } const sign=BigInt(delta.numerator);return{id:e.id,period:e.period,status:'determined',delta,...(rawRatio&&ratio?{rawRatio,ratio}:{}),reason:sign>0n?'A favor do sujeito passivo — campo12':sign<0n?'A favor do Estado — campo13':'Sem diferença;evento conservado,sem nova dedução',details:`${details};revisão original${e.source.revision}/${e.source.calculatedAt};${lines.some(l=>l.input.origin==='projected')?'Pressuposto de simulação;sem declaração fiscal submetida':'Dados efectivos declarados;submissão fora do portal'};${e.reference};${e.reason}`,source:regularizationSource}; } // Change detection only. Keep frozen source values, without embedding them again in every calculation key. function fingerprint(value:unknown):string{const raw=JSON.stringify(value);let hash=14695981039346656037n;for(let i=0;i,book:RegularizationBook,now=new Date().toISOString()):RegularizationBook{ if(new Set(book.events.map(e=>e.id)).size!==book.events.length)throw Error('Evento repetido;sem dupla regularização.'); const identities=book.events.flatMap(e=>e.rowIds.map(id=>identity(e,id)));if(new Set(identities).size!==identities.length)throw Error('Mesma aquisição/ano ou documento já incluído noutra regularização deste motivo;consolide o evento original.'); // Mixing an annual correction and a credit note on the same deduction needs sequencing, not two independent deltas. const seen=new Map();for(const e of book.events)for(const id of e.rowIds){const k=identity({...e,kind:'purchase_correction'},id),prior=seen.get(k);if(prior&&prior!==e.kind)throw Error('Acerto anual e documento rectificativo sobre a mesma parcela exigem reconciliação sequencial;sem dupla correcção.');seen.set(k,e.kind);} const result:Calculation={version:1,calculatedAt:now,key:key(book),events:book.events.map(e=>evaluateRegularization(draft,e))};return{...book,...(book.result?{history:[...(book.history??[]),structuredClone(book.result)]}:{}),result}; } export function regularizationsCurrent(draft:Record,b:RegularizationBook):boolean{return !!b.result&&b.result.version===1&&b.result.key===key(b)&&b.result.events.length===b.events.length&&b.events.every(e=>sourceAvailable(draft,e)&&b.result!.events.filter(r=>r.id===e.id&&r.period===e.period).length===1);} export function regularizationFields(draft:Record,period:string,currentRows?:Acquisition[]):{fields:Record;reasons:string[]}|null{ const b=regularizationBook(draft);if(!b.events.some(e=>e.period===period)&&!b.completePeriods.includes(period))return null; const events=b.result?.events.filter(e=>e.period===period)??[]; if(currentRows&&b.events.some(e=>e.period===period&&e.source&&frozenLines(e).some(l=>l.input&¤tRows.some(r=>r.id===l.input.id&&JSON.stringify(r)!==JSON.stringify(l.input)))))return{fields:{'12':null,'13':null},reasons:['A aquisição actual difere da parcela congelada usada emE;reconcilie a diferença antes de somar a regularização,sem recalcular o histórico.']}; if(!b.completePeriods.includes(period)||!regularizationsCurrent(draft,b)||events.some(e=>!e.delta))return{fields:{'12':null,'13':null},reasons:['RegularizaçõesE:complete/reconcilie o período e guarde o cálculo correspondente;sem zero presumido nem uso de resultado histórico.']}; return{fields:{'12':addFractions(events.map(e=>positiveFraction(e.delta!))),'13':addFractions(events.map(e=>positiveFraction(fraction(-BigInt(e.delta!.numerator),BigInt(e.delta!.denominator)))) )},reasons:['E:apenas eventos documentados declarados completos;12/13 separados,sem recalcular aquisições históricas.']}; } export function regularizationSourceKey(draft:Record):string{const b=regularizationBook(draft);const reduced=(store(draft).regulatedReducedBases as Record|undefined)?.[scope(draft)]?.rows??[];return b.events.length||b.completePeriods.length?fingerprint([b,regularizationsCurrent(draft,b),reduced]):'';} export function regularizationReportRows(draft:Record):Array<[string,string,string,string]>{const b=regularizationBook(draft);if(!b.events.length)return[];return[['IVA mensal','RegularizaçõesE',regularizationsCurrent(draft,b)?'Resultado correspondente às entradas':'Histórico preservado;rever entradas/origem',regularizationSource],...(b.result?.events??[]).map(e=>['IVA mensal',`Regularização ${e.id} — ${e.period}`,`${e.status==='determined'?e.reason:'Indeterminado: '+e.reason};delta${e.delta?fractionLabel(e.delta):'indeterminado'}`,e.details+';'+e.source] as [string,string,string,string])];} export function frozenAssetRows(draft:Record):Array<[string,string,string,string]>{const sources=regularizationSources(draft),seen=new Set(),out:Array<[string,string,string,string]>=[];for(const s of [...sources].reverse())for(const r of s.inputs.filter(r=>r.field==='08'||r.investmentAsset===true)){const k=JSON.stringify([r.supplier,r.documentRef,r.documentDate,r.documentLine]);if(seen.has(k))continue;seen.add(k);const v=s.rows.find(v=>v.id===r.id);out.push(['IVA mensal',`Activo — ${r.description||r.id}`,`${r.documentRef};linha${r.documentLine||'única'};aquisição${r.documentDate};IVA suportado${v?.vat??'indeterminado'};IVA deduzido${v?.deductibleExact?fractionLabel(v.deductibleExact):v?.deductible??'indeterminado'}`,`RCIVA27,Decreto7/2008 PDF5/p103;${s.currency};revisão${s.revision}/${s.calculatedAt};${r.origin==='projected'?'Pressuposto de simulação':'Dados efectivos declarados'};sem amortização fiscal ou regularização plurianual presumidas.`]);}return out;}