43 lines
5.2 KiB
TypeScript
43 lines
5.2 KiB
TypeScript
import type {AcquisitionBook} from './iva-acquisitions.ts';
|
|
import {beverageParcel} from './iva-beverage-parcels.ts';
|
|
import {addFractions,decimalFraction,fraction,fractionLabel,subtractFractions,type FiscalFraction} from './fiscal-rational.ts';
|
|
|
|
/** Technical representation of disjoint VAT portions, not an additional fiscal condition. */
|
|
export type BeverageAllocation={from:string;to:string;reference:string;deductionFrom:string;deductionTo:string;deductionReference:string};
|
|
type Source={parcel:string;origin:string;vat:FiscalFraction};
|
|
export type BeverageRange={from:FiscalFraction;to:FiscalFraction;amount:FiscalFraction};
|
|
function boundary(raw:string):FiscalFraction{const parts=/^(\d{1,40})\/(\d{1,40})$/.exec(raw);if(!parts)return decimalFraction(raw);if(BigInt(parts[2])===0n)throw Error('Denominador nulo nos limites da parcela.');return fraction(BigInt(parts[1]),BigInt(parts[2]));}
|
|
export function beverageRange(source:Source,allocation?:BeverageAllocation):BeverageRange {
|
|
const from=allocation?boundary(allocation.from):fraction(0n),to=allocation?boundary(allocation.to):source.vat,amount=subtractFractions(to,from);
|
|
if(BigInt(amount.numerator)<=0n||BigInt(subtractFractions(source.vat,to).numerator)<0n)throw Error('Limites da parcela fora do IVA documental; indique início inferior ao fim e fim até ao total da linha.');
|
|
if(allocation&&!allocation.reference.trim())throw Error('Identifique o suporte da repartição da linha.');
|
|
return {from,to,amount};
|
|
}
|
|
export function beverageRangesOverlap(a:BeverageRange,b:BeverageRange):boolean{return BigInt(subtractFractions(a.to,b.from).numerator)>0n&&BigInt(subtractFractions(b.to,a.from).numerator)>0n;}
|
|
export function beverageDeductionEvidence(d:Record<string,unknown>,source:Source):{amount:FiscalFraction|null;reason:string;records:Array<{context:string;revision:number;calculatedAt:string;amount:FiscalFraction;documents:string[]}>} {
|
|
const store=JSON.parse(String(d.modelo_a_snapshot||'{}')),books=(store.books??{}) as Record<string,AcquisitionBook>,records:Array<{context:string;revision:number;calculatedAt:string;amount:FiscalFraction;documents:string[]}>=[];
|
|
for(const[context,book]of Object.entries(books))for(const calc of [...(book.history??[]),...(book.result?[book.result]:[])]){
|
|
const inputs=calc.inputs.filter(row=>row.origin==='effective'&&source.origin==='effective'&&beverageParcel(row)===source.parcel);
|
|
const amounts:FiscalFraction[]=[];
|
|
for(const input of inputs){const result=calc.rows.find(r=>r.id===input.id);if(!result||result.deductible===null){if(input.bc?.beveragePath==='deduction')return{amount:null,reason:'Dedução efectiva anterior indeterminada; identificar montante exacto e documento antes da repartição.',records};continue;}
|
|
const amount=result.deductibleExact??decimalFraction(result.deductible);if(BigInt(amount.numerator)>0n)amounts.push(amount);
|
|
}
|
|
if(amounts.length)records.push({context,revision:calc.revision,calculatedAt:calc.calculatedAt,amount:addFractions(amounts),documents:inputs.map(row=>`${row.documentRef};linha${row.documentLine};${row.period}`)});
|
|
}
|
|
const unique=[...new Map(records.map(r=>[fractionLabel(r.amount),r.amount])).values()];
|
|
if(unique.length>1)return{amount:null,reason:'Montantes de dedução efectiva divergentes nas versões recuperadas; reconciliar a sequência e o fundamento das regularizações. Uma correcção preparada não liberta IVA para E.',records};
|
|
return{amount:unique[0]??fraction(0n),reason:records.length?'Dedução efectiva declarada preservada; a sua afectação à parcela exige suporte expresso.':'Sem dedução efectiva positiva identificada nos livros recuperados; não é consulta à conta corrente AT.',records};
|
|
}
|
|
export function reconcileBeverageAllocation(d:Record<string,unknown>,source:Source,allocation?:BeverageAllocation):{range:BeverageRange|null;prior:FiscalFraction|null;reason:string} {
|
|
const evidence=beverageDeductionEvidence(d,source);if(!evidence.amount)return{range:null,prior:null,reason:evidence.reason};
|
|
try{
|
|
const range=beverageRange(source,allocation);
|
|
if(BigInt(evidence.amount.numerator)>0n){
|
|
if(!allocation||!allocation.deductionReference.trim())return{range:null,prior:evidence.amount,reason:'Dedução efectiva anterior identificada; indique limites e suporte da parcela já deduzida, sem a apagar ou repetir em restituição.'};
|
|
const deduction=beverageRange(source,{...allocation,from:allocation.deductionFrom,to:allocation.deductionTo});
|
|
if(BigInt(subtractFractions(deduction.amount,evidence.amount).numerator)!==0n||beverageRangesOverlap(range,deduction))return{range:null,prior:evidence.amount,reason:'Parcela da dedução não coincide com o montante efectivo anterior ou sobrepõe-se à restituição; benefício duplicado impedido. Se toda a linha foi deduzida, é necessária regularização com fundamento próprio, ainda não automatizada nesta via.'};
|
|
}else if(allocation&&(allocation.deductionFrom||allocation.deductionTo||allocation.deductionReference))return{range:null,prior:evidence.amount,reason:'Foi indicada dedução anterior sem correspondência nos livros efectivos recuperados; resolver a divergência antes de calcular.'};
|
|
return{range,prior:evidence.amount,reason:evidence.reason};
|
|
}catch(error){return{range:null,prior:evidence.amount,reason:String(error instanceof Error?error.message:error)};}
|
|
}
|