85 lines
16 KiB
TypeScript
85 lines
16 KiB
TypeScript
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<string,Choice>};
|
||
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,unknown>)=>String(draft.context_opportunity||'general');
|
||
function store(draft:Record<string,unknown>):Record<string,unknown>{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<string,unknown>):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<string,unknown>):RegularizationBook{
|
||
const value=(store(draft).regularizations as Record<string,RegularizationBook>|undefined)?.[scope(draft)];
|
||
if(!value)return{version:1,events:[],completePeriods:[]};
|
||
assertRegularizationBook(value);
|
||
return value;
|
||
}
|
||
export function saveRegularizationBook(draft:Record<string,unknown>,book:RegularizationBook):string{const s=store(draft);return JSON.stringify({...s,regularizations:{...(s.regularizations as object??{}),[scope(draft)]:book}});}
|
||
export function regularizationSources(draft:Record<string,unknown>):AcquisitionCalculation[]{const b=(store(draft).books as Record<string,{result?:AcquisitionCalculation;history?:AcquisitionCalculation[]}>)[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<string,unknown>,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<string,unknown>,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.receivedDate<lines[0].input.documentDate||e.period<received||e.period>last||e.period<lines[0].input.period)return stop('Período fora da recepção/seguinte51(4),ou anterior à dedução original;sem alteração retroactiva.');
|
||
if(e.facts.supplierCorrection!=='yes'||e.facts.rightUnchanged!=='yes')return stop('Confirme rectificação pelo fornecedor e manutenção do âmbito/condições da dedução. Mudança de direito exige enquadramento próprio.');
|
||
let corrected:FiscalFraction;try{corrected=decimalFraction(e.correctedDeductible);}catch{return stop('Indique a dedução corrigida documentada,sem taxa ou dedução presumida.');}
|
||
if(BigInt(subtractFractions(corrected,lines[0].result.deductibleExact!).numerator)>0n)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<raw.length;i++)hash=BigInt.asUintN(64,(hash^BigInt(raw.charCodeAt(i)))*1099511628211n);return`${raw.length}:${hash.toString(16)}`;}
|
||
function key(book:RegularizationBook){return fingerprint([book.version,book.events,book.completePeriods]);}
|
||
export function recordRegularizations(draft:Record<string,unknown>,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<string,string>();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<string,unknown>,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<string,unknown>,period:string,currentRows?:Acquisition[]):{fields:Record<string,FiscalFraction|null>;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,unknown>):string{const b=regularizationBook(draft);const reduced=(store(draft).regulatedReducedBases as Record<string,{rows:unknown[]}>|undefined)?.[scope(draft)]?.rows??[];return b.events.length||b.completePeriods.length?fingerprint([b,regularizationsCurrent(draft,b),reduced]):'';}
|
||
export function regularizationReportRows(draft:Record<string,unknown>):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<string,unknown>):Array<[string,string,string,string]>{const sources=regularizationSources(draft),seen=new Set<string>(),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;}
|