64 lines
13 KiB
TypeScript
64 lines
13 KiB
TypeScript
import {acquisitionScope,readAcquisitionStore} from './iva-acquisitions.ts';
|
||
|
||
export const reducedBaseVersion='proportional-bases/1';
|
||
export const reducedBaseSource='CIVA15(2)j/k/l/m,15(1)/(5)/(6)/(9),Lei13/2016PDF13–14/p1426(271–272);Lei22/2022PDF3mantém15(2)j–m;Lei10/2025não altera15 eadita18(6),PDF13:dedução do imposto suportado relativo a estas operações na mesma proporção da base reduzida. Recorte2026/MZN;base apenas,sem taxaIVA/IVA/dedução/campo11 ou totaisModeloA. Energia62%,taxas aeronáuticas85%,obras redução60%(base40%),água75%. Percentagem de base não é taxaIVA.';
|
||
export const reducedBaseTreatments=[{id:'regulated_energy',branch:'P096',clause:'j',label:'P096 — Energia com preço fixado por Autoridade Pública',percent:62},{id:'aeronautical_fees',branch:'P097',clause:'k',label:'P097 — Serviços com preço fixado por taxas aeronáuticas',percent:85},{id:'public_works',branch:'P098',clause:'l',label:'P098 — Obras públicas abrangidas',percent:40},{id:'public_water',branch:'P099',clause:'m',label:'P099 — Água potável por rede pública e preço fixado',percent:75}] as const;
|
||
export const reducedBaseConditions=[['taxableOperation','Está identificada a operação sujeita a IVA e a correspondência com este tratamento'],['fullNetInvoice','O montante é a contraprestação total da operação/factura antes desta redução,sem o próprioIVA ecom composição15(1)/(5)/(6)conferida'],['scopeMatches','Documentos/referências ou pressupostos expressos correspondem ao objecto,local,operação eperíodo']] as const;
|
||
export const reducedOwnConditions={
|
||
regulated_energy:[['energySupply','Transmissão de energia identificada'],['publicFixedPrice','Preço fixado pela Autoridade Pública identificada na referência aplicável']],
|
||
aeronautical_fees:[['aeronauticalPrice','Preço do serviço fixado através de taxas aeronáuticas;uma descrição de comunicações,saneamento ou telecomunicações não basta']],
|
||
public_works:[['publicWorks','Construção ou reabilitação de obras públicas abrangidas15(2)l/15(9)'],['workCategoryMatches','A categoria eâmbito da obra correspondem ao projecto público identificado']],
|
||
public_water:[['potableWater','Fornecimento de água potável'],['publicNetwork','Fornecimento através de rede pública'],['publicFixedPrice','Preço fixado pela Autoridade Pública identificada na referência aplicável']],
|
||
} as const;
|
||
export const publicWorkCategories=[{id:'roads',label:'Estradas'},{id:'bridges',label:'Pontes'},{id:'water_infrastructure',label:'Infra-estruturas de abastecimento de água/saneamento15(9)a'},{id:'rural_electricity',label:'Electrificação rural em projectos públicos15(9)b'},{id:'agricultural_hydraulics',label:'Hidráulica agrícola no âmbito definido15(9)c'}] as const;
|
||
export function ownReducedConditions(treatment:string):ReadonlyArray<readonly[string,string]>{return reducedOwnConditions[treatment as keyof typeof reducedOwnConditions]??[];}
|
||
type Choice=''|'yes'|'no';type Condition=typeof reducedBaseConditions[number][0];
|
||
export type ReducedBaseRow={id:string;treatment:string;description:string;period:string;origin:'projected'|'effective';currency:string;operationDate:string;priceRef:string;priceScope:string;priceFrom:string;priceTo:string;documentRef:string;documentDate:string;consideration:string;projectRef:string;workCategory:string;compositionRef:string;evidence:string;conditions:Partial<Record<string,Choice>>};
|
||
type LineResult={id:string;status:'determined'|'not_applicable'|'indeterminate';baseAmount:string|null;deductionProportion:string|null;vat:null;rate:null;reason:string};
|
||
export type ReducedBaseCalculation={version:string;revision:number;calculatedAt:string;opportunity:string;location:string;sourceKey:string;source:string;inputs:ReducedBaseRow[];rows:LineResult[]};
|
||
export type ReducedBaseBook={rows:ReducedBaseRow[];result?:ReducedBaseCalculation;history?:ReducedBaseCalculation[]};
|
||
type Store=ReturnType<typeof readAcquisitionStore>&{regulatedReducedBases?:Record<string,ReducedBaseBook>};
|
||
export function newReducedBase(id:string,currency:string):ReducedBaseRow{return{id,treatment:'',description:'',period:'',origin:'projected',currency,operationDate:'',priceRef:'',priceScope:'',priceFrom:'',priceTo:'',documentRef:'',documentDate:'',consideration:'',projectRef:'',workCategory:'',compositionRef:'',evidence:'',conditions:{}};}
|
||
export function switchReducedTreatment(row:ReducedBaseRow,treatment:string):ReducedBaseRow{return{...row,treatment,conditions:Object.fromEntries(reducedBaseConditions.map(([key])=>[key,row.conditions[key]??'']))};}
|
||
|
||
function readStore(draft:Record<string,unknown>):Store{
|
||
const store=readAcquisitionStore(draft.modelo_a_snapshot) as Store;
|
||
if(store.regulatedReducedBases!==undefined&&(!store.regulatedReducedBases||typeof store.regulatedReducedBases!=='object'||Array.isArray(store.regulatedReducedBases)))throw new Error('Bases reduzidas não recuperáveis;originais preservados.');
|
||
const fields=Object.keys(newReducedBase('','')).filter(key=>key!=='conditions');
|
||
for(const book of Object.values(store.regulatedReducedBases??{}))if(!book||!Array.isArray(book.rows)||(book.history!==undefined&&!Array.isArray(book.history))||book.rows.some(row=>!row||fields.some(key=>typeof row[key as keyof ReducedBaseRow]!=='string')||!row.conditions||typeof row.conditions!=='object'||Array.isArray(row.conditions)))throw new Error('Linhas de bases reduzidas não recuperáveis;originais preservados,sem substituição por lista vazia.');
|
||
return store;
|
||
}
|
||
export function reducedBaseBook(draft:Record<string,unknown>):ReducedBaseBook{return readStore(draft).regulatedReducedBases?.[acquisitionScope(draft)]??{rows:[]};}
|
||
export function saveReducedBaseBook(draft:Record<string,unknown>,book:ReducedBaseBook):string{const current=readStore(draft),encoded=JSON.stringify({...current,regulatedReducedBases:{...current.regulatedReducedBases,[acquisitionScope(draft)]:book}});readStore({...draft,modelo_a_snapshot:encoded});return encoded;}
|
||
function decimal(raw:unknown):bigint|null{if(typeof raw!=='string'||!/^\d{1,15}(\.\d{1,6})?$/.test(raw.trim()))return null;const[whole,fraction='']=raw.trim().split('.');return BigInt(whole)*1000000n+BigInt(fraction.padEnd(6,'0'));}
|
||
function money(n:bigint):string{return`${n/100000000n}.${(n%100000000n).toString().padStart(8,'0').replace(/0+$/,'').padEnd(2,'0')}`;}
|
||
|
||
function validDate(value:string):boolean{const d=new Date(value+'T00:00:00Z');return /^\d{4}-\d{2}-\d{2}$/.test(value)&&Number.isFinite(d.getTime())&&d.toISOString().slice(0,10)===value;}
|
||
export function evaluateReducedBase(row:ReducedBaseRow,currency:unknown):LineResult{
|
||
const result:LineResult={id:row.id,status:'indeterminate',baseAmount:null,deductionProportion:null,vat:null,rate:null,reason:''};const stop=(reason:string,status:LineResult['status']='indeterminate')=>({...result,status,reason});
|
||
const rule=reducedBaseTreatments.find(t=>t.id===row.treatment);if(!rule)return stop('Identifique energia,taxas aeronáuticas,obras públicas ou água;sem percentagem por defeito.');
|
||
if(currency!=='MZN'||row.currency!==currency)return stop('Moeda fora do âmbitoMZN ou divergente do projecto;sem conversão silenciosa.');
|
||
if(!/^2026-(0[1-9]|1[0-2])$/.test(row.period)||!['projected','effective'].includes(row.origin))return stop('Indique mês2026 e dados efectivos ou pressupostos expressos de projecção.');
|
||
const conditions=[...reducedBaseConditions,...ownReducedConditions(row.treatment)];if(conditions.some(([key])=>row.conditions[key]==='no'))return stop('Este tratamento de base reduzida não se aplica aos factos declarados;rever natureza/composição/âmbito,sem isenção ou IVAzero presumido.','not_applicable');
|
||
if(conditions.some(([key])=>row.conditions[key]!=='yes'))return stop('Condições por esclarecer;desconhecido não é não aplicável ou zero. Confirme o montante total antes da redução,sem próprioIVA.');
|
||
if(!row.description.trim()||!row.documentRef.trim()||!row.compositionRef.trim()||!row.evidence.trim()||!row.priceScope.trim()||!validDate(row.documentDate)||!validDate(row.operationDate)||row.operationDate.slice(0,7)!==row.period)return stop('Identifique operação,data/mês,documento/data,âmbito e composição ou pressupostos expressos;reutilize referências sem certificado/aprovador novo.');
|
||
if(row.treatment==='public_works'){
|
||
if(!row.projectRef.trim()||!publicWorkCategories.some(c=>c.id===row.workCategory))return stop('Identifique projecto/contrato público,categoria eâmbito da obra15(2)l/15(9);não aplicar a qualquer obra privada.');
|
||
}else if(!row.priceRef.trim()||!validDate(row.priceFrom)||row.operationDate<row.priceFrom||(row.priceTo!==''&&(!validDate(row.priceTo)||row.priceFrom>row.priceTo||row.operationDate>row.priceTo)))return stop('Identifique referência aplicável do preço/taxa e intervalo que abrange a operação;fim pode ficar vazio sem termo fixado,com correspondência temporal confirmada. Sem preço actual presumido para outro período.');
|
||
const amount=decimal(row.consideration);if(amount===null)return stop('Indique contraprestação total antes da redução,sem próprioIVA;desconhecido/inválido não é zero. Se só existe total comIVA,separe a composição no percurso aplicável antes de usar este campo.');
|
||
const base=money(amount*BigInt(rule.percent));
|
||
return{...result,status:'determined',baseAmount:base,deductionProportion:String(rule.percent)+'%',reason:`${rule.branch}:15(2)${rule.clause},base=${row.consideration}×${rule.percent}%=${base}MZN,${row.origin==='projected'?'sob pressupostos expressos':'nos dados efectivos declarados'}. ${row.treatment==='public_works'?'Redução de60% significa base40%,não base60%.':'Percentagem aplicada uma única vez ao montante total antes da redução,sem próprioIVA.'} Art18(6),Lei10/2025:dedução relativa a estas operações na mesma proporção(${rule.percent}%),sujeita às condições próprias;isto não calcula nem concede dedução/campo11. Base não é imposto;sem taxaIVA,IVA ou totaisModeloA. Decimal exacto,sem arredondamento fiscal presumido.`};
|
||
}
|
||
|
||
function key(draft:Record<string,unknown>,book:ReducedBaseBook):string{return JSON.stringify({version:reducedBaseVersion,scope:acquisitionScope(draft),location:draft.selection_labels??{},currency:draft.currency,rows:book.rows});}
|
||
export function reducedBaseCurrent(draft:Record<string,unknown>,book:ReducedBaseBook):boolean{return!!book.result&&book.result.sourceKey===key(draft,book);}
|
||
export function recordReducedBases(draft:Record<string,unknown>,book:ReducedBaseBook):ReducedBaseBook{
|
||
if(!book.rows.length||new Set(book.rows.map(row=>row.id)).size!==book.rows.length)throw new Error('Indique operações com identificadores distintos;originais conservados.');
|
||
const result:ReducedBaseCalculation={version:reducedBaseVersion,revision:(book.result?.revision??0)+1,calculatedAt:new Date().toISOString(),opportunity:acquisitionScope(draft),location:JSON.stringify(draft.selection_labels??{}),sourceKey:key(draft,book),source:reducedBaseSource,inputs:structuredClone(book.rows),rows:book.rows.map(row=>evaluateReducedBase(row,draft.currency))};
|
||
const next={...book,result,history:[...(book.history??[]),...(book.result?[book.result]:[])]};saveReducedBaseBook(draft,next);return next;
|
||
}
|
||
export function reducedBaseReportRows(draft:Record<string,unknown>):Array<[string,string,string,string]>{
|
||
try{const book=reducedBaseBook(draft),result=book.result;if(!result)return[];return result.inputs.map((row,index)=>['IVA mensal',`Base ${reducedBaseTreatments.find(t=>t.id===row.treatment)?.branch??'reduzida'} — ${row.period} — ${row.id}`,`${row.description};base tributável: ${result.rows[index].baseAmount??'Indeterminada'} ${row.currency};IVA: Indeterminado;proporção18(6):${result.rows[index].deductionProportion??'Indeterminada'}(sem dedução calculada);${result.rows[index].reason}`,`${result.opportunity};${result.location};${reducedBaseCurrent(draft,book)?'Resultado actual':'Entradas alteradas;resultado histórico conservado'};versão${result.revision};${result.calculatedAt};${result.source};${row.origin==='projected'?'Projecção':'Dados efectivos declarados'};operação ${row.operationDate};âmbito ${row.priceScope};${row.treatment==='public_works'?`projecto/contrato ${row.projectRef};categoria ${row.workCategory}`:`preço/taxa ${row.priceRef};intervalo ${row.priceFrom} a ${row.priceTo||'sem termo fixado,correspondência confirmada'}`};contraprestação total antes redução ${row.consideration} ${row.currency};composição ${row.compositionRef};documento ${row.documentRef},${row.documentDate};condições ${[...reducedBaseConditions,...ownReducedConditions(row.treatment)].map(([key,label])=>label+': '+(row.conditions[key]==='yes'?'Sim':row.conditions[key]==='no'?'Não':'Por indicar')).join('; ')};fundamento ${row.evidence};sem dedução/campo11/totaisModeloA.`]);}
|
||
catch{return[['IVA mensal','Bases reduzidas','Dados não recuperáveis','Originais preservados;sem substituir por zero.']];}
|
||
}
|