Implement P131 P132 event timing and conditional P104 draft in QA
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
import {useState} from 'react';
|
||||
import {currencyEventBook,currencyEventCurrent,currencyEventReportRows,currencyEventSource,currencyEventTreatments,newCurrencyEvent,recordCurrencyEvents,saveCurrencyEventBook,switchCurrencyEvent,type CurrencyEventBook,type CurrencyEventRow} from '@/lib/iva-currency-events';
|
||||
export function SimulationCurrencyEvents({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}){
|
||||
const[error,setError]=useState('');let book:CurrencyEventBook;try{book=currencyEventBook(draft);}catch(e){return<p role="alert">{String(e)}</p>;}
|
||||
const save=(next:CurrencyEventBook)=>{try{onChange('modelo_a_snapshot',saveCurrencyEventBook(draft,next));setError('');}catch(e){setError(String(e));}};
|
||||
const change=(id:string,patch:Partial<CurrencyEventRow>)=>save({...book,rows:book.rows.map(r=>r.id===id?{...r,...patch}:r)});
|
||||
return <details className="rounded border p-3"><summary>Moeda e datas do facto gerador/exigibilidade</summary><p>Facto gerador e exigibilidade são estados separados. Identifique a operação já preparada,sem repetir os seus cálculos. Conversão P104 é apenas cenário condicionado,enquanto a regra fiscal permanece por corresponder.</p><p>{currencyEventSource}</p>{error&&<p role="alert">{error}</p>}
|
||||
<button type="button" onClick={()=>save({...book,rows:[...book.rows,newCurrencyEvent(crypto.randomUUID(),String(draft.currency??''))]})}>Acrescentar moeda/evento</button>
|
||||
{book.rows.map(row=><details key={row.id} className="rounded border p-3"><summary>{row.description||'Operação por identificar'} — {row.period||'mês por indicar'}</summary>
|
||||
<label>Percurso de moeda/evento<select value={row.treatment} onChange={e=>change(row.id,switchCurrencyEvent(row,e.target.value))}><option value="">Por indicar</option>{currencyEventTreatments.map(t=><option key={t.id} value={t.id}>{t.label}</option>)}</select></label>
|
||||
<label>Natureza dos dados<select value={row.origin} onChange={e=>change(row.id,{origin:e.target.value as CurrencyEventRow['origin']})}><option value="projected">Projectados / pressupostos</option><option value="effective">Efectivos declarados</option></select></label>
|
||||
{([['description','Operação identificada'],['operationRef','Referência da operação existente'],['period','Mês do contexto'],['documentRef','Documento / referência da previsão'],['documentDate','Data do documento'],['scope','Âmbito e parcelas abrangidas'],['evidence','Factos ou pressupostos expressos'],['amount','Montante total do âmbito identificado'],['currency','Moeda do montante / destino da conversão']] as const).map(([field,label])=><label className="block" key={field}>{label}<input value={row[field]} type={field==='period'?'month':field==='documentDate'?'date':'text'} onChange={e=>change(row.id,{[field]:e.target.value})}/></label>)}
|
||||
<label>Factos próprios correspondem ao percurso,operação e âmbito<select value={row.factsConfirmed} onChange={e=>change(row.id,{factsConfirmed:e.target.value as CurrencyEventRow['factsConfirmed']})}><option value="">Por esclarecer</option><option value="yes">Sim</option><option value="no">Não</option></select></label>
|
||||
{row.treatment==='foreign_scenario'?<>
|
||||
{([['sourceCurrency','Moeda original'],['fxRate','Taxa de conversão indicada'],['fxDate','Data efectiva da taxa'],['fxSource','Fonte e exemplar da taxa'],['fxRuleRef','Regra/pressuposto da escolha da taxa e data']] as const).map(([field,label])=><label className="block" key={field}>{label}<input value={row[field]} type={field==='fxDate'?'date':'text'} onChange={e=>change(row.id,{[field]:e.target.value})}/></label>)}
|
||||
<label>Direcção da taxa<select value={row.fxDirection} onChange={e=>change(row.id,{fxDirection:e.target.value as CurrencyEventRow['fxDirection']})}><option value="">Por indicar</option><option value="mzn_per_unit">MZN por unidade da moeda original</option><option value="units_per_mzn">Unidades da moeda original por MZN</option></select></label><p>O valor convertido é uma simulação condicionada. Não substitui a base fiscal,nem altera os valores/moedas dos livros anteriores.</p>
|
||||
</>:row.treatment&&<>
|
||||
{([['eventDate','Data própria do evento escolhido'],['eventRef','Referência e factos do evento7/8'],['reviewDate','Data até à qual os eventos foram conferidos']] as const).filter(([field])=>field!=='eventDate'||!['advance_payment','consignment_deadline'].includes(row.treatment)).map(([field,label])=><label className="block" key={field}>{label}<input value={row[field]} type={field==='eventRef'?'text':'date'} onChange={e=>change(row.id,{[field]:e.target.value})}/></label>)}
|
||||
{['consignment_deadline','service_early_charge','contract_effects'].includes(row.treatment)&&<label>{row.treatment==='consignment_deadline'?'Entrega ao destinatário da consignação':row.treatment==='service_early_charge'?'Data de realização do serviço posterior à cobrança/débito':'Disponibilização anterior aos efeitos translativos'}<input type="date" value={row.otherDate} onChange={e=>change(row.id,{otherDate:e.target.value})}/></label>}
|
||||
{row.treatment==='contract_effects'&&<p>Este percurso7(7) exclui locação vinculativa e venda com reserva3(3)a/b. Confirme essa distinção nos factos.</p>}
|
||||
{row.treatment==='continuous_period'&&<p>Use o termo do período a que respeita cada pagamento sucessivo,não a data arbitrária de emissão.</p>}
|
||||
{row.treatment==='consignment_deadline'&&<p>7(6) fixa exigibilidade no termo180dias da consignação. P123 conserva a conferência da não devolução;embalagensP102 são distintas.</p>}
|
||||
{!['import_clearance','import_auction'].includes(row.treatment)&&<>
|
||||
<label>Operação sujeita à obrigação de emitir factura25<select value={row.invoiceRequired} onChange={e=>change(row.id,{invoiceRequired:e.target.value as CurrencyEventRow['invoiceRequired']})}><option value="">Por esclarecer</option><option value="yes">Sim</option><option value="no">Não</option></select></label>
|
||||
{row.invoiceRequired==='yes'&&<>
|
||||
{([['legalDeadline','Termo legal do prazo de emissão aplicável'],['deadlineRef','Disposição,calendário e âmbito que fundamentam esse prazo']] as const).map(([field,label])=><label className="block" key={field}>{label}<input value={row[field]} type={field==='legalDeadline'?'date':'text'} onChange={e=>change(row.id,{[field]:e.target.value})}/></label>)}
|
||||
<label>Facturação até à conferência<select value={row.invoiceState} onChange={e=>change(row.id,{invoiceState:e.target.value as CurrencyEventRow['invoiceState']})}><option value="">Por esclarecer</option><option value="issued">Factura emitida</option><option value="not_issued">Factura não emitida</option></select></label>
|
||||
{row.invoiceState==='issued'&&<>{([['invoiceDate','Data de emissão da factura'],['invoiceRef','Referência da factura']] as const).map(([field,label])=><label key={field}>{label}<input value={row[field]} type={field==='invoiceDate'?'date':'text'} onChange={e=>change(row.id,{[field]:e.target.value})}/></label>)}</>}
|
||||
<label>Recebimentos neste âmbito<select value={row.paymentState} onChange={e=>change(row.id,{paymentState:e.target.value as CurrencyEventRow['paymentState']})}><option value="">Por esclarecer</option><option value="none">Sem recebimento</option><option value="received">Recebimento identificado</option></select></label>
|
||||
{row.paymentState==='received'&&<>{([['paymentDate','Data de recebimento'],['paymentAmount','Montante recebido'],['paymentRef','Referência e âmbito do recebimento']] as const).map(([field,label])=><label className="block" key={field}>{label}<input value={row[field]} type={field==='paymentDate'?'date':'text'} onChange={e=>change(row.id,{[field]:e.target.value})}/></label>)}</>}
|
||||
<p>O recebimento anterior é separado do restante,sem dupla contagem. Para parcelas com factos distintos,use âmbitos distintos e conserve as referências.</p>
|
||||
</>}
|
||||
</>}
|
||||
</>}
|
||||
<p>Trocar percurso conserva dados ocultos e referências;reveja as confirmações aplicáveis.</p></details>)}
|
||||
{!!book.rows.length&&<button type="button" onClick={()=>{try{save(recordCurrencyEvents(draft,book));}catch(e){setError(String(e));}}}>Conferir moeda/eventos e guardar versão</button>}
|
||||
{book.result&&<><p role="status">{currencyEventCurrent(draft,book)?'Resultados correspondentes às entradas guardadas.':'Entradas alteradas;histórico conservado.'} {book.history?.length??0} versões anteriores.P104condicionado,semIVA/base/dedução activados.</p><dl>{currencyEventReportRows(draft).map(([,label,value,origin],i)=><div key={i}><dt>{label}</dt><dd>{value}<small className="block">{origin}</small></dd></div>)}</dl></>}
|
||||
</details>;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SimulationCurrencyEvents } from './simulation-currency-events';
|
||||
import { SimulationPropertyTransfers } from './simulation-property-transfers';
|
||||
import { SimulationGoodsPresumptions } from './simulation-goods-presumptions';
|
||||
import { InvestmentContextAssessment } from './investment-context-assessment';
|
||||
@@ -123,6 +124,7 @@ export function SimulationMonthlyIva({draft, busy, onCalculate, onChange}: {
|
||||
<SimulationReducedBases draft={draft} onChange={onChange} />
|
||||
<SimulationOwnName draft={draft} onChange={onChange} />
|
||||
<SimulationGoodsAllocation draft={draft} onChange={onChange} />
|
||||
<SimulationCurrencyEvents draft={draft} onChange={onChange} />
|
||||
<SimulationPropertyTransfers draft={draft} onChange={onChange} />
|
||||
<SimulationGoodsPresumptions draft={draft} onChange={onChange} />
|
||||
<SimulationSecondhand draft={draft} onChange={onChange} />
|
||||
|
||||
@@ -824,7 +824,7 @@ function scaledDecimal(value: string, scale: number, label: string): bigint {
|
||||
);
|
||||
}
|
||||
|
||||
function convertCents(
|
||||
export function convertCents(
|
||||
value: bigint,
|
||||
rateScaled: bigint,
|
||||
rateScale: bigint,
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
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:Lei2/2006 ainda por corresponder;conversão apenas cenário condicionado,sem base fiscal confirmada.';
|
||||
export const currencyEventTreatments=[
|
||||
{id:'foreign_scenario',branch:'P104',label:'Moeda estrangeira — cenário condicionado'},
|
||||
{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={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);
|
||||
return stop(`P104:cenário condicionado=${result.scenarioAmount}MZN,origem${row.sourceCurrency},taxa${row.fxRate},${row.fxDirection},data${row.fxDate}. Regra fiscal15(8)/Lei2/2006 ainda por corresponder;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)=>['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'};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.']];}}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { currencyEventReportRows } from './iva-currency-events.ts';
|
||||
import { propertyTransferReportRows } from './iva-property-transfers.ts';
|
||||
import { goodsPresumptionReportRows } from './iva-goods-presumptions.ts';
|
||||
import { displayValue } from './display-value.ts';
|
||||
@@ -656,6 +657,7 @@ export function buildProjectDossierRows(
|
||||
rows.push(...secondhandReportRows(draft));
|
||||
rows.push(...goodsPresumptionReportRows(draft));
|
||||
rows.push(...propertyTransferReportRows(draft));
|
||||
rows.push(...currencyEventReportRows(draft));
|
||||
rows.push(...offerPreparationReportRows(draft));
|
||||
const operationRows = salesReportRows(draft);
|
||||
rows.push(...operationRows);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class IvaCurrencyEventDraftTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function test_currencyevents_recover_privately_preserving_all_delivered_books(): void
|
||||
{
|
||||
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
|
||||
$this->assertSame('simulator_fiscal_delta_20261021_test', $target->name);
|
||||
$this->assertSame(55441, (int) $target->port);
|
||||
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
|
||||
$this->actingAs(User::factory()->create(['email_verified_at' => now()]));
|
||||
$draft = json_decode(file_get_contents(base_path('tests/frontend/fixtures/iva-currency-events.json')), true, flags: JSON_THROW_ON_ERROR);
|
||||
$id = $this->postJson('/api/v1/simulation-drafts', ['personal_snapshot' => [], 'economic_snapshot' => $draft], ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
|
||||
$recovered = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk()->json('data.economic_snapshot');
|
||||
$this->assertSame($draft['modelo_a_snapshot'], $recovered['modelo_a_snapshot']);
|
||||
$this->assertSame('opportunity:357', $recovered['context_opportunity']);
|
||||
$this->assertSame('Manica', $recovered['selection_labels']['province_id']);
|
||||
$store = json_decode($recovered['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
|
||||
$book = $store['currencyEvents']['opportunity:357'];
|
||||
$this->assertCount(15, $book['rows']);
|
||||
$this->assertSame('conditional',$book['result']['rows'][0]['status']);
|
||||
$this->assertSame('64500.00',$book['result']['rows'][0]['scenarioAmount']);
|
||||
$this->assertSame('2026-12-10',$book['result']['rows'][13]['eventDate']);
|
||||
$this->assertSame(['200.00','800.00'],array_column($book['result']['rows'][14]['dueEvents'],'amount'));
|
||||
foreach ($book['result']['rows'] as $row) {
|
||||
$this->assertNull($row['baseAmount']);$this->assertNull($row['vat']);$this->assertNull($row['rate']);
|
||||
}
|
||||
$this->assertSame($book['rows'], $book['result']['inputs']);
|
||||
$this->assertCount(1, $book['history']);
|
||||
$this->assertSame('64500.00',$book['history'][0]['rows'][0]['scenarioAmount']);
|
||||
$frozen = json_decode(file_get_contents(base_path('tests/frontend/fixtures/iva-currency-events-preserved.json')), true, flags: JSON_THROW_ON_ERROR);
|
||||
$prior = json_decode($frozen['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
|
||||
$this->assertSame($prior, array_diff_key($store, ['currencyEvents' => true]));
|
||||
$this->assertSame('1250.30', $store['publicAuthorityBases']['opportunity:339']['result']['rows'][1]['baseAmount']);
|
||||
$this->assertNull($store['publicAuthorityBases']['opportunity:339']['result']['rows'][1]['vat']);
|
||||
$this->assertSame('400.00', $store['importExemptions']['opportunity:338']['result']['rows'][1]['importVat']);
|
||||
$this->assertNull($store['books']['opportunity:329']['result']['periods'][0]['fields']['11']);
|
||||
file_put_contents(base_path('outputs/iva-l1-currency-events-20261025/recovered.json'), json_encode($recovered, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,40 @@
|
||||
import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';
|
||||
import {newCurrencyEvent,currencyEventTreatments,evaluateCurrencyEvent,recordCurrencyEvents,saveCurrencyEventBook,currencyEventBook,currencyEventCurrent,currencyEventReportRows,switchCurrencyEvent} from '../../resources/js/lib/iva-currency-events.ts';
|
||||
const frozen=JSON.parse(fs.readFileSync('tests/frontend/fixtures/iva-currency-events-preserved.json','utf8'));
|
||||
const draft={...frozen,currency:'MZN',context_opportunity:'opportunity:357',project_title:'SYNTHETIC QA — Moeda e eventos / Manica'};
|
||||
function row(t){return{...newCurrencyEvent(t,'MZN'),treatment:t,description:'SYNTHETIC QA '+t,period:'2026-12',origin:'projected',operationRef:'SYNTHETIC QA operação existente',documentRef:'SYNTHETIC QA documento/pressuposto',documentDate:'2026-12-12',scope:'SYNTHETIC QA conjunto/parte/operação/período',evidence:'SYNTHETIC QA pressupostos expressos',eventDate:'2026-12-10',eventRef:'SYNTHETIC QA evento específico/artigo7,excepções próprias conferidas',otherDate:t==='consignment_deadline'?'2026-06-13':t==='contract_effects'?'2026-12-01':'2026-12-12',reviewDate:'2026-12-31',factsConfirmed:'yes',invoiceRequired:'yes',invoiceState:'issued',invoiceDate:'2026-12-12',invoiceRef:'SYNTHETIC QA factura',legalDeadline:'2026-12-17',deadlineRef:'SYNTHETIC QA prazo/disposição/calendário identificados,não regra vigente presumida',paymentState:t==='advance_payment'?'received':'none',paymentDate:'2026-12-09',paymentRef:'SYNTHETIC QA recebimento identificado',amount:'1000.00',paymentAmount:'200.00',sourceCurrency:'USD',fxRate:'64.5',fxDate:'2026-12-10',fxSource:'SYNTHETIC QA taxa/exemplar',fxRuleRef:'SYNTHETIC QA pressuposto da taxa/data,não base fiscal',fxDirection:'mzn_per_unit'};}
|
||||
for(const rule of currencyEventTreatments)test(rule.branch+' '+rule.id+' own event and noVAT/base',()=>{
|
||||
const r=row(rule.id),v=evaluateCurrencyEvent(r);assert.equal(v.status,rule.branch==='P104'?'conditional':'determined');assert.equal(v.baseAmount,null);assert.equal(v.vat,null);assert.equal(v.rate,null);
|
||||
if(rule.branch==='P104')assert.equal(v.scenarioAmount,'64500.00');else{assert.equal(v.dueEvents.at(-1).date,['import_clearance','import_auction'].includes(rule.id)?'2026-12-10':'2026-12-12');assert.equal(v.eventDate,rule.id==='advance_payment'?null:rule.id==='consignment_deadline'?'2026-12-10':'2026-12-10');}
|
||||
for(const f of ['operationRef','documentRef','scope','evidence'])assert.equal(evaluateCurrencyEvent({...r,[f]:''}).status,'indeterminate');assert.equal(evaluateCurrencyEvent({...r,factsConfirmed:''}).status,'indeterminate');assert.equal(evaluateCurrencyEvent({...r,factsConfirmed:'no'}).status,'not_applicable');
|
||||
});
|
||||
test('P131/P132 invoice intime/late/absent,partial payment and no duplicate remainder',()=>{
|
||||
const r=row('goods_available');let v=evaluateCurrencyEvent({...r,paymentState:'received'});assert.deepEqual(v.dueEvents.map(x=>[x.date,x.amount]),[['2026-12-09','200.00'],['2026-12-12','800.00']]);
|
||||
v=evaluateCurrencyEvent({...r,invoiceDate:'2026-12-20'});assert.equal(v.dueEvents[0].date,'2026-12-17');
|
||||
v=evaluateCurrencyEvent({...r,invoiceState:'not_issued'});assert.equal(v.dueEvents[0].date,'2026-12-17');
|
||||
v=evaluateCurrencyEvent({...r,invoiceState:'not_issued',reviewDate:'2026-12-11',paymentState:'received'});assert.equal(v.dueEvents.length,1);assert.equal(v.remainingPending,true);
|
||||
v=evaluateCurrencyEvent({...r,paymentState:'received',paymentDate:'2026-12-19',invoiceDate:'2026-12-20'});assert.deepEqual(v.dueEvents.map(x=>[x.date,x.amount]),[['2026-12-17','1000.00']]);
|
||||
v=evaluateCurrencyEvent({...r,paymentState:'received',paymentAmount:'1000'});assert.equal(v.dueEvents.length,1);assert.equal(v.dueEvents[0].amount,'1000.00');
|
||||
for(const patch of [{paymentState:''},{legalDeadline:''},{deadlineRef:''},{invoiceState:''},{paymentState:'received',paymentAmount:'1001'},{paymentState:'received',amount:''}])assert.equal(evaluateCurrencyEvent({...r,...patch}).status,'indeterminate');
|
||||
assert.equal(evaluateCurrencyEvent({...r,invoiceRequired:'no',amount:''}).dueEvents[0].amount,null);
|
||||
assert.equal(evaluateCurrencyEvent({...r,eventDate:'2026-12-20',reviewDate:'2026-12-11',invoiceState:'not_issued',legalDeadline:'2026-12-28',paymentState:'received'}).dueEvents[0].date,'2026-12-09');
|
||||
});
|
||||
test('own events,consignmentterm,servicecharge,contractexceptions and dates',()=>{
|
||||
assert.equal(evaluateCurrencyEvent({...row('consignment_deadline'),otherDate:''}).status,'indeterminate');
|
||||
assert.equal(evaluateCurrencyEvent({...row('consignment_deadline'),invoiceRequired:'no'}).dueEvents[0].date,'2026-12-10');
|
||||
assert.equal(evaluateCurrencyEvent({...row('service_early_charge'),otherDate:'2026-12-09'}).status,'indeterminate');
|
||||
assert.equal(evaluateCurrencyEvent({...row('contract_effects'),otherDate:'2026-12-11'}).status,'indeterminate');
|
||||
for(const field of ['reviewDate','eventDate','documentDate'])assert.equal(evaluateCurrencyEvent({...row('goods_available'),[field]:'2026-02-30'}).status,'indeterminate');
|
||||
const r=row('own_goods'),sw=switchCurrencyEvent(r,'foreign_scenario');assert.equal(sw.eventDate,r.eventDate);assert.equal(sw.fxSource,r.fxSource);assert.equal(sw.factsConfirmed,'');
|
||||
});
|
||||
test('P104 conditionalconversion direction/exactrounding/missingparams never fiscalbase',()=>{
|
||||
const r=row('foreign_scenario');assert.equal(evaluateCurrencyEvent({...r,fxDirection:'units_per_mzn',fxRate:'2',amount:'100'}).scenarioAmount,'50.00');
|
||||
assert.equal(evaluateCurrencyEvent({...r,amount:'0.000001',fxRate:'1.5'}).scenarioAmount,'0.000002');
|
||||
for(const patch of [{fxRate:'0'},{fxRate:'-1'},{fxSource:''},{fxDate:''},{fxRuleRef:''},{fxDirection:''},{sourceCurrency:'MZN'}])assert.equal(evaluateCurrencyEvent({...r,...patch}).status,'indeterminate');assert.equal(evaluateCurrencyEvent(r).baseAmount,null);
|
||||
});
|
||||
test('snapshot/history/report preserve every delivered book and contextual scope',()=>{
|
||||
const rows=currencyEventTreatments.map(t=>row(t.id)),a=recordCurrencyEvents(draft,{rows}),next={...draft,modelo_a_snapshot:saveCurrencyEventBook(draft,a)},b=recordCurrencyEvents(next,a);next.modelo_a_snapshot=saveCurrencyEventBook(next,b);
|
||||
assert.equal(b.history.length,1);assert.equal(currencyEventCurrent(next,b),true);assert.equal(currencyEventCurrent(next,{...b,rows:rows.slice(1)}),false);assert.equal(currencyEventReportRows(next).length,15);assert.deepEqual(currencyEventBook({...next,context_opportunity:'opportunity:358'}),{rows:[]});
|
||||
const store=JSON.parse(next.modelo_a_snapshot);delete store.currencyEvents;assert.deepEqual(store,JSON.parse(frozen.modelo_a_snapshot));assert.throws(()=>recordCurrencyEvents(next,{rows:[rows[0],rows[0]]}));assert.throws(()=>currencyEventBook({...next,modelo_a_snapshot:JSON.stringify({...store,currencyEvents:[]})}));
|
||||
if(process.env.IVA_CURRENCY_EVENTS_FIXTURE)fs.writeFileSync(process.env.IVA_CURRENCY_EVENTS_FIXTURE,JSON.stringify(next,null,2)+'\n');
|
||||
});
|
||||
Reference in New Issue
Block a user