56 lines
10 KiB
TypeScript
56 lines
10 KiB
TypeScript
import type {Acquisition,AcquisitionResult} from './iva-acquisitions.ts';
|
||
import {acquisitionScope} from './iva-acquisitions.ts';
|
||
import {evaluateAcquisitionBC} from './iva-acquisition-bc.ts';
|
||
import {reducedBaseBook,reducedBaseCurrent,reducedBaseTreatments,evaluateReducedBase} from './iva-reduced-bases.ts';
|
||
import {decimalFraction,fraction,fractionLabel,fractionPresentation,subtractFractions,type FiscalFraction} from './fiscal-rational.ts';
|
||
export const acquisitionAllocationVersion='acquisition-allocation/1';
|
||
export const acquisitionAllocationSource='L2-D: CIVA22(1–8),Lei13/2016 PDF16/p1426(274);Lei10/2025,18(6),PDF13/p2180(13).22(4)numerador anual semIVA com direito18/19;denominador anual semIVA de todas operações,incluindo fora de campo;22(5)exclui vendas de activo usado e operações imobiliárias/financeiras acessórias.22(8)quociente à centésima imediatamente superior.22(2)afectação real com comunicação prévia;22(6–7)provisório,regularização anual emE. Não substitui condições18–20 ou calcula restituiçãoL3.';
|
||
type Choice=''|'yes'|'no';
|
||
export type AcquisitionAllocation={version:1;method:''|'actual'|'pro_rata'|'reduced';numerator:string;denominator:string;unit:string;referenceYear:string;basis:''|'prior_year'|'initial_changed';scopeRef:string;source:string;communicationRef:string;declarationRef:string;actualRequired:Choice;noExcludedPayments:Choice;facts:Record<string,Choice>;reducedId:string;reducedFingerprint:string};
|
||
export function newAcquisitionAllocation(period:string):AcquisitionAllocation{return{version:1,method:'',numerator:'',denominator:'',unit:'',referenceYear:String(Number(period.slice(0,4))-1),basis:'',scopeRef:'',source:'',communicationRef:'',declarationRef:'',actualRequired:'',noExcludedPayments:'',facts:{},reducedId:'',reducedFingerprint:''};}
|
||
export function validAcquisitionAllocation(v:unknown):v is AcquisitionAllocation{if(!v||typeof v!=='object'||Array.isArray(v))return false;const d=v as AcquisitionAllocation;return d.version===1&&['','actual','pro_rata','reduced'].includes(d.method)&&['','prior_year','initial_changed'].includes(d.basis)&&['numerator','denominator','unit','referenceYear','scopeRef','source','communicationRef','declarationRef','reducedId','reducedFingerprint'].every(k=>typeof d[k as keyof AcquisitionAllocation]==='string')&&[d.actualRequired,d.noExcludedPayments].every(v=>['','yes','no'].includes(v))&&!!d.facts&&typeof d.facts==='object'&&!Array.isArray(d.facts)&&Object.values(d.facts).every(v=>['','yes','no'].includes(v));}
|
||
export function allocationReducedChoices(draft:Record<string,unknown>){return reducedBaseBook(draft).rows.map(row=>({id:row.id,label:`${row.description||row.id} — ${row.period} — ${reducedBaseTreatments.find(t=>t.id===row.treatment)?.label??row.treatment}`,row}));}
|
||
export function adoptAllocationReduced(draft:Record<string,unknown>,d:AcquisitionAllocation,id:string):AcquisitionAllocation{const source=allocationReducedChoices(draft).find(r=>r.id===id);if(!source)throw Error('Base reduzida de origem indisponível;originais preservados.');return {...d,reducedId:id,reducedFingerprint:JSON.stringify(source.row),facts:{...d.facts,scopeConfirmed:''}};}
|
||
export function allocationSourceKey(draft:Record<string,unknown>,rows:Acquisition[]):string{return rows.some(r=>r.d)?JSON.stringify({version:acquisitionAllocationVersion,reducedOperations:reducedBaseBook(draft).rows,sources:rows.filter(r=>r.d?.method==='reduced').map(r=>({row:allocationReducedChoices(draft).find(s=>s.id===r.d?.reducedId)?.row??null,current:reducedBaseCurrent(draft,reducedBaseBook(draft))}))}):'';}
|
||
export function allocationHasUnconsolidatedOutputs(draft:Record<string,unknown>,period:string):boolean{return reducedBaseBook(draft).rows.some(r=>r.period===period);}
|
||
type Decision={ratio:FiscalFraction|null;rawRatio?:FiscalFraction;reason:string;source:string};
|
||
export function acquisitionAllocationDecision(draft:Record<string,unknown>|undefined,row:Acquisition):Decision{
|
||
const d=row.d;const stop=(reason:string):Decision=>({ratio:null,reason,source:acquisitionAllocationSource});
|
||
if(!d||!validAcquisitionAllocation(d))return stop('Método/parâmetrosD não recuperáveis;sem percentagem por defeito.');
|
||
if(!draft||draft.currency!=='MZN'||row.sourceCurrency!=='MZN'||!/^2026-(0[1-9]|1[0-2])$/.test(row.period))return stop('ÂmbitoD2026/MZN ou contexto em falta.');
|
||
if(!d.scopeRef.trim()||!d.source.trim()||d.facts.scopeConfirmed!=='yes')return stop('Âmbito da aquisição/actividade,fonte e correspondência por confirmar;sem inferir da oportunidade.');
|
||
if(d.method==='reduced'){
|
||
if(row.allocation!=='taxable_only')return stop('18(6) neste percurso exige afectação exclusiva à operação de base reduzida;combinação com afectação mista ainda não demonstrada.');
|
||
const b=reducedBaseBook(draft),source=b.rows.find(r=>r.id===d.reducedId);if(!source||!reducedBaseCurrent(draft,b)||JSON.stringify(source)!==d.reducedFingerprint)return stop('Origem de base reduzida alterada/incompleta:rever e adoptar expressamente;histórico conservado.');
|
||
const result=evaluateReducedBase(source,draft.currency),rule=reducedBaseTreatments.find(r=>r.id===source.treatment);if(!rule||result.status!=='determined')return stop('Condições da base reduzida de origem ainda indeterminadas;não basta escolher percentagem.');
|
||
return{ratio:fraction(BigInt(rule.percent),100n),reason:`18(6):proporção específica${rule.percent}% reutilizada de${rule.branch}/${source.id};aplicada uma vez ao IVA elegível,sem converter percentagem de base em taxaIVA.`,source:acquisitionAllocationSource+';'+b.result!.source};
|
||
}
|
||
if(row.allocation!=='mixed')return stop('Método22 exige afectação mista expressamente identificada;conserve a afectação exclusiva quando aplicável.');
|
||
if(d.facts.rightBasis!=='yes')return stop('Identifique a parte/operações com direito18/19;isenção ou sector não estabelecem o direito.');
|
||
if(!/^\d{1,15}(\.\d{1,6})?$/.test(d.numerator)||!/^\d{1,15}(\.\d{1,6})?$/.test(d.denominator))return stop('Numerador/denominador em falta ou inválidos;sem percentagem presumida.');
|
||
const n=decimalFraction(d.numerator),den=decimalFraction(d.denominator);if(BigInt(den.numerator)===0n)return stop('Denominador zero:não existe quociente aplicável.');
|
||
const rawRatio=fraction(BigInt(n.numerator)*BigInt(den.denominator),BigInt(n.denominator)*BigInt(den.numerator));if(BigInt(rawRatio.numerator)>BigInt(rawRatio.denominator))return stop('Numerador excede denominador;resolver bases sem limitar silenciosamente a100%.');
|
||
if(d.method==='actual'){
|
||
if(!d.unit.trim()||d.facts.actualScope!=='yes'||d.facts.communication!=='yes'||!d.communicationRef.trim()||d.facts.conditionsMet!=='yes')return stop('Afectação real22(2):critério/unidade,repartição efectiva,comunicação prévia e condições concretamente aplicáveis por completar;sem criar aprovação adicional.');
|
||
return{ratio:rawRatio,rawRatio,reason:`Afectação real22(2):${d.numerator}/${d.denominator} ${d.unit};critério e comunicação${d.communicationRef}. Fracção real exacta,sem arredondamento de pro rata.`,source:acquisitionAllocationSource};
|
||
}
|
||
if(d.method!=='pro_rata')return stop('Escolha afectação real,pro rata provisório ou proporção específica18(6).');
|
||
if(d.actualRequired==='yes')return stop('Está identificada imposição de afectação real22(3);não aplicar pro rata em substituição.');
|
||
if(d.facts.annualScope!=='yes'||d.facts.netExclusions!=='yes')return stop('Confirmar totais anuais semIVA e âmbito22(4–5):inclui fora de campo;exclui vendas de activo usado e imobiliárias/financeiras acessórias. Não usar só vendas de um mês como total anual.');
|
||
const year=Number(row.period.slice(0,4));
|
||
if(d.basis==='prior_year'){if(d.referenceYear!==String(year-1))return stop('Pro rata provisório22(6) usa operações do ano anterior ao exercício;sem substituir pelo mês corrente.');}
|
||
else if(d.basis==='initial_changed'){if(d.referenceYear!==String(year)||d.facts.initialChanged!=='yes'||!d.declarationRef.trim())return stop('Estimativa22(7):início/alteração substancial,ano e referência da declaração de início/alterações por identificar.');}
|
||
else return stop('Indique base provisória22(6) ou estimativa expressa22(7);regularização final22(6) segueE.');
|
||
const numerator=BigInt(rawRatio.numerator),denominator=BigInt(rawRatio.denominator);const hundredths=(numerator*100n+denominator-1n)/denominator;
|
||
return{ratio:fraction(hundredths,100n),rawRatio,reason:`Pro rata provisório22(4/6/7/8):quociente${fractionLabel(rawRatio)}→${hundredths}% (quociente à centésima superior). Base anual${d.referenceYear},${d.basis==='prior_year'?'ano anterior':'estimativa declarada'}. Acerto anual emE,sem recalcular históricos.`,source:acquisitionAllocationSource};
|
||
}
|
||
export function evaluateAcquisitionAllocation(row:Acquisition,currency:string,registered:Choice,draft:Record<string,unknown>|undefined,raw:AcquisitionResult):AcquisitionResult{
|
||
const decision=acquisitionAllocationDecision(draft,row),d=row.d!;
|
||
const unknown=(reason:string):AcquisitionResult=>({...raw,deductible:null,reason,eligibility:{status:'indeterminate',reason,source:decision.source}});
|
||
if(!decision.ratio)return unknown(decision.reason);if(!row.bc)return unknown('Complete a natureza/condiçõesB/C da aquisição antes da proporção;sem isenção de condições documentais.');
|
||
const base=evaluateAcquisitionBC(row,currency,registered,draft,raw,{right:{status:'eligible',reason:decision.reason,source:decision.source,scopeId:draft?acquisitionScope(draft):undefined},noExcludedPayments:d.noExcludedPayments});
|
||
if(base.deductible===null||base.eligibility?.status!=='eligible')return base;
|
||
const before=base.deductibleExact??decimalFraction(base.deductible);const exact=fraction(BigInt(before.numerator)*BigInt(decision.ratio.numerator),BigInt(before.denominator)*BigInt(decision.ratio.denominator));const reason=base.reason+' '+decision.reason;
|
||
return {...base,deductible:fractionPresentation(exact).decimal,deductibleExact:exact,nonDeductibleExact:subtractFractions(decimalFraction(base.vat!),exact),reason,eligibility:{...base.eligibility,status:'eligible',reason},allocationCalculation:{version:acquisitionAllocationVersion,ratio:decision.ratio,rawRatio:decision.rawRatio??decision.ratio,before,source:decision.source}};
|
||
}
|