Files
Investidor-V2/resources/js/lib/iva-goods-allocation.ts
T

66 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {acquisitionScope,readAcquisitionStore} from './iva-acquisitions.ts';
export const goodsAllocationVersion='goods-allocation-base/1';
export const goodsAllocationSource='CIVA3(3)e/f,Lei13/2016PDF6–7/p1426(264–265):afectação permanente de bens a uso próprio/particular/alheio,transmissão gratuita,afectação a sector isento ou ao activo imobilizado de bens20(1)a,com dedução total/parcial anterior nos bens/elementos;excepção de amostras/ofertas de pequeno valor na alíneae. Art15(2)b,PDF13/p1426(271):preço de aquisição ou,na sua falta,preço de custo,reportados ao momento da operação. Art20(1)a,PDF15/p1426(273). Lei22/2022PDF3mantém15(2)b e20(1)a;Lei10/2025mantém3(3)e/f e não altera15/20. Recorte2026/MZN,sem nova dedução/campo11,regularização,taxa,IVA ou totaisModeloA. Uso temporário de bens como serviçoP089 é distinto.';
export const goodsAllocationTreatments=[{id:'permanent_private_goods',label:'Afectação permanente a uso próprio,particular ou fins alheios',clause:'e'},{id:'free_goods_transfer',label:'Transmissão gratuita de bens',clause:'e'},{id:'exempt_sector_goods',label:'Afectação de bens a sector de actividade isento',clause:'f'},{id:'fixed_asset_20a',label:'Afectação ao activo imobilizado de bens20(1)a',clause:'f'}] as const;
export const goodsAllocationConditions=[['businessGoods','São bens da empresa identificados,não uma prestação de serviços ou venda onerosa normal'],['priorDeduction','Houve dedução total ou parcial anterior nos bens ou elementos que os constituem,com referência histórica'],['factsCorrespond','Objecto,destino,operação,data eâmbito correspondem ao percurso identificado']] as const;
export const allocationOwnConditions={
permanent_private_goods:[['permanentAllocation','A afectação dos bens é permanente,não mero uso temporário'],['privatePurpose','Uso próprio do titular,pessoal ou fins alheios à empresa identificado'],['notSmallGift','Não se trata de amostra ou oferta de pequeno valor abrangida pela excepção3(3)e']],
free_goods_transfer:[['gratuitousTransfer','Está identificada uma transmissão gratuita dos bens'],['notSmallGift','Não se trata de amostra ou oferta de pequeno valor abrangida pela excepção3(3)e']],
exempt_sector_goods:[['exemptSector','Os bens são afectados ao sector de actividade isento identificado']],
fixed_asset_20a:[['fixedAsset','Está identificada a afectação ao activo imobilizado'],['goods20a','Os bens correspondem aos referidos em20(1)a,com âmbito e referência identificados']],
} as const;
export function allocationSpecificConditions(treatment:string):ReadonlyArray<readonly[string,string]>{return allocationOwnConditions[treatment as keyof typeof allocationOwnConditions]??[];}
type Choice=''|'yes'|'no';type Condition=typeof goodsAllocationConditions[number][0];
export type GoodsAllocationRow={id:string;treatment:string;description:string;period:string;origin:'projected'|'effective';currency:string;operationDate:string;operationRef:string;documentRef:string;documentDate:string;goodsRef:string;deductionRef:string;destinationRef:string;exceptionRef:string;asset20aRef:string;valuationMode:string;acquisitionPrice:string;acquisitionRef:string;currentCost:string;costRef:string;valuationDate:string;valuationScope:string;evidence:string;conditions:Partial<Record<string,Choice>>};
type LineResult={id:string;status:'determined'|'not_applicable'|'indeterminate';classification:'assimilated_goods'|'not_applicable'|'indeterminate';baseAmount:string|null;vat:null;rate:null;reason:string};
export type GoodsAllocationCalculation={version:string;revision:number;calculatedAt:string;opportunity:string;location:string;sourceKey:string;source:string;inputs:GoodsAllocationRow[];rows:LineResult[]};
export type GoodsAllocationBook={rows:GoodsAllocationRow[];result?:GoodsAllocationCalculation;history?:GoodsAllocationCalculation[]};
type Store=ReturnType<typeof readAcquisitionStore>&{regulatedGoodsAllocations?:Record<string,GoodsAllocationBook>};
export function newGoodsAllocation(id:string,currency:string):GoodsAllocationRow{return{id,treatment:'',description:'',period:'',origin:'projected',currency,operationDate:'',operationRef:'',documentRef:'',documentDate:'',goodsRef:'',deductionRef:'',destinationRef:'',exceptionRef:'',asset20aRef:'',valuationMode:'',acquisitionPrice:'',acquisitionRef:'',currentCost:'',costRef:'',valuationDate:'',valuationScope:'',evidence:'',conditions:{}};}
export function switchAllocationTreatment(row:GoodsAllocationRow,treatment:string):GoodsAllocationRow{return{...row,treatment,conditions:Object.fromEntries(goodsAllocationConditions.map(([key])=>[key,row.conditions[key]??'']))};}
export function switchAllocationValuation(row:GoodsAllocationRow,valuationMode:string):GoodsAllocationRow{return{...row,valuationMode,conditions:{...row.conditions,valuationConfirmed:'',acquisitionUnavailable:''}};}
function readStore(draft:Record<string,unknown>):Store{
const store=readAcquisitionStore(draft.modelo_a_snapshot) as Store;
if(store.regulatedGoodsAllocations!==undefined&&(!store.regulatedGoodsAllocations||typeof store.regulatedGoodsAllocations!=='object'||Array.isArray(store.regulatedGoodsAllocations)))throw new Error('Afectações de bens não recuperáveis;originais preservados.');
const fields=Object.keys(newGoodsAllocation('','')).filter(key=>key!=='conditions');
for(const book of Object.values(store.regulatedGoodsAllocations??{}))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 GoodsAllocationRow]!=='string')||!row.conditions||typeof row.conditions!=='object'||Array.isArray(row.conditions)))throw new Error('Linhas de afectações de bens não recuperáveis;originais preservados,sem substituição por lista vazia.');
return store;
}
export function goodsAllocationBook(draft:Record<string,unknown>):GoodsAllocationBook{return readStore(draft).regulatedGoodsAllocations?.[acquisitionScope(draft)]??{rows:[]};}
export function saveGoodsAllocationBook(draft:Record<string,unknown>,book:GoodsAllocationBook):string{const current=readStore(draft),encoded=JSON.stringify({...current,regulatedGoodsAllocations:{...current.regulatedGoodsAllocations,[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/1000000n}.${(n%1000000n).toString().padStart(6,'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 evaluateGoodsAllocation(row:GoodsAllocationRow,currency:unknown):LineResult{
const result:LineResult={id:row.id,status:'indeterminate',classification:'indeterminate',baseAmount:null,vat:null,rate:null,reason:''};const stop=(reason:string,status:LineResult['status']='indeterminate')=>({...result,status,classification:status==='not_applicable'?'not_applicable' as const:result.classification,reason});
const rule=goodsAllocationTreatments.find(t=>t.id===row.treatment);if(!rule)return stop('Identifique afectação permanente,transmissão gratuita,sector isento ou activo imobilizado20(1)a.');
if(!/^2026-(0[1-9]|1[0-2])$/.test(row.period)||!['projected','effective'].includes(row.origin)||!validDate(row.operationDate)||row.operationDate.slice(0,7)!==row.period)return stop('Identifique mês2026,data correspondente e dados efectivos ou pressupostos expressos.');
const conditions=[...goodsAllocationConditions,...allocationSpecificConditions(row.treatment)];if(conditions.some(([key])=>row.conditions[key]==='no'))return stop('Este percurso de transmissão assimilada não se aplica aos factos declarados;conferir natureza,dedução histórica ou excepção,não presumir isenção/IVAzero.','not_applicable');
if(conditions.some(([key])=>row.conditions[key]!=='yes'))return stop('Factos próprios ou dedução anterior por esclarecer;desconhecido não é não aplicável ou zero. Dedução histórica não é nova dedução concedida.');
if(!row.description.trim()||!row.operationRef.trim()||!row.documentRef.trim()||!validDate(row.documentDate)||!row.goodsRef.trim()||!row.deductionRef.trim()||!row.destinationRef.trim()||!row.evidence.trim())return stop('Identifique bens,operação,documento/data,dedução histórica,destino eâmbito factual ou pressupostos expressos;reutilize referências,sem certificado/aprovador novo.');
if(rule.clause==='e'&&!row.exceptionRef.trim())return stop('Identifique os factos/referência que distinguem a operação da excepção de amostras/ofertas de pequeno valor,ou pressuposto expresso;sem limiar inventado.');
if(row.treatment==='fixed_asset_20a'&&!row.asset20aRef.trim())return stop('Identifique enquadramento dos bens20(1)a eafectação ao activo imobilizado,com referência/âmbito.');
result.classification='assimilated_goods';
if(currency!=='MZN'||row.currency!==currency)return stop('Operação3(3)'+rule.clause+' identificada;moeda fora do recorteMZN ou divergente,base indeterminada,sem conversão silenciosa.');
if(!['acquisition','cost'].includes(row.valuationMode))return stop('Operação3(3)'+rule.clause+' identificada;base15(2)b por esclarecer:preço aquisição ou,na sua falta demonstrada,preço custo,reportados ao momento da operação.');
if(row.conditions.valuationConfirmed!=='yes'||!validDate(row.valuationDate)||row.valuationDate!==row.operationDate||!row.valuationScope.trim())return stop('Operação assimilada identificada;base por esclarecer. Confirme preço de aquisição/custo reportado ao momento desta operação,data/âmbito ecomposição sem próprioIVA;não usar custo histórico automaticamente.');
if(row.valuationMode==='cost'&&row.conditions.acquisitionUnavailable!=='yes')return stop('Operação assimilada identificada;não substituir aquisição desconhecida por custo. O custo15(2)b só é usado na falta demonstrada de preço aquisição;conferir esse facto.');
const raw=row.valuationMode==='acquisition'?row.acquisitionPrice:row.currentCost,ref=row.valuationMode==='acquisition'?row.acquisitionRef:row.costRef;const amount=decimal(raw);
if(amount===null||!ref.trim())return stop('Operação assimilada identificada;indique o preço e referência correspondentes ao modo escolhido,reportados ao momento da operação;desconhecido não é zero.');
return{...result,status:'determined',baseAmount:money(amount),reason:`P124:transmissão assimilada3(3)${rule.clause};P088:base15(2)b=${money(amount)}MZN,${row.valuationMode==='acquisition'?'preço aquisição':'preço custo,na falta demonstrada de preço aquisição'},reportado a${row.operationDate},${row.origin==='projected'?'sob pressupostos expressos':'segundo dados efectivos declarados'}. Dedução anterior total/parcial é facto histórico dos bens/elementos,não nova dedução/campo11. Não confundir afectação/transmissão de bens comserviço por uso temporárioP089. Sem calcular IVA,taxa,regularização ou totaisModeloA.`};
}
function key(draft:Record<string,unknown>,book:GoodsAllocationBook):string{return JSON.stringify({version:goodsAllocationVersion,scope:acquisitionScope(draft),location:draft.selection_labels??{},currency:draft.currency,rows:book.rows});}
export function goodsAllocationCurrent(draft:Record<string,unknown>,book:GoodsAllocationBook):boolean{return!!book.result&&book.result.sourceKey===key(draft,book);}
export function recordGoodsAllocations(draft:Record<string,unknown>,book:GoodsAllocationBook):GoodsAllocationBook{
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:GoodsAllocationCalculation={version:goodsAllocationVersion,revision:(book.result?.revision??0)+1,calculatedAt:new Date().toISOString(),opportunity:acquisitionScope(draft),location:JSON.stringify(draft.selection_labels??{}),sourceKey:key(draft,book),source:goodsAllocationSource,inputs:structuredClone(book.rows),rows:book.rows.map(row=>evaluateGoodsAllocation(row,draft.currency))};
const next={...book,result,history:[...(book.history??[]),...(book.result?[book.result]:[])]};saveGoodsAllocationBook(draft,next);return next;
}
export function goodsAllocationReportRows(draft:Record<string,unknown>):Array<[string,string,string,string]>{
try{const book=goodsAllocationBook(draft),result=book.result;if(!result)return[];return result.inputs.map((row,index)=>{const line=result.rows[index],rule=goodsAllocationTreatments.find(t=>t.id===row.treatment);return['IVA mensal',`Afectação P124/baseP088 — ${row.period} — ${row.id}`,`${row.description};classificação:${line.classification==='assimilated_goods'?'Transmissão assimilada de bens':line.classification==='not_applicable'?'Percurso não aplicável':'Indeterminada'};base:${line.baseAmount??'Indeterminada'} ${row.currency};IVA: Indeterminado;${line.reason}`,`${result.opportunity};${result.location};${goodsAllocationCurrent(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'};${rule?.label??'Percurso por indicar'};operação ${row.operationRef},${row.operationDate};bens ${row.goodsRef};dedução histórica ${row.deductionRef};destino ${row.destinationRef};${rule?.clause==='e'?`excepção amostras/ofertas ${row.exceptionRef};`:''}${row.treatment==='fixed_asset_20a'?`enquadramento20(1)a ${row.asset20aRef};`:''}base ${row.valuationMode};${row.valuationMode==='cost'?`custo ${row.currentCost},${row.costRef};falta aquisição ${row.conditions.acquisitionUnavailable??'por indicar'}`:`aquisição ${row.acquisitionPrice},${row.acquisitionRef}`};data/âmbito base ${row.valuationDate},${row.valuationScope};documento ${row.documentRef},${row.documentDate};condições ${[...goodsAllocationConditions,...allocationSpecificConditions(row.treatment)].map(([key,label])=>label+': '+(row.conditions[key]==='yes'?'Sim':row.conditions[key]==='no'?'Não':'Por indicar')).join('; ')};fundamento ${row.evidence};sem nova dedução/campo11/IVA/regularização/totaisModeloA.`];});}
catch{return[['IVA mensal','Afectação de bens','Dados não recuperáveis','Originais preservados;sem base ou classificação por defeito.']];}
}