Files
Investidor-V2/resources/js/lib/iva-value-review.ts
T

65 lines
12 KiB
TypeScript

import {acquisitionScope,readAcquisitionStore} from './iva-acquisitions.ts';
export const valueReviewVersion='value-review-base/1';
export const valueReviewSource='CIVA republicado Lei13/2016,15(7),PDF14/p1426(272):para efeitos15(1),quando a contraprestação é inferior à resultante dos preços correntes ou normais de venda à porta da fábrica,por grosso ou a retalho,ou aos preços correntes/normais do serviço,tendo em conta a natureza das transmissões,a Administração Tributária pode proceder à correcção. Lei22/2022PDF3mantém15(7);Lei10/2025não altera15. Recorte2026/MZN;comparação não exerce o poder da AT,nem substitui automaticamente a contraprestação pelo valor de referência. Sem incidência,taxa,IVA,dedução/campo11,regularizações ou totaisModeloA.';
export const valueReviewTreatments=[{id:'price_comparison',label:'Comparar sem correcção automática'},{id:'referenced_correction',label:'Registar correcção referenciada / cenário condicionado'}] as const;
export const priceContexts=[{id:'factory',label:'Venda à porta da fábrica'},{id:'wholesale',label:'Venda por grosso'},{id:'retail',label:'Venda a retalho'},{id:'service',label:'Preço corrente ou normal do serviço'}] as const;
export const valueReviewConditions=[
['taxableOperation','A operação e a contraprestação para efeitos15(1)estão identificadas'],
['referenceCorresponds','O preço corrente ou normal e o contexto escolhido correspondem à natureza desta transmissão ou serviço,no período e âmbito identificados'],
['netComposition','Os montantes comparados têm composição equivalente,sem próprioIVA,com inclusões/exclusões aplicáveis15(5)/(6)consideradas'],
] as const;
type Choice=''|'yes'|'no';type Condition=typeof valueReviewConditions[number][0];
export type ValueReviewRow={id:string;treatment:string;priceContext:string;description:string;period:string;origin:'projected'|'effective';currency:string;operationRef:string;documentRef:string;documentDate:string;declaredAmount:string;referenceAmount:string;referenceRef:string;referenceScope:string;compositionRef:string;evidence:string;correctedAmount:string;correctionRef:string;correctionScope:string;correctionDate:string;correctionApplies:Choice;conditions:Partial<Record<Condition,Choice>>};
type LineResult={id:string;status:'determined'|'not_applicable'|'indeterminate';baseAmount:string|null;vat:null;rate:null;comparison:'below'|'not_below'|null;reason:string};
export type ValueReviewCalculation={version:string;revision:number;calculatedAt:string;opportunity:string;location:string;sourceKey:string;source:string;inputs:ValueReviewRow[];rows:LineResult[]};
export type ValueReviewBook={rows:ValueReviewRow[];result?:ValueReviewCalculation;history?:ValueReviewCalculation[]};
type Store=ReturnType<typeof readAcquisitionStore>&{valueReviews?:Record<string,ValueReviewBook>};
export function newValueReview(id:string,currency:string):ValueReviewRow{return{id,treatment:'',priceContext:'',description:'',period:'',origin:'projected',currency,operationRef:'',documentRef:'',documentDate:'',declaredAmount:'',referenceAmount:'',referenceRef:'',referenceScope:'',compositionRef:'',evidence:'',correctedAmount:'',correctionRef:'',correctionScope:'',correctionDate:'',correctionApplies:'',conditions:{}};}
export function switchValueReview(row:ValueReviewRow,treatment:string):ValueReviewRow{return{...row,treatment,correctionApplies:''};}
export function switchPriceContext(row:ValueReviewRow,priceContext:string):ValueReviewRow{return{...row,priceContext,conditions:{...row.conditions,referenceCorresponds:''},correctionApplies:''};}
export function switchValueOrigin(row:ValueReviewRow,origin:ValueReviewRow['origin']):ValueReviewRow{return{...row,origin,correctionApplies:''};}
function readStore(draft:Record<string,unknown>):Store{
const store=readAcquisitionStore(draft.modelo_a_snapshot) as Store;
if(store.valueReviews!==undefined&&(!store.valueReviews||typeof store.valueReviews!=='object'||Array.isArray(store.valueReviews)))throw new Error('Bases de contraprestação não recuperáveis;originais preservados.');
const fields=Object.keys(newValueReview('','')).filter(key=>key!=='conditions');
for(const book of Object.values(store.valueReviews??{}))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 ValueReviewRow]!=='string')||!row.conditions||typeof row.conditions!=='object'||Array.isArray(row.conditions)))throw new Error('Linhas de contraprestação não recuperáveis;originais preservados,sem substituição por lista vazia.');
return store;
}
export function valueReviewBook(draft:Record<string,unknown>):ValueReviewBook{return readStore(draft).valueReviews?.[acquisitionScope(draft)]??{rows:[]};}
export function saveValueReviewBook(draft:Record<string,unknown>,book:ValueReviewBook):string{const current=readStore(draft),encoded=JSON.stringify({...current,valueReviews:{...current.valueReviews,[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 evaluateValueReview(row:ValueReviewRow,currency:unknown):LineResult{
const result:LineResult={id:row.id,status:'indeterminate',baseAmount:null,vat:null,rate:null,comparison:null,reason:''};const stop=(reason:string,status:LineResult['status']='indeterminate')=>({...result,status,reason});
if(!valueReviewTreatments.some(item=>item.id===row.treatment)||!priceContexts.some(item=>item.id===row.priceContext))return stop('Identifique o percurso e o contexto do preço de referência;desconhecido não permite presumir correcção.');
if(currency!=='MZN'||row.currency!==currency)return stop('Moeda fora do âmbito MZN 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ês em2026 e dados efectivos declarados ou pressupostos expressos de projecção.');
const failed=valueReviewConditions.filter(([key])=>row.conditions[key]==='no');if(failed.length)return stop('Não se aplica esta comparação: '+failed.map(([,label])=>label).join('; ')+'. Não é isenção nem IVA zero.','not_applicable');
if(valueReviewConditions.some(([key])=>row.conditions[key]!=='yes'))return stop('Confirme as condições próprias da comparação;desconhecido não é zero,não aplicável ou correcção.');
if(!row.description.trim()||!row.operationRef.trim()||!row.documentRef.trim()||!validDate(row.documentDate)||!row.referenceRef.trim()||!row.referenceScope.trim()||!row.compositionRef.trim()||!row.evidence.trim())return stop('Identifique operação,documento/data,preço corrente/normal,referência/âmbito e composição,ou pressupostos expressos;sem avaliação automática/certificado/aprovador novo.');
const declared=decimal(row.declaredAmount),reference=decimal(row.referenceAmount);if(declared===null||reference===null)return stop('Indique contraprestação e preço de referência comparáveis;valor desconhecido ou inválido não é zero.');
result.comparison=declared<reference?'below':'not_below';
const comparison=declared<reference?`Contraprestação ${money(declared)} inferior à referência ${money(reference)} por ${money(reference-declared)}MZN;AT pode proceder à correcção,sem automatismo.`:`Contraprestação ${money(declared)} não inferior à referência ${money(reference)}MZN;não se demonstra este pressuposto15(7).`;
if(row.treatment==='price_comparison')return{...result,status:'determined',baseAmount:money(declared),reason:comparison+' Contraprestação declarada conservada como base neste recorte,sem correcção pelo portal;IVA/taxa,dedução/campo11,regularizações e totaisModeloA não determinados.'};
if(declared>=reference)return stop(comparison+' A correcção deste percurso15(7)não é aplicada. Registo e referências conservados.','not_applicable');
if(row.correctionApplies==='no')return stop(comparison+' A correcção referenciada não abrange esta operação/período/condições;sem aplicação automática.','not_applicable');
if(row.correctionApplies!=='yes'||!row.correctionRef.trim()||!row.correctionScope.trim()||!validDate(row.correctionDate))return stop(comparison+' Identifique referência,data e âmbito da correcção AT aplicável ou pressuposto expresso do cenário condicionado;comparar não substitui uma correcção.');
const corrected=decimal(row.correctedAmount);if(corrected===null)return stop(comparison+' Indique o montante da correcção referenciada ou pressuposto condicionado;sem usar automaticamente o maior valor ou a referência.');
return{...result,status:'determined',baseAmount:money(corrected),reason:comparison+` ${row.origin==='projected'?'Cenário condicionado à correcção expressamente assumida,não decisão AT concedida':'Correcção AT nos dados efectivos declarados,com referência/data/âmbito identificados'}:base ${money(corrected)}MZN. Montante da referência não substituído automaticamente. Sem IVA/taxa,dedução/campo11,regularizações ou totaisModeloA.`};
}
function key(draft:Record<string,unknown>,book:ValueReviewBook):string{return JSON.stringify({version:valueReviewVersion,scope:acquisitionScope(draft),location:draft.selection_labels??{},currency:draft.currency,rows:book.rows});}
export function valueReviewCurrent(draft:Record<string,unknown>,book:ValueReviewBook):boolean{return!!book.result&&book.result.sourceKey===key(draft,book);}
export function recordValueReviews(draft:Record<string,unknown>,book:ValueReviewBook):ValueReviewBook{
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:ValueReviewCalculation={version:valueReviewVersion,revision:(book.result?.revision??0)+1,calculatedAt:new Date().toISOString(),opportunity:acquisitionScope(draft),location:JSON.stringify(draft.selection_labels??{}),sourceKey:key(draft,book),source:valueReviewSource,inputs:structuredClone(book.rows),rows:book.rows.map(row=>evaluateValueReview(row,draft.currency))};
const next={...book,result,history:[...(book.history??[]),...(book.result?[book.result]:[])]};saveValueReviewBook(draft,next);return next;
}
export function valueReviewReportRows(draft:Record<string,unknown>):Array<[string,string,string,string]>{
try{const book=valueReviewBook(draft),result=book.result;if(!result)return[];return result.inputs.map((row,index)=>['IVA mensal',`Base P103 — ${row.period} — ${row.id}`,`${row.description};base tributável: ${result.rows[index].baseAmount??'Indeterminada'} ${row.currency};IVA: Indeterminado;${result.rows[index].reason}`,`${result.opportunity};${result.location};${valueReviewCurrent(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'};${valueReviewTreatments.find(t=>t.id===row.treatment)?.label??'Percurso por indicar'};contexto ${priceContexts.find(t=>t.id===row.priceContext)?.label??'Por indicar'};operação ${row.operationRef};contraprestação ${row.declaredAmount};preço referência ${row.referenceAmount},${row.referenceRef};âmbito ${row.referenceScope};${row.treatment==='referenced_correction'?`correcção ${row.correctedAmount},referência ${row.correctionRef},data ${row.correctionDate},âmbito ${row.correctionScope},aplica:${row.correctionApplies==='yes'?'Sim':row.correctionApplies==='no'?'Não':'Por indicar'}`:'Sem correcção automática'};composição ${row.compositionRef};documento ${row.documentRef},${row.documentDate};condições ${valueReviewConditions.map(([key,label])=>label+': '+(row.conditions[key]==='yes'?'Sim':row.conditions[key]==='no'?'Não':'Por indicar')).join('; ')};fundamento ${row.evidence};não preenche totaisModeloA ou campo11.`]);}
catch{return[['IVA mensal','Comparação/correcção da contraprestação','Dados não recuperáveis','Originais preservados;sem substituir por zero.']];}
}