feat(iva): add travel period margin and special travel suspensive events

This commit is contained in:
2026-10-25 01:42:05 +02:00
parent 91c4063338
commit 7986d02405
10 changed files with 415 additions and 0 deletions
@@ -1,4 +1,6 @@
import { SimulationDigital } from './simulation-digital';
import { SimulationTravel } from './simulation-travel';
import { SimulationTravelEvents } from './simulation-travel-events';
import { SimulationForeignOperations } from './simulation-foreign-operations';
import { SimulationCurrencyEvents } from './simulation-currency-events';
import { SimulationPropertyTransfers } from './simulation-property-transfers';
@@ -128,6 +130,8 @@ export function SimulationMonthlyIva({draft, busy, onCalculate, onChange}: {
<SimulationGoodsAllocation draft={draft} onChange={onChange} />
<SimulationForeignOperations draft={draft} onChange={onChange} />
<SimulationDigital draft={draft} onChange={onChange} />
<SimulationTravel draft={draft} onChange={onChange} />
<SimulationTravelEvents draft={draft} onChange={onChange} />
<SimulationCurrencyEvents draft={draft} onChange={onChange} />
<SimulationPropertyTransfers draft={draft} onChange={onChange} />
<SimulationGoodsPresumptions draft={draft} onChange={onChange} />
@@ -0,0 +1,44 @@
import {useState} from 'react';
import {travelEventBook,travelEventCurrent,travelEventReportRows,travelEventSource,travelEventSources,newTravelEvent,recordTravelEvents,saveTravelEventBook,switchTravelEvent,type TravelEventBook,type TravelEventRow} from '@/lib/iva-travel-events';
export function SimulationTravelEvents({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}){
const[error,setError]=useState('');let book:TravelEventBook;try{book=travelEventBook(draft);}catch(e){return<p role="alert">{String(e)}</p>;}
const save=(next:TravelEventBook)=>{try{onChange('modelo_a_snapshot',saveTravelEventBook(draft,next));setError('');}catch(e){setError(String(e));}};
const change=(id:string,patch:Partial<TravelEventRow>)=>save({...book,rows:book.rows.map(r=>r.id===id?{...r,...patch}:r)});
const fields=(row:TravelEventRow,list:ReadonlyArray<readonly[keyof TravelEventRow,string]>)=>list.map(([key,label])=><label className="block" key={key}>{label}<input type={key==='period'?'month':String(key).endsWith('Date')||key==='legalDeadline'?'date':'text'} value={row[key]} onChange={e=>change(row.id,{[key]:e.target.value})}/></label>);
const select=(row:TravelEventRow,key:keyof TravelEventRow,label:string,options:ReadonlyArray<readonly[string,string]>)=><label>{label}<select value={row[key]} onChange={e=>change(row.id,{[key]:e.target.value})}><option value="">Por esclarecer</option>{options.map(([value,text])=><option key={value} value={value}>{text}</option>)}</select></label>;
const yn=[['yes','Sim'],['no','Não']] as const;
let sources:ReturnType<typeof travelEventSources>=[];try{sources=travelEventSources(draft);}catch{/* Origin reader preserves originals. */}
return<details className="rounded border p-3"><summary>Eventos de viagens e cessação de regimes suspensivos</summary><p>Conserve separados facto gerador e exigibilidade. Não inferir pagamento integral por um sinal, início pela emissão do bilhete ou cessação do regime por mudança de armazém. Estes eventos não calculam imposto nem concedem benefício.</p><p>{travelEventSource}</p>{error&&<p role="alert">{error}</p>}
<button type="button" onClick={()=>save({...book,rows:[...book.rows,newTravelEvent(crypto.randomUUID(),String(draft.currency??''))]})}>Acrescentar evento especial</button>
{book.rows.map(row=>{const source=sources.find(s=>s.id===row.travelRowId);return<details className="rounded border p-3" key={row.id}><summary>{row.description||'Evento por identificar'} — {row.period||'período por indicar'}</summary>
<label>Percurso do evento especial<select value={row.treatment} onChange={e=>change(row.id,switchTravelEvent(row,e.target.value))}><option value="">Por esclarecer</option><option value="travel_special">Viagens — pagamento integral/início7(9)</option><option value="suspensive_exit">Cessação de regime/procedimento7(8)</option></select></label>
{fields(row,[['description','Evento ou operação identificada'],['operationRef','Referência da operação existente'],['period','Mês de contexto do evento'],['documentRef','Documento disponível / referência da previsão'],['documentDate','Data do documento / prevista'],['reviewDate','Data até à qual os eventos são conferidos'],['scope','Âmbito e parcela efectivamente abrangidos'],['evidence','Factos ou pressupostos expressos']])}
{select(row,'origin','Natureza dos dados',[['projected','Projectados / pressupostos'],['effective','Efectivos declarados']])}
{select(row,'factsConfirmed','Factos correspondem ao percurso e âmbito identificados',yn)}
{row.treatment==='suspensive_exit'&&<>
{select(row,'regimeClause','Regime/procedimento identificado no artigo14(1)',[['a','a — Importação/colocação em regimes ou depósitos indicados'],['b','b — Expedição/transporte para as zonas ou depósitos e conexos'],['c','c — Transmissão e serviços conexos sob o regime'],['d','d — Trânsito, transbordo, draubaque ou importação temporária e conexos']])}
{fields(row,[['entryDate','Data de colocação sob o regime/procedimento'],['entryRef','Referência do regime, bens e operação de entrada']])}
{select(row,'regimeStatus','Situação do regime na conferência',[['still','Continua abrangido pelo regime'],['ceased','Cessou efectivamente o regime']])}
{row.regimeStatus==='ceased'&&<>{fields(row,[['exitDate','Data da cessação efectiva'],['exitRef','Referência e âmbito da cessação']])}{select(row,'cessationConfirmed','Confirmada cessação fiscal, distinguida de transferência entre regimes ou mudança física',yn)}</>}
<p>A referência demonstra o facto e o âmbito. Não se exige certificado ou autorização inventados; não há desembaraço, valor aduaneiro, direitos ou ICE automáticos.</p>
</>}
{row.treatment==='travel_special'&&<>
<label>Operação de viagens já classificada nesta oportunidade<select value={row.travelRowId} onChange={e=>change(row.id,{travelRowId:e.target.value})}><option value="">Escolher operação</option>{sources.map(s=><option key={s.id} value={s.id}>{s.description} — {s.period} — {s.classification.regime}{s.current?'':' — histórico'}</option>)}</select></label>
{source&&<><p>Origem: {source.operationRef}; {source.scope}. Período do registo: {source.period}. {source.classification.reason}</p><button type="button" onClick={()=>change(row.id,{operationRef:row.operationRef||source.operationRef,description:row.description||source.description,documentRef:row.documentRef||source.input.documentRef,documentDate:row.documentDate||source.input.documentDate,currency:row.currency||source.input.currency,scope:row.scope||source.scope})}>Preencher apenas referências em falta a partir da origem</button><p>Referências já preenchidas são conservadas; divergências precisam de conferência. O período do evento pode diferir do período da margem.</p></>}
{select(row,'fullPaymentState','Estado do pagamento da contraprestação',[['full','Pagamento integral confirmado'],['partial','Apenas pagamento parcial'],['none','Sem pagamentos']])}
{row.fullPaymentState==='full'&&fields(row,[['fullPaymentDate','Data do pagamento integral'],['fullPaymentRef','Referência da liquidação integral / conjunto de recebimentos']])}
{select(row,'startState','Início da viagem/alojamento e primeiro serviço',[['started','Primeiro serviço/início ocorrido no cenário conferido'],['not_started','Ainda não iniciado']])}
{row.startState==='started'&&fields(row,[['startDate','Data do primeiro serviço ao cliente'],['startRef','Referência do primeiro serviço e âmbito']])}
<p>«Imediatamente antes» identifica o momento; não se subtrai um dia à data. Pagamento e início na mesma data não demonstram ordem intradiária.</p>
{fields(row,[['amount','Contraprestação total da parcela analisada'],['currency','Moeda da contraprestação']])}
{select(row,'invoiceRequired','Operação com obrigação de emitir factura25',yn)}
{row.invoiceRequired==='yes'&&<>{fields(row,[['legalDeadline','Termo do prazo legal de emissão aplicável'],['deadlineRef','Disposição, calendário e âmbito que fundamentam o prazo']])}{select(row,'invoiceState','Factura até à conferência',[['issued','Emitida'],['not_issued','Não emitida']])}{row.invoiceState==='issued'&&fields(row,[['invoiceDate','Data de emissão da factura'],['invoiceRef','Referência da factura']])}</>}
{select(row,'paymentState','Recebimento ou parcela anterior à liquidação integral',[['none','Sem recebimento'],['received','Recebimento identificado']])}
{row.paymentState==='received'&&fields(row,[['paymentDate','Data do recebimento identificado'],['paymentAmount','Montante recebido nessa parcela'],['paymentRef','Referência e âmbito da parcela recebida']])}
<p>Se existe sinal anterior e posterior liquidação integral, identifique ambos: a parcela e o saldo não são somados duas vezes. A menção «IVA incluído» não é factura emitida por este simulador.</p>
</>}
<p>Mudar percurso conserva campos ocultos e referências; reveja as confirmações próprias.</p></details>;})}
{!!book.rows.length&&<button type="button" onClick={()=>{try{save(recordTravelEvents(draft,book));}catch(e){setError(String(e));}}}>Conferir eventos especiais e guardar versão</button>}
{book.result&&<><p role="status">{travelEventCurrent(draft,book)?'Resultados correspondem às entradas e origens guardadas.':'Entradas/origens alteradas; resultado histórico conservado. Reveja o delta.'} {book.history?.length??0} versões anteriores. Sem base/IVA/dedução calculados.</p><dl>{travelEventReportRows(draft).map(([,label,value,origin],i)=><div key={i}><dt>{label}</dt><dd>{value}<small className="block">{origin}</small></dd></div>)}</dl></>}
</details>;
}
@@ -0,0 +1,37 @@
import {useState} from 'react';
import {travelBook,travelConditions,travelCostKinds,travelCurrent,travelReportRows,travelSource,newTravel,newTravelCost,newTravelPeriod,recordTravel,saveTravelBook,switchTravelResources,type TravelBook,type TravelRow,type TravelPeriod,type TravelCost} from '@/lib/iva-travel';
export function SimulationTravel({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}){
const[error,setError]=useState('');let book:TravelBook;try{book=travelBook(draft);}catch(e){return<p role="alert">{String(e)}</p>;}
const save=(next:TravelBook)=>{try{onChange('modelo_a_snapshot',saveTravelBook(draft,next));setError('');}catch(e){setError(String(e));}};
const change=(id:string,patch:Partial<TravelRow>)=>save({...book,rows:book.rows.map(r=>r.id===id?{...r,...patch}:r)});
const period=(id:string,patch:Partial<TravelPeriod>)=>save({...book,periods:book.periods.map(p=>p.id===id?{...p,...patch}:p)});
const cost=(row:TravelRow,id:string,patch:Partial<TravelCost>)=>change(row.id,{costs:row.costs.map(c=>c.id===id?{...c,...patch}:c)});
return<details className="space-y-3 rounded border p-3"><summary>Agências de viagens — pacote, meios utilizados e margem do período</summary>
<p>Preço único, actuação em nome próprio e meios de terceiros determinam o percurso específico. Meios próprios, intermediação e revenda simples conservam o regime geral aplicável. Reutilize documentos e referências; não é necessário criar certificados ou aprovadores.</p><p>A margem é global por operador e período do registo simulado, com âmbito completo declarado. Não somar margem por pacote nem misturar dados efectivos e projectados. A base não é IVA devido nem dedução. Facturação oficial, regularização e apuramento integral continuam fora deste cálculo.</p><p>{travelSource}</p>{error&&<p role="alert">{error}</p>}
<button type="button" onClick={()=>save({...book,rows:[...book.rows,newTravel(crypto.randomUUID(),String(draft.currency??''))]})}>Acrescentar operação de viagem</button>
{book.rows.map(row=><details key={row.id} className="rounded border p-3"><summary>{row.description||'Viagem por identificar'} — {row.period||'período do registo por indicar'}</summary><div className="grid gap-3 md:grid-cols-2">
{([['description','Viagem ou conjunto de serviços'],['operatorRef','Operador: referência da agência/organizador'],['operationRef','Referência da operação / pressuposto'],['period','Período do registo de contraprestação e custos'],['documentRef','Documento existente / referência da previsão'],['documentDate','Data do documento / prevista'],['scope','Âmbito: pacote, parcela, período e condições'],['evidence','Factos / pressupostos expressos'],['agencyRef','Sede/estabelecimento a partir do qual presta e referência'],['currency','Moeda original do registo']] as const).map(([field,label])=><label key={field}>{label}<input type={field==='period'?'month':field==='documentDate'?'date':'text'} value={row[field]} onChange={e=>change(row.id,{[field]:e.target.value})}/></label>)}
<label>Natureza dos dados<select value={row.origin} onChange={e=>change(row.id,{origin:e.target.value as TravelRow['origin']})}><option value="projected">Projectados / pressupostos</option><option value="effective">Efectivos declarados</option></select></label>
<label>País do estabelecimento prestador<select value={row.agencyCountry} onChange={e=>change(row.id,{agencyCountry:e.target.value})}><option value="">Por esclarecer</option><option value="MZ">Moçambique</option><option value="foreign">Estrangeiro</option></select></label>
<label>Meios utilizados na viagem<select value={row.resources} onChange={e=>change(row.id,switchTravelResources(row,e.target.value))}><option value="">Por esclarecer</option><option value="third_party">Apenas meios de terceiros</option><option value="own">Apenas meios próprios</option><option value="mixed">Meios próprios e de terceiros</option></select></label>
<label>Revenda retalhista de viagem organizada por grossista<select value={row.retailer} onChange={e=>change(row.id,{retailer:e.target.value as TravelRow['retailer']})}><option value="">Por esclarecer</option><option value="yes">Sim</option><option value="no">Não</option></select></label>
{row.retailer==='yes'&&<><label>Acrescenta serviços não meramente acessórios/complementares e apresenta novo conjunto em nome próprio?<select value={row.newPackage} onChange={e=>change(row.id,{newPackage:e.target.value as TravelRow['newPackage']})}><option value="">Por esclarecer</option><option value="yes">Sim</option><option value="no">Não</option></select></label><label>Referência do novo conjunto e serviços acrescidos<input value={row.packageRef} onChange={e=>change(row.id,{packageRef:e.target.value})}/></label></>}
</div><fieldset><legend>Factos do percurso</legend>{travelConditions.map(([key,label])=><label className="block" key={key}>{label}<select value={row.conditions[key]??''} onChange={e=>change(row.id,{conditions:{...row.conditions,[key]:e.target.value as ''|'yes'|'no'}})}><option value="">Por esclarecer</option><option value="yes">Sim</option><option value="no">Não</option></select></label>)}</fieldset>
{['third_party','mixed'].includes(row.resources)&&<><label>Parcela especial identificada como tributável no âmbito nacional<select value={row.domesticTaxable} onChange={e=>change(row.id,{domesticTaxable:e.target.value as TravelRow['domesticTaxable']})}><option value="">Por esclarecer</option><option value="yes">Sim, segundo fundamento indicado</option><option value="no">Não</option></select></label><label>Fundamento do enquadramento tributável<input value={row.taxRef} onChange={e=>change(row.id,{taxRef:e.target.value})}/></label>
<label>Contraprestação registada no período, com IVA incluído<input inputMode="decimal" value={row.grossPrice} onChange={e=>change(row.id,{grossPrice:e.target.value})}/></label>
{row.resources==='mixed'&&<><label>Contraprestação da parte que utiliza meios de terceiros, com IVA<input inputMode="decimal" value={row.thirdPartyGross} onChange={e=>change(row.id,{thirdPartyGross:e.target.value})}/></label><label>Fundamento da repartição entre meios próprios e terceiros<input value={row.allocationRef} onChange={e=>change(row.id,{allocationRef:e.target.value})}/></label></>}
<p>Custos registados neste mesmo período; indicar montantes com IVA incluído. A lista vazia só representa ausência declarada de custos no âmbito completo. Publicidade, câmbio, comunicações da agência e comissões de venda não reduzem esta margem41(3); a sua eventual dedução não é calculada aqui.</p>
<button type="button" onClick={()=>change(row.id,{costs:[...row.costs,newTravelCost(crypto.randomUUID())]})}>Acrescentar custo da viagem</button>
{row.costs.map(c=><fieldset key={c.id}><legend>{c.description||'Custo por identificar'}</legend><label>Natureza do custo<select value={c.kind} onChange={e=>cost(row,c.id,{kind:e.target.value})}><option value="">Por esclarecer</option>{travelCostKinds.map(([id,label])=><option key={id} value={id}>{label}</option>)}</select></label>{([['description','Custo identificado'],['amount','Montante com IVA incluído'],['ref','Referência existente do custo / pressuposto']] as const).map(([field,label])=><label key={field}>{label}<input value={c[field]} onChange={e=>cost(row,c.id,{[field]:e.target.value})}/></label>)}
{['direct','other'].includes(c.kind)&&([['thirdParty','Fornecimento por terceiro'],['directBenefit','Benefício directo do cliente durante a viagem']] as const).map(([field,label])=><label key={field}>{label}<select value={c[field]} onChange={e=>cost(row,c.id,{[field]:e.target.value})}><option value="">Por esclarecer</option><option value="yes">Sim</option><option value="no">Não</option></select></label>)}</fieldset>)}
</>}
<p>Alterar meios ou percurso conserva referências e custos ocultos; reveja as confirmações próprias. Alterações de custos ou preço ficam em nova versão referenciada, sem regularização automática.</p></details>)}
<h4>Registo global por operador/período</h4><button type="button" onClick={()=>save({...book,periods:[...book.periods,newTravelPeriod(crypto.randomUUID())]})}>Acrescentar período de margem</button>
{book.periods.map(p=><fieldset key={p.id}><legend>{p.period||'Período por indicar'} — {p.operatorRef||'operador por indicar'}</legend>{([['period','Período do registo global'],['operatorRef','Mesma referência do operador nas operações'],['scope','Âmbito completo do registo simulado'],['registerRef','Referência do registo ou pressuposto de completude']] as const).map(([field,label])=><label key={field}>{label}<input type={field==='period'?'month':'text'} value={p[field]} onChange={e=>period(p.id,{[field]:e.target.value})}/></label>)}<label>Contraprestações e custos completos neste âmbito/período<select value={p.complete} onChange={e=>period(p.id,{complete:e.target.value as TravelPeriod['complete']})}><option value="">Por esclarecer</option><option value="yes">Sim</option><option value="no">Não, registo parcial</option></select></label>
<label>Excesso negativo do período anterior<select value={p.priorMode} onChange={e=>period(p.id,{priorMode:e.target.value})}><option value="">Por esclarecer</option><option value="none">Inexistência expressamente indicada</option><option value="manual">Montante anterior com referência</option><option value="previous">Reutilizar período imediatamente anterior calculado do mesmo operador</option></select></label>{p.priorMode==='manual'&&<><label>Excesso negativo anterior<input value={p.priorAmount} onChange={e=>period(p.id,{priorAmount:e.target.value})}/></label><label>Referência do registo anterior / alteração fundamentada<input value={p.priorRef} onChange={e=>period(p.id,{priorRef:e.target.value})}/></label></>}
{p.priorMode!=='previous'&&<label>Se o excesso indicado divergir do anterior calculado, confirmar a correcção referenciada após conferir o impacto na base e reporte<select value={p.priorOverride} onChange={e=>period(p.id,{priorOverride:e.target.value as TravelPeriod['priorOverride']})}><option value="">Conferir divergência se existir</option><option value="yes">Confirmo usar o valor indicado e a referência da correcção</option><option value="no">Conservar/reutilizar o anterior</option></select></label>}
</fieldset>)}
{!!book.rows.length&&<button type="button" onClick={()=>{try{save(recordTravel(draft,book));}catch(e){setError(String(e));}}}>Classificar viagens e apurar margem do período</button>}
{book.result&&<><p role="status">{travelCurrent(draft,book)?'Resultados correspondem ao registo guardado.':'Entradas alteradas; resultado histórico conservado. Reveja o delta.'} {book.history?.length??0} versões anteriores. Sem IVA devido ou dedução calculados.</p><dl>{travelReportRows(draft).map(([,label,value,origin],i)=><div key={i}><dt>{label}</dt><dd>{value}<small className="block">{origin}</small></dd></div>)}</dl></>}
</details>;
}