104 lines
18 KiB
TypeScript
104 lines
18 KiB
TypeScript
import {selectFx,fxSelectionKey,fxTempoSource,type FxSelection} from './iva-fx-selection.ts';
|
||
import {acquisitionScope,readAcquisitionStore} from './iva-acquisitions.ts';
|
||
import {convertCents as convertScaledAmount} from './business-financial-plan.ts';
|
||
export const currencyEventVersion='currency-events/1';
|
||
export const currencyEventSource='CIVA Lei13/2016,7(1)–(7),PDF8/p1426(266);8,PDF9/p1426(267);15(8),PDF14/p1426(272).Lei22/2022 eLei10/2025 não alteram7/8,15(8)mantido.RIVA21/2008PDF4:regra histórica5dias úteis,não prazo automático vigente;usar prazo e disposição aplicáveis identificados. P104:orientação operacional da Tempo25/10/2026 compra/venda/médiaBM conforme natureza e perspectiva;data de submissão indicada;excepções fiscais identificadas. Cenários históricos permanecem condicionados;sem base/IVA/dedução automaticamente activados.';
|
||
export const currencyEventTreatments=[
|
||
{id:'foreign_scenario',branch:'P104',label:'Moeda estrangeira — selecção operacional / cenário histórico'},
|
||
{id:'goods_available',branch:'P131',label:'Bens postos à disposição7(1)a'},
|
||
{id:'goods_transport',branch:'P131',label:'Início do transporte7(2)'},
|
||
{id:'goods_installation',branch:'P131',label:'Instalação/montagem concluída7(2)'},
|
||
{id:'service_completed',branch:'P131',label:'Serviço realizado7(1)b'},
|
||
{id:'service_early_charge',branch:'P131',label:'Serviço cobrado/debitado antes da realização7(1)b'},
|
||
{id:'import_clearance',branch:'P131',label:'Desembaraço aduaneiro7(1)c'},
|
||
{id:'import_auction',branch:'P131',label:'Arrematação/venda na importação7(1)c'},
|
||
{id:'continuous_period',branch:'P131',label:'Termo do período de pagamento sucessivo7(3)'},
|
||
{id:'contract_effects',branch:'P131',label:'Efeitos translativos posteriores7(7)'},
|
||
{id:'own_goods',branch:'P132',label:'Afectação/transmissão assimilada3(3)e/f e7(4)'},
|
||
{id:'own_services',branch:'P132',label:'Serviço assimilado4(2) e7(4)'},
|
||
{id:'commission_delivery',branch:'P132',label:'Comissário põe bens à disposição7(5)'},
|
||
{id:'consignment_deadline',branch:'P132',label:'Termo de180dias da consignação7(6)'},
|
||
{id:'advance_payment',branch:'P132',label:'Pagamento anterior à factura8c'},
|
||
] as const;
|
||
type Choice=''|'yes'|'no';
|
||
export type CurrencyEventRow={fxSelection?:FxSelection;id:string;treatment:string;description:string;period:string;origin:'projected'|'effective';operationRef:string;documentRef:string;documentDate:string;scope:string;evidence:string;eventDate:string;eventRef:string;otherDate:string;reviewDate:string;factsConfirmed:Choice;invoiceRequired:Choice;invoiceState:''|'issued'|'not_issued';invoiceDate:string;invoiceRef:string;legalDeadline:string;deadlineRef:string;paymentState:''|'none'|'received';paymentDate:string;paymentRef:string;amount:string;paymentAmount:string;currency:string;sourceCurrency:string;fxRate:string;fxDate:string;fxSource:string;fxRuleRef:string;fxDirection:''|'mzn_per_unit'|'units_per_mzn';};
|
||
type DueEvent={date:string;amount:string|null;reason:string};
|
||
type LineResult={id:string;status:'determined'|'not_applicable'|'indeterminate'|'conditional';eventDate:string|null;dueEvents:DueEvent[];remainingPending:boolean;scenarioAmount:string|null;baseAmount:null;vat:null;rate:null;reason:string};
|
||
export type CurrencyEventCalculation={version:string;revision:number;calculatedAt:string;opportunity:string;location:string;sourceKey:string;source:string;inputs:CurrencyEventRow[];rows:LineResult[]};
|
||
export type CurrencyEventBook={rows:CurrencyEventRow[];result?:CurrencyEventCalculation;history?:CurrencyEventCalculation[]};
|
||
type Store=ReturnType<typeof readAcquisitionStore>&{currencyEvents?:Record<string,CurrencyEventBook>};
|
||
export function newCurrencyEvent(id:string,currency:string):CurrencyEventRow{return{id,treatment:'',description:'',period:'',origin:'projected',operationRef:'',documentRef:'',documentDate:'',scope:'',evidence:'',eventDate:'',eventRef:'',otherDate:'',reviewDate:'',factsConfirmed:'',invoiceRequired:'',invoiceState:'',invoiceDate:'',invoiceRef:'',legalDeadline:'',deadlineRef:'',paymentState:'',paymentDate:'',paymentRef:'',amount:'',paymentAmount:'',currency,sourceCurrency:'',fxRate:'',fxDate:'',fxSource:'',fxRuleRef:'',fxDirection:''};}
|
||
export function switchCurrencyEvent(row:CurrencyEventRow,treatment:string):CurrencyEventRow{return{...row,treatment,factsConfirmed:'',invoiceRequired:''};}
|
||
function readStore(draft:Record<string,unknown>):Store{const store=readAcquisitionStore(draft.modelo_a_snapshot) as Store;
|
||
if(store.currencyEvents!==undefined&&(!store.currencyEvents||typeof store.currencyEvents!=='object'||Array.isArray(store.currencyEvents)))throw new Error('Moeda/eventos não recuperáveis;originais preservados.');
|
||
const fields=Object.keys(newCurrencyEvent('',''));
|
||
for(const b of Object.values(store.currencyEvents??{}))if(!b||!Array.isArray(b.rows)||(b.history!==undefined&&!Array.isArray(b.history))||b.rows.some(r=>!r||fields.some(k=>typeof r[k as keyof CurrencyEventRow]!=='string')))throw new Error('Linhas de moeda/eventos não recuperáveis;sem lista vazia por defeito.');return store;}
|
||
export function currencyEventBook(draft:Record<string,unknown>):CurrencyEventBook{return readStore(draft).currencyEvents?.[acquisitionScope(draft)]??{rows:[]};}
|
||
export function saveCurrencyEventBook(draft:Record<string,unknown>,book:CurrencyEventBook):string{const store=readStore(draft),encoded=JSON.stringify({...store,currencyEvents:{...store.currencyEvents,[acquisitionScope(draft)]:book}});readStore({...draft,modelo_a_snapshot:encoded});return encoded;}
|
||
function date(s:string):number|null{const n=Date.parse(s+'T00:00:00Z');return /^\d{4}-\d{2}-\d{2}$/.test(s)&&Number.isFinite(n)&&new Date(n).toISOString().slice(0,10)===s?n:null;}
|
||
function decimal(s:string,places=6):bigint|null{if(!new RegExp('^\\d{1,15}(\\.\\d{1,'+places+'})?$').test(s.trim()))return null;const[w,f='']=s.trim().split('.');return BigInt(w)*10n**BigInt(places)+BigInt(f.padEnd(places,'0'));}
|
||
function money(n:bigint):string{return`${n/1000000n}.${(n%1000000n).toString().padStart(6,'0').replace(/0+$/,'').padEnd(2,'0')}`;}
|
||
export function evaluateCurrencyEvent(row:CurrencyEventRow):LineResult{
|
||
const result:LineResult={id:row.id,status:'indeterminate',eventDate:null,dueEvents:[],remainingPending:false,scenarioAmount:null,baseAmount:null,vat:null,rate:null,reason:''};const stop=(reason:string,status:LineResult['status']='indeterminate')=>({...result,status,reason});
|
||
if(!currencyEventTreatments.some(t=>t.id===row.treatment))return stop('Identifique percurso de moeda/evento.');
|
||
if(!/^2026-(0[1-9]|1[0-2])$/.test(row.period)||!['projected','effective'].includes(row.origin)||date(row.documentDate)===null||![row.description,row.operationRef,row.documentRef,row.scope,row.evidence].every(v=>!!v.trim()))return stop('Identifique mês de contexto2026,operação,documento/data,âmbito e factos ou pressupostos expressos.');
|
||
if(row.factsConfirmed==='no')return stop('Os factos declarados afastam este percurso;sem isenção ou IVA zero por defeito.','not_applicable');
|
||
if(row.factsConfirmed!=='yes')return stop('Factos do percurso por esclarecer;não usar datas ou câmbio por defeito.');
|
||
if(row.treatment==='foreign_scenario'){
|
||
const amount=decimal(row.amount),fx=decimal(row.fxRate,8);
|
||
if(row.currency!=='MZN'||!['USD','EUR','ZAR','GBP','CNY','BRL','JPY','CHF','AUD','CAD'].includes(row.sourceCurrency)||amount===null||fx===null||fx===0n||date(row.fxDate)===null||![row.fxSource,row.fxRuleRef].every(v=>!!v.trim())||!['mzn_per_unit','units_per_mzn'].includes(row.fxDirection))return stop('Indique moeda original,valor,taxa positiva,direcção,data,fonte e regra/pressuposto expresso.Sem substituir moeda original porMZN ou reutilizar taxa de apresentação como regra fiscal.');
|
||
result.scenarioAmount=money(row.fxDirection==='mzn_per_unit'?convertScaledAmount(amount,fx,100000000n):(amount*100000000n+fx/2n)/fx);
|
||
if(row.fxSelection){
|
||
const converted=result.scenarioAmount;result.scenarioAmount=null;
|
||
try{const selected=selectFx(row.fxSelection),applied=row.fxSelection.applied;if(!applied||applied.selectionKey!==fxSelectionKey(row.fxSelection)||applied.rate!==selected.rate||applied.date!==selected.date||row.sourceCurrency!==row.fxSelection.sourceCurrency||row.fxDirection!==row.fxSelection.direction||row.fxRate!==selected.rate||row.fxDate!==selected.date||row.fxSource!==applied.source)return stop('Selecção cambial alterada ou não aplicada explicitamente;histórico preservado,sem recálculo automático.');
|
||
result.scenarioAmount=converted;
|
||
const columnLabel=({buy:'Compra',sell:'Venda',mean:'Média de referência BM',exception:'Excepção fiscal identificada'} as Record<string,string>)[selected.column];
|
||
return stop(`Conversão operacional segundo a orientação da Tempo de25/10/2026:${row.amount} ${row.sourceCurrency} → ${result.scenarioAmount} MZN. Coluna ${columnLabel};taxa ${row.fxRate};${row.fxDirection==='mzn_per_unit'?'MZN por unidade da moeda original':'unidades da moeda original por MZN'};data ${row.fxDate};boletim ${row.fxSelection.bulletinRef}. Sem cálculo de IVA ou dedução e sem alteração dos livros fiscais anteriores.`,'determined');
|
||
}catch(error){return stop(String(error));}
|
||
}
|
||
return stop(`P104:cenário condicionado=${result.scenarioAmount}MZN,origem${row.sourceCurrency},taxa${row.fxRate},${row.fxDirection},data${row.fxDate}. CotejoLei2/2006 concluido:106(5)nao define taxa/data;regra complementar15(8) ainda por demonstrar;referência indicada é pressuposto,não validação da taxa legal.Base fiscal permanece indeterminada.Sem IVA ou activação nos livros anteriores.`,`conditional`);
|
||
}
|
||
const review=date(row.reviewDate),event=date(row.eventDate);if(review===null||!row.eventRef.trim())return stop('Identifique data de conferência e documento/âmbito do evento específico.');
|
||
if(row.treatment==='consignment_deadline'){
|
||
const delivery=date(row.otherDate);if(delivery===null)return stop('Identifique entrega das mercadorias à consignação3(3)d;7(6) exige termo180dias,não embalagensP102.');
|
||
result.eventDate=new Date(delivery+180*86400000).toISOString().slice(0,10);
|
||
}else if(row.treatment==='advance_payment'){
|
||
if(date(row.paymentDate)===null)return stop('Identifique recebimento anterior à factura para8c.');result.eventDate=null;
|
||
}else{
|
||
if(event===null)return stop('Identifique a data própria do evento7,sem inferir da emissão do documento.');result.eventDate=row.eventDate;
|
||
if(row.treatment==='service_early_charge'&&(date(row.otherDate)===null||event>=date(row.otherDate)!))return stop('Indique realização do serviço posterior à cobrança/débito;aplique o evento apenas ao montante respectivo.');
|
||
if(row.treatment==='contract_effects'&&(date(row.otherDate)===null||event<date(row.otherDate)!))return stop('Indique disponibilização anterior ou simultânea aos efeitos translativos;este7(7) exclui3(3)a/b,que têm percursos próprios.');
|
||
}
|
||
if(['import_clearance','import_auction'].includes(row.treatment)){
|
||
if(date(result.eventDate!)!>review)return stop('Evento aduaneiro futuro na conferência;sem exigibilidade declarada.');
|
||
result.dueEvents=[{date:result.eventDate!,amount:decimal(row.amount)===null?null:money(decimal(row.amount)!),reason:'7(1)c:desembaraço/arrematação/venda na importação;factura comercial não desloca o evento aduaneiro'}];return stop('Importação7(1)c:facto gerador/exigibilidade pelo evento aduaneiro identificado;sem aplicar8 por factura comercial,nem calcular base/IVA.','determined');
|
||
}
|
||
if(row.invoiceRequired==='')return stop('Identifique se esta operação dá lugar à obrigação de factura25;7 e8 são estados separados.');
|
||
if(row.invoiceRequired==='no'){
|
||
if(result.eventDate&&date(result.eventDate)!>review)return stop('Evento futuro na conferencia;sem exigibilidade declarada.');
|
||
if(row.treatment==='advance_payment')return stop('Percurso8c exige operação com obrigação de factura;conferir enquadramento.','not_applicable');
|
||
result.dueEvents=[{date:result.eventDate!,amount:decimal(row.amount)===null?null:money(decimal(row.amount)!),reason:'Exigibilidade segundo evento7,sem obrigação de factura declarada'}];return stop('Facto gerador e exigibilidade7 identificados no âmbito declarado;sem calcular IVA,base ou dedução.','determined');
|
||
}
|
||
const deadline=date(row.legalDeadline);if(deadline===null||!row.deadlineRef.trim()||(result.eventDate&&deadline<date(result.eventDate)!))return stop('Facto gerador identificado;indique prazo legal de emissão e disposição/calendário aplicáveis.Sem presumir automaticamente cinco dias úteis2008 ou feriados.');
|
||
if(!['issued','not_issued'].includes(row.invoiceState)||!['none','received'].includes(row.paymentState))return stop('Identifique emissão e recebimentos;desconhecido não é ausência de pagamento.');
|
||
const invoice=row.invoiceState==='issued'?date(row.invoiceDate):null;
|
||
if(row.invoiceState==='issued'&&(invoice===null||invoice>review||!row.invoiceRef.trim()))return stop('Identifique factura efectivamente emitida/data/referência até à conferência.');
|
||
const normalDate=invoice!==null&&invoice<=deadline?row.invoiceDate:row.legalDeadline;const total=decimal(row.amount);
|
||
let paid=0n;
|
||
if(row.paymentState==='received'){
|
||
const payment=date(row.paymentDate),amount=decimal(row.paymentAmount);
|
||
if(payment===null||payment>review||amount===null||total===null||amount>total||!row.paymentRef.trim())return stop('Identifique recebimento,parcela e total do âmbito na mesma moeda;pagamento não pode exceder o total.Sem duplicar parcelas nem substituir montante desconhecido porzero.');
|
||
if((invoice===null||payment<invoice)&&payment<date(normalDate)!){paid=amount;result.dueEvents.push({date:row.paymentDate,amount:money(amount),reason:'8c:montante recebido antes da factura,sem prejuízo do termo do prazo8b'});}
|
||
else if(row.treatment==='advance_payment')return stop('Pagamento não é anterior à factura/termo aplicável;não antecipar por8c.','not_applicable');
|
||
}
|
||
if(row.treatment==='advance_payment'&&row.paymentState!=='received')return stop('Identifique o recebimento anterior à factura.');
|
||
const remainder=total===null?null:total-paid;
|
||
if(date(normalDate)!<=review){if(remainder===null||remainder>0n)result.dueEvents.push({date:normalDate,amount:remainder===null?null:money(remainder),reason:invoice!==null&&invoice<=deadline?'8a:factura emitida dentro do prazo':'8b:termo do prazo de emissão'});}
|
||
else result.remainingPending=remainder===null||remainder>0n;
|
||
return stop('Exigibilidade separada em parcelas8a/b/c,sem dupla contagem do recebimento e restante. Facto gerador7 conservado;prazo legal parametrizado com referência aplicável.Sem IVA,base fiscal,nova dedução ou totaisModeloA.','determined');
|
||
}
|
||
function key(draft:Record<string,unknown>,book:CurrencyEventBook):string{return JSON.stringify({version:currencyEventVersion,scope:acquisitionScope(draft),location:draft.selection_labels??{},rows:book.rows});}
|
||
export function currencyEventCurrent(draft:Record<string,unknown>,book:CurrencyEventBook):boolean{return!!book.result&&book.result.sourceKey===key(draft,book);}
|
||
export function recordCurrencyEvents(draft:Record<string,unknown>,book:CurrencyEventBook):CurrencyEventBook{if(!book.rows.length||new Set(book.rows.map(r=>r.id)).size!==book.rows.length)throw new Error('Indique operações com identidades distintas;originais conservados.');const result:CurrencyEventCalculation={version:currencyEventVersion,revision:(book.result?.revision??0)+1,calculatedAt:new Date().toISOString(),opportunity:acquisitionScope(draft),location:JSON.stringify(draft.selection_labels??{}),sourceKey:key(draft,book),source:currencyEventSource,inputs:structuredClone(book.rows),rows:book.rows.map(evaluateCurrencyEvent)};const next={...book,result,history:[...(book.history??[]),...(book.result?[book.result]:[])]};saveCurrencyEventBook(draft,next);return next;}
|
||
export function currencyEventReportRows(draft:Record<string,unknown>):Array<[string,string,string,string]>{try{const book=currencyEventBook(draft),r=book.result;if(!r)return[];return r.inputs.map((row,i):[string,string,string,string]=>{if(row.treatment==='foreign_scenario'&&row.fxSelection)return['IVA mensal',`P104 — ${row.period} — ${row.id}`,`${row.description};${r.rows[i].reason};Original:${row.amount} ${row.sourceCurrency};Convertido:${r.rows[i].scenarioAmount??'Indeterminado'} MZN`,`${r.opportunity};${currencyEventCurrent(draft,book)?'Resultado actual':'Entradas alteradas;resultado histórico conservado'};versão${r.revision};${r.calculatedAt};documento:${row.documentRef},${row.documentDate};âmbito:${row.scope};boletim:${row.fxSelection.bulletinRef};fonte:${row.fxSelection.source};natureza:${row.fxSelection.nature};perspectiva:${row.fxSelection.bulletinPerspective};relação:${row.fxSelection.relation};taxa:${row.fxRate};data:${row.fxDate};sentido:${row.fxDirection};regra/excepção:${row.fxRuleRef};sem base/IVA/dedução activados.`];return['IVA mensal',`${currencyEventTreatments.find(t=>t.id===row.treatment)?.branch??'Evento'} — ${row.period} — ${row.id}`,`${row.description};${r.rows[i].reason};facto gerador:${r.rows[i].eventDate??'Indeterminado/não apurado'};parcelas:${r.rows[i].dueEvents.map(p=>p.date+': '+(p.amount??'Montante indeterminado')+' '+row.currency+' ('+p.reason+')').join('; ')||'Nenhuma apurada'};restante pendente:${r.rows[i].status==='indeterminate'?'Indeterminado':r.rows[i].remainingPending?'Sim':'Não'};${row.treatment==='foreign_scenario'&&r.rows[i].status==='determined'?'conversão operacional':'cenário condicionado'}:${r.rows[i].scenarioAmount??'Indeterminado'};base/IVA:Indeterminados`,`${r.opportunity};${r.location};${currencyEventCurrent(draft,book)?'Resultado actual':'Entradas alteradas;resultado histórico conservado'};versão${r.revision};${r.calculatedAt};${r.source};${row.origin};documento ${row.documentRef},${row.documentDate};ambito ${row.scope};${row.treatment==='foreign_scenario'?`origem ${row.amount} ${row.sourceCurrency};taxa ${row.fxRate},${row.fxDirection},${row.fxDate};fonte ${row.fxSource};pressuposto ${row.fxRuleRef}`:`evento ${row.eventDate},${row.eventRef};outra data ${row.otherDate};conferencia ${row.reviewDate};factura ${row.invoiceRequired},${row.invoiceState},${row.invoiceDate},${row.invoiceRef};prazo ${row.legalDeadline},${row.deadlineRef};recebimento ${row.paymentState},${row.paymentDate},${row.paymentAmount},${row.paymentRef};total do ambito ${row.amount} ${row.currency}`};fundamento ${row.evidence};sem nova dedução/campo11/IVA ou activação cambial nos livros anteriores.`];});}catch{return[['IVA mensal','Moeda/eventos','Dados não recuperáveis','Originais preservados;sem substituição porzero.']];}}
|