feat(iva): classify P050 services included in import taxable base

This commit is contained in:
2026-10-24 20:39:23 +02:00
parent 77d1290258
commit f9fb04f947
8 changed files with 126 additions and 18 deletions
@@ -1,6 +1,16 @@
# Rastreabilidade, utilização e formação
## 24/10/2026 — preparação G6F/P050 e próxima entrega parcial946fc089
TempoaceitouG6E/P052c1314cdf/536559d6;**75entregues+57funcionaispendentes+P001=133**. Autorizou946fc089na próximaentrega como preparação/simulação condicionadaP084/P085,coberturaparcial,nãobenefíciosconfirmadosnemfecho. Nãoinstaladaautomaticamente nesta passagemQA;incluirâmbitototalexpresso nomanifestoseguinte. PesquisaP084/P085delimitadaàligação/procedimentoregulamentar14(3),âmbitosdistintos,semrepetircotejoModelI.1/CBFjáaceite neminventarcertificados/aprovadores. Rascunhos/âmbitos/cenários/históricopreservados,nãobloqueiamramosindependentes.
Inventárioantesdostestes:75fechos;27livrosfiscais+1livroderascunhosP084/P085 comestados/decisões,âmbitos,edições/cenários eversões congelados,fixtureiva-offer-paths-inputs.json;semperdasconfirmadas. Provasaceitescódigo/schema/dados/configuração/permissõesdestinoreutilizadas,174tabelas/369ficheiros/29fichas/28relações/408geometrias;semrecalcularanterioresouescrevernoactivo.
**G6F/P050**:CIVA12(1)(e),Lei13/2016PDF11/p1426(269);valor do serviço incluídona basetributável daimportaçãodebens,conforme16. Art16(1)valoraduaneiro eacréscimosna medidaquenãoincluídos;16(1)(b)despesasacessórias(embalagem/transporte/seguros/outrosencargos)atéprimeirodestinonointeriordoPaís;16(2)destinododocumentodetransporteou,na suafalta,primeirarupturadecarganointeriordoPaís. Página14/p1426(272)conferida integralmente,nãoaplica17%históricoimpressonessapáginacomo taxapresente. Distinguir valorjácompreendidonovaloraduaneiro eparcelaacessória adicionadaconforme16(1)(b)/2;duasperfisdeumramo,sem duplicarserviço/bemou presumir inclusão porcategoriasectorial. Nãosomavalorcalculadoàbaseaduaneira,nãocalculaIVA dosbensnemdireitos,nãopreenchecampo11;resultadoexpressamenteIVA do serviço. Informações/desconhecidosnãozeramimposto;projecçõescomfundamentoexplícito,semcertificaçãoATinventada. Fontesaceites/inventáriosoriginais/hashesreutilizados,semsegundavalidação. L1aberto;P001só05/06;bebidasL2/L3;Estatísticas/IAadiadas.
## 24/10/2026 — percursos distintos P084/P085 e cenários demonstradosQA
Candidata`946fc089`,[registo](../simulations/iva-l1-offer-paths-qa-20261024.json). OrientaçãoexpressaTempo incorporada nosrascunhos existentes,novoscampos opcionais/retrocompatíveis. P084:estadosdeclarados preparação/apresentado/deferido/indeferido,referências/documentos/datas disponíveis e registosversionadosdeâmbito(bens,operação,período,condições). Guardar rascunho incompleto continua permitido;pararegistardecisão conserva os4elementosdeâmbitopedidosTempo. Datasexactas,meses outextoliteraldoâmbito(porexemploatérevogação/semtermo,operaçõesconjuntas) são conservados,sem inventarfimoualargarconcessãoaoutrosbens/períodos/oportunidades. Documento/datadisponíveis,nãocertificado/aprovadornovo. TodososregistosanterioresaparecemnoUI/relatório;ediçõesnopididoactual nãosobrescrevemdecisãoanterior. Registar estado nãoapresentapedidoàautoridade nemconcedeisençãopelosimulador.
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
import { useState } from 'react';
import { importExemptionBook,importExemptionCurrent,importExemptionReportRows,importExemptionRules,importRuleSource,newImportExemption,recordImportExemptions,saveImportExemptionBook,type ImportExemptionBook,type ImportExemptionRow } from '@/lib/iva-import-exemptions';
import { importExemptionBook,importExemptionCurrent,importExemptionReportRows,importExemptionRules,importRuleSource,isImportService,newImportExemption,recordImportExemptions,saveImportExemptionBook,type ImportExemptionBook,type ImportExemptionRow } from '@/lib/iva-import-exemptions';
export function SimulationImportExemptions({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}) {
const [error,setError]=useState('');
@@ -8,25 +8,28 @@ export function SimulationImportExemptions({draft,onChange}:{draft:Record<string
const save=(next:ImportExemptionBook)=>{try {onChange('modelo_a_snapshot',saveImportExemptionBook(draft,next));setError('');}catch(cause){setError(String(cause));}};
const change=(id:string,patch:Partial<ImportExemptionRow>)=>save({...book,rows:book.rows.map(row=>row.id===id?{...row,...patch}:row)});
return <details className="space-y-3 rounded border p-3">
<summary>IVA na importação — enquadramentos disponíveis</summary>
<summary>IVA na importação e serviços associados — enquadramentos disponíveis</summary>
<p>Oportunidade: {String(draft.context_opportunity||'entrada geral')}. Indique apenas importações relevantes: reimportação no mesmo estado, ouro pelo Banco de Moçambique, objectos de arte, veículo de combate a incêndios, embarcação/avião elegível ou objecto da sua exploração, bens em regime aduaneiro com isenção total de direitos, importação sob controlo alfandegário em zona/depósito/loja franca nas condições próprias, ou bens de abastecimento a bordo de embarcação/avião de navegação internacional. Escolha o ramo para apresentar as condições aplicáveis. Cobertura2026/MZN; as compras internas e os seus resultados conservam-se.</p>
<p>O valor declarado/previsional é conservado, sem o fixar como base aduaneira. A classificação não preenche o campo11 de dedução nem os totais do ModeloA. Uma condição não cumprida afasta só esta isenção; o imposto normal fica por enquadrar. Nas projecções, as respostas são pressupostos, sem certificação aduaneira.</p>
<p>Nos serviços associados, identifique a importação e a inclusão do valor na sua base, conforme o artigo16. O IVA do serviço é apresentado separadamente do IVA dos bens; o simulador não soma novamente esse valor à base.</p>
{error&&<p role="alert">{error}</p>}
<button type="button" className="underline" onClick={()=>save({...book,rows:[...book.rows,newImportExemption(crypto.randomUUID(),String(draft.currency??''))]})}>Acrescentar importação relevante</button>
<button type="button" className="underline" onClick={()=>save({...book,rows:[...book.rows,newImportExemption(crypto.randomUUID(),String(draft.currency??''))]})}>Acrescentar importação ou serviço relevante</button>
{book.rows.map(row=>{
const rule=importExemptionRules.find(item=>item.id===row.treatment);
return <details key={row.id} className="rounded border p-3"><summary>{row.description||'Importação por identificar'} — {row.period||'mês por indicar'}</summary>
<div className="grid gap-3 md:grid-cols-2">
<label>Ramo da importação<select className="block rounded border p-2" value={row.treatment} onChange={e=>change(row.id,{treatment:e.target.value})}><option value="">Por indicar</option>{importExemptionRules.map(item=><option key={item.id} value={item.id}>{item.label}</option>)}</select></label>
<label>Natureza dos dados<select className="block rounded border p-2" value={row.origin} onChange={e=>change(row.id,{origin:e.target.value as ImportExemptionRow['origin']})}><option value="projected">Projectados / pressupostos</option><option value="effective">Efectivos declarados</option></select></label>
{(['description','importer','declaredValue','goodsCode','originCountry','documentRef','documentDate','period','evidence'] as const).map((field,index)=><label key={field}>{['Bens importados','Importador','Valor declarado / previsão (moeda indicada)','Código aduaneiro, se disponível','País de origem, se disponível','Documento aduaneiro / referência da previsão','Data do documento / prevista','Mês da importação','Fundamento factual das condições / pressupostos'][index]}<input className="block rounded border p-2" type={field==='documentDate'?'date':field==='period'?'month':'text'} value={row[field]} onChange={e=>change(row.id,{[field]:e.target.value})}/></label>)}
{(['description','importer','declaredValue','goodsCode','originCountry','documentRef','documentDate','period','evidence'] as const).map((field,index)=>isImportService(row.treatment)&&['goodsCode','originCountry'].includes(field)?null:<label key={field}>{isImportService(row.treatment)&&field==='description'?'Serviço associado à importação':isImportService(row.treatment)&&field==='importer'?'Importador destinatário do serviço':isImportService(row.treatment)&&field==='declaredValue'?'Valor do serviço incluído na base (moeda indicada)':isImportService(row.treatment)&&field==='documentRef'?'Documento do serviço / referência da previsão':isImportService(row.treatment)&&field==='period'?'Mês do serviço':['Bens importados','Importador','Valor declarado / previsão (moeda indicada)','Código aduaneiro, se disponível','País de origem, se disponível','Documento aduaneiro / referência da previsão','Data do documento / prevista','Mês da importação','Fundamento factual das condições / pressupostos'][index]}<input className="block rounded border p-2" type={field==='documentDate'?'date':field==='period'?'month':'text'} value={row[field]} onChange={e=>change(row.id,{[field]:e.target.value})}/></label>)}
{isImportService(row.treatment)&&(['serviceImportRef','serviceBaseRef','serviceProvider'] as const).map((field,index)=><label key={field}>{['Importação de bens associada / documento ou previsão','Referência da base tributável que inclui o valor do serviço','Prestador do serviço, se disponível'][index]}<input className="block rounded border p-2" value={row[field]??''} onChange={e=>change(row.id,{[field]:e.target.value})}/></label>)}
{row.treatment==='accessory_service_first_destination'&&<label>Primeiro destino e documento de transporte ou primeira ruptura de carga<input className="block rounded border p-2" value={row.serviceFirstDestination??''} onChange={e=>change(row.id,{serviceFirstDestination:e.target.value})}/></label>}
<label>Moeda de origem<input className="block rounded border p-2" value={row.currency} onChange={e=>change(row.id,{currency:e.target.value})}/></label>
</div>
{rule?<><p>{importRuleSource(row.treatment)}</p><fieldset><legend>Condições de{rule.article}; respostas conservadas ao mudar de ramo</legend>{rule.conditions.map(([key,label])=><label className="block" key={key}>{label}<select className="block rounded border p-2" value={row.conditions[key]??''} onChange={e=>change(row.id,{conditions:{...row.conditions,[key]:e.target.value}})}><option value="">Por indicar</option><option value="yes">Sim</option><option value="no">Não</option></select></label>)}</fieldset></>:<p>Escolha o ramo para apresentar apenas as condições aplicáveis; as respostas anteriores mantêm-se.</p>}
<p>Editar não substitui o resultado anterior. Ao classificar novamente, a versão anterior passa ao histórico.</p>
</details>;
})}
{!!book.rows.length&&<button type="button" className="rounded bg-emerald-800 p-2 text-white" onClick={()=>{try {save(recordImportExemptions(draft,book));}catch(cause){setError(String(cause));}}}>Classificar importações e guardar versão</button>}
{!!book.rows.length&&<button type="button" className="rounded bg-emerald-800 p-2 text-white" onClick={()=>{try {save(recordImportExemptions(draft,book));}catch(cause){setError(String(cause));}}}>{book.rows.some(row=>isImportService(row.treatment))?'Classificar importações e serviços e guardar versão':'Classificar importações e guardar versão'}</button>}
{book.result&&<><p role="status">{importExemptionCurrent(draft,book)?'Classificação correspondente às entradas guardadas.':'Entradas alteradas; classificação histórica preservada. Reveja e classifique novamente.'} {book.history?.length??0} versões anteriores.</p><dl>{importExemptionReportRows(draft).map(([,label,value,origin],index)=><div key={index}><dt className="font-semibold">{label}</dt><dd>{value}<small className="block">{origin}</small></dd></div>)}</dl></>}
</details>;
}
+19 -10
View File
@@ -14,7 +14,12 @@ export const importSpecialRegimeVersion = 'import-special-regimes/1';
export const importSpecialRegimeSource = 'CIVA republicado pela Lei13/2016, BR156/10.º suplemento, art.14(1)(a) e condição inicial14(1),PDF13/p.1426(271). Lei22/2022 e Lei10/2025 não alteram14 neste acervo. Importações sob controlo alfandegário e disposições especificamente aplicáveis, enquanto os bens permanecem nos regimes enumerados, sem utilização nem consumo finais nas áreas mencionadas. Cobertura2026/MZN; sem dedução/campo11, cálculo de direitos ou saída do regime.';
export const importSupplyVersion = 'import-onboard-supplies/1';
export const importSupplySource = 'CIVA republicado pela Lei13/2016,BR156/10.º suplemento,12(1)(g),12(3)(a)(i–iv)/(b),PDF11–12/p.1426(269–270),e definição13(3). Lei22/2022PDF2/p.2252(26) e Lei10/2025PDF13/p.2180(13) mantêm g/3 por remissão. Cobertura2026/MZN; abastecimentos importados na janela própria,consumidos ou a bordo de embarcação de navegação internacional marítima/fluvial/lacustre ou avião de navegação aérea internacional;exclusões próprias de provisões e depósitos normais. Sem exigir companhia principalmente internacional,sem dedução/campo11,direitos ou base aduaneira.';
export const importServiceVersion = 'import-included-services/1';
export const importServiceSource = 'CIVA republicado Lei13/2016,12(1)(e),PDF11/p.1426(269): prestações de serviços cujo valor esteja incluído na base tributável da importação de bens,conforme16. Art16(1)/16(1)(b)/16(2),PDF14/p.1426(272): valor aduaneiro e despesas acessórias até primeiro destino,do documento de transporte ou,na falta,primeira ruptura de carga no interior do País. Lei22/2022PDF1–2 e Lei10/2025PDF12–13 mantêm e por remissão e não alteram16. Cobertura2026/MZN; IVA do serviço,sem calcular base aduaneira,direitos,IVA dos bens,dedução/campo11 ou rectificação automática de factura.';
export const importExemptionRules = [
{id:'service_in_customs_value',branch:'P050',label:"Serviço já compreendido no valor aduaneiro",article:'12(1)(e)',conditions:[["serviceImportLinked", "A prestação está ligada à importação de bens identificada na referência própria; não basta tratar-se de serviço prestado a um importador"], ["serviceFullValueIncluded", "O valor integral do serviço desta linha está incluído na base tributável da importação identificada, nos termos do artigo16; documento ou pressuposto discrimina a inclusão, não apenas a intenção de a incluir"], ["serviceInCustomsValue", "O valor deste serviço já está compreendido no valor aduaneiro que integra a base do artigo16(1); não é uma nova parcela a somar pelo simulador"]]},
{id:'accessory_service_first_destination',branch:'P050',label:"Serviço acessório até ao primeiro destino",article:'12(1)(e)',conditions:[["serviceImportLinked", "A prestação está ligada à importação de bens identificada na referência própria; não basta tratar-se de serviço prestado a um importador"], ["serviceFullValueIncluded", "O valor integral do serviço desta linha está incluído na base tributável da importação identificada, nos termos do artigo16; documento ou pressuposto discrimina a inclusão, não apenas a intenção de a incluir"], ["serviceAccessoryCost", "O serviço é uma despesa acessória da importação (embalagem, transporte, seguro ou outro encargo) adicionada à base por ainda não estar compreendida no valor aduaneiro, conforme16(1)(b)"], ["serviceToFirstDestination", "A despesa verifica-se até ao primeiro lugar de destino no interior do País: o constante do documento de transporte ou, na sua falta, o local da primeira ruptura de carga no interior do País, conforme16(2); destino e percurso identificados no fundamento"]]},
{id:'ship_provisions_import',branch:'P052',label:"Embarcação internacional — Provisões de bordo",article:'12(1)(g)',conditions:[["supplyInternationalShip", "A embarcação identificada efectua navegação marítima, fluvial ou lacustre internacional; não basta pertencer a uma empresa marítima"], ["supplyProvisions", "São provisões de bordo: produtos destinados exclusivamente ao consumo da tripulação e dos passageiros"], ["supplyImportWindow", "Os bens são importados desde a entrada no território nacional até à chegada ao porto ou aeroporto nacionais de destino, ou durante a permanência nesses locais pelo período normal necessário ao cumprimento das tarefas; trajecto e datas constam do fundamento"], ["supplyOnboard", "Os bens importados são consumidos ou encontram-se a bordo da embarcação ou avião identificados no fundamento factual"], ["supplyNoDismantleOtherUse", "As provisões não se encontram em embarcação a desmantelar ou utilizada para fins diferentes da navegação internacional, durante essa situação"], ["supplyNoFloatingHospitality", "As provisões não se encontram em embarcação utilizada em hotel, restaurante, sala de jogos flutuante ou fim semelhante, durante permanência em porto ou águas territoriais/interiores nacionais"], ["supplyNoRecreation", "As provisões não se encontram em embarcação de recreio durante permanência em porto ou águas territoriais/interiores nacionais"], ["supplyNoCoastalFishing", "As provisões não se encontram em embarcação de pesca costeira"]]},
{id:'ship_fuel_import',branch:'P052',label:"Embarcação internacional — Combustíveis ou carburantes",article:'12(1)(g)',conditions:[["supplyInternationalShip", "A embarcação identificada efectua navegação marítima, fluvial ou lacustre internacional; não basta pertencer a uma empresa marítima"], ["supplyFuel", "São combustíveis ou carburantes destinados ao funcionamento das máquinas de propulsão ou de outros aparelhos de uso técnico instalados a bordo"], ["supplyImportWindow", "Os bens são importados desde a entrada no território nacional até à chegada ao porto ou aeroporto nacionais de destino, ou durante a permanência nesses locais pelo período normal necessário ao cumprimento das tarefas; trajecto e datas constam do fundamento"], ["supplyOnboard", "Os bens importados são consumidos ou encontram-se a bordo da embarcação ou avião identificados no fundamento factual"], ["supplyNormalTanks", "Os combustíveis/carburantes importados são os contidos nos depósitos normais; depósitos suplementares ou externos não cumprem esta condição"]]},
{id:'ship_technical_products_import',branch:'P052',label:"Embarcação internacional — Lubrificantes e outros produtos de funcionamento técnico",article:'12(1)(g)',conditions:[["supplyInternationalShip", "A embarcação identificada efectua navegação marítima, fluvial ou lacustre internacional; não basta pertencer a uma empresa marítima"], ["supplyTechnicalProducts", "São lubrificantes ou outros produtos, distintos dos combustíveis/carburantes, destinados ao funcionamento das máquinas de propulsão ou de outros aparelhos de uso técnico instalados a bordo"], ["supplyImportWindow", "Os bens são importados desde a entrada no território nacional até à chegada ao porto ou aeroporto nacionais de destino, ou durante a permanência nesses locais pelo período normal necessário ao cumprimento das tarefas; trajecto e datas constam do fundamento"], ["supplyOnboard", "Os bens importados são consumidos ou encontram-se a bordo da embarcação ou avião identificados no fundamento factual"]]},
@@ -43,15 +48,16 @@ export const importExemptionRules = [
{id:'customs_temporary_import',branch:'P048',label:'Importação temporária de bens com isenção total de direitos',article:'12(1)(c)',conditions:[['customsTemporary','Os bens identificados são importados sob regime de importação temporária; regime e operação constam do documento ou pressuposto expresso, não apenas intenção de utilização temporária'],['fullCustomsExemption','Esta importação beneficia de isenção total de direitos aduaneiros, identificada no fundamento factual; redução parcial ou mera suspensão não comprovam esta condição']]},
{id:'customs_drawback_import',branch:'P048',label:'Importação de bens em draubaque com isenção total de direitos',article:'12(1)(c)',conditions:[['customsDrawback','Os bens identificados são importados sob regime de draubaque; regime e operação constam do documento ou pressuposto expresso, não qualquer promessa de restituição futura'],['fullCustomsExemption','Esta importação beneficia de isenção total de direitos aduaneiros, identificada no fundamento factual; redução parcial ou mera suspensão não comprovam esta condição']]},
] as const;
type Condition = 'supplyCargoAccessories'|'supplyFuel'|'supplyImportWindow'|'supplyInternationalAircraft'|'supplyInternationalShip'|'supplyNoCoastalFishing'|'supplyNoDismantleOtherUse'|'supplyNoFloatingHospitality'|'supplyNoRecreation'|'supplyNormalTanks'|'supplyOnboard'|'supplyProvisions'|'supplyTechnicalProducts'|'specialEconomicZone'|'freeZone'|'freeWarehouse'|'generalFreeWarehouse'|'customsWarehouse'|'dutyFreeShop'|'specialCustomsControl'|'specialApplicableProvisions'|'specialRegimePermanence'|'specialNoFinalUseConsumption'|'sameExporter'|'sameState'|'customsExempt'|'gold'|'centralBank'|'artObjects'|'eligibleArtImporter'|'fireVehicle'|'fireBrigade'|'exclusiveOwnUse'|'eligibleShip'|'importedShip'|'shipObjectLink'|'eligibleAircraft'|'importedAircraft'|'aircraftObjectLink'|'customsTransit'|'customsTemporary'|'customsDrawback'|'fullCustomsExemption';
type Condition = 'serviceImportLinked'|'serviceFullValueIncluded'|'serviceInCustomsValue'|'serviceAccessoryCost'|'serviceToFirstDestination'|'supplyCargoAccessories'|'supplyFuel'|'supplyImportWindow'|'supplyInternationalAircraft'|'supplyInternationalShip'|'supplyNoCoastalFishing'|'supplyNoDismantleOtherUse'|'supplyNoFloatingHospitality'|'supplyNoRecreation'|'supplyNormalTanks'|'supplyOnboard'|'supplyProvisions'|'supplyTechnicalProducts'|'specialEconomicZone'|'freeZone'|'freeWarehouse'|'generalFreeWarehouse'|'customsWarehouse'|'dutyFreeShop'|'specialCustomsControl'|'specialApplicableProvisions'|'specialRegimePermanence'|'specialNoFinalUseConsumption'|'sameExporter'|'sameState'|'customsExempt'|'gold'|'centralBank'|'artObjects'|'eligibleArtImporter'|'fireVehicle'|'fireBrigade'|'exclusiveOwnUse'|'eligibleShip'|'importedShip'|'shipObjectLink'|'eligibleAircraft'|'importedAircraft'|'aircraftObjectLink'|'customsTransit'|'customsTemporary'|'customsDrawback'|'fullCustomsExemption';
function beneficiaryImport(id:string):boolean {return id==='author_art_import'||id==='fire_brigade_vehicle';}
function assetImport(id:string):boolean {return ['eligible_ship_import','ship_operation_object_import','eligible_aircraft_import','aircraft_operation_object_import'].includes(id);}
function customsImport(id:string):boolean {return ['customs_transit_import','customs_temporary_import','customs_drawback_import'].includes(id);}
function specialRegimeImport(id:string):boolean {return ["special_economic_zone_import", "free_zone_import", "free_warehouse_import", "general_free_warehouse_import", "customs_warehouse_import", "duty_free_shop_import"].includes(id);}
function supplyImport(id:string):boolean {return ["ship_provisions_import", "ship_fuel_import", "ship_technical_products_import", "ship_cargo_accessories_import", "aircraft_provisions_import", "aircraft_fuel_import", "aircraft_technical_products_import", "aircraft_cargo_accessories_import"].includes(id);}
export function importRuleSource(id:string):string {return supplyImport(id)?importSupplySource:specialRegimeImport(id)?importSpecialRegimeSource:customsImport(id)?importCustomsSource:assetImport(id)?importAssetSource:beneficiaryImport(id)?importBeneficiarySource:importExemptionSource;}
export type ImportExemptionRow = {id:string;treatment:string;description:string;period:string;origin:'projected'|'effective';currency:string;declaredValue:string;goodsCode:string;originCountry:string;importer:string;documentRef:string;documentDate:string;evidence:string;conditions:Partial<Record<Condition,Choice>>};
type ImportResult = {id:string;status:'exempt'|'not_applicable'|'indeterminate';importVat:string|null;reason:string};
export function isImportService(id:string):boolean {return ['service_in_customs_value','accessory_service_first_destination'].includes(id);}
export function importRuleSource(id:string):string {return isImportService(id)?importServiceSource:supplyImport(id)?importSupplySource:specialRegimeImport(id)?importSpecialRegimeSource:customsImport(id)?importCustomsSource:assetImport(id)?importAssetSource:beneficiaryImport(id)?importBeneficiarySource:importExemptionSource;}
export type ImportExemptionRow = {id:string;treatment:string;description:string;period:string;origin:'projected'|'effective';currency:string;declaredValue:string;goodsCode:string;originCountry:string;importer:string;documentRef:string;documentDate:string;evidence:string;conditions:Partial<Record<Condition,Choice>>;serviceImportRef?:string;serviceBaseRef?:string;serviceFirstDestination?:string;serviceProvider?:string};
type ImportResult = {id:string;status:'exempt'|'not_applicable'|'indeterminate';importVat:string|null;serviceVat?:string|null;reason:string};
export type ImportExemptionCalculation = {version:string;revision:number;calculatedAt:string;opportunity:string;location:string;sourceKey:string;source:string;inputs:ImportExemptionRow[];rows:ImportResult[]};
export type ImportExemptionBook = {rows:ImportExemptionRow[];result?:ImportExemptionCalculation;history?:ImportExemptionCalculation[]};
type Store = {version:1;books:Record<string,unknown>;importExemptions?:Record<string,ImportExemptionBook>;[key:string]:unknown};
@@ -60,36 +66,39 @@ function store(draft:Record<string,unknown>):Store {
if(value?.version!==1||!value.books||typeof value.books!=='object'||Array.isArray(value.books))throw new Error('Dados fiscais não recuperáveis; originais preservados, sem substituição.');
if(value.importExemptions!==undefined&&(!value.importExemptions||typeof value.importExemptions!=='object'||Array.isArray(value.importExemptions)))throw new Error('Importações guardadas não recuperáveis; originais preservados.');
for(const book of Object.values(value.importExemptions??{})) {
if(Array.isArray(book?.rows)&&book.rows.some(row=>row&&['serviceImportRef','serviceBaseRef','serviceFirstDestination','serviceProvider'].some(key=>row[key as keyof ImportExemptionRow]!==undefined&&typeof row[key as keyof ImportExemptionRow]!=='string')))throw new Error('Referências do serviço associado à importação não recuperáveis; originais preservados.');
if(!book||!Array.isArray(book.rows)||(book.history!==undefined&&!Array.isArray(book.history))||book.rows.some(row=>!row||['id','treatment','description','period','origin','currency','declaredValue','goodsCode','originCountry','importer','documentRef','documentDate','evidence'].some(k=>typeof row[k as keyof ImportExemptionRow]!=='string')||!row.conditions||typeof row.conditions!=='object'||Array.isArray(row.conditions)))throw new Error('Linhas de importação não recuperáveis; sem substituição por lista vazia.');
}
return value;
}
export function importExemptionBook(draft:Record<string,unknown>):ImportExemptionBook {return store(draft).importExemptions?.[acquisitionScope(draft)]??{rows:[]};}
export function saveImportExemptionBook(draft:Record<string,unknown>,book:ImportExemptionBook):string {
const current=store(draft);return JSON.stringify({...current,importExemptions:{...current.importExemptions,[acquisitionScope(draft)]:book}});
const current=store(draft),encoded=JSON.stringify({...current,importExemptions:{...current.importExemptions,[acquisitionScope(draft)]:book}});store({...draft,modelo_a_snapshot:encoded});return encoded;
}
export function newImportExemption(id:string,currency:string):ImportExemptionRow {return {id,treatment:'',description:'',period:'',origin:'projected',currency,declaredValue:'',goodsCode:'',originCountry:'',importer:'',documentRef:'',documentDate:'',evidence:'',conditions:{}};}
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 evaluateImportExemption(row:ImportExemptionRow,currency:unknown):ImportResult {
const result:ImportResult={id:row.id,status:'indeterminate',importVat:null,reason:''};
const result:ImportResult={id:row.id,status:'indeterminate',importVat:null,reason:'',...(isImportService(row.treatment)?{serviceVat:null}:{})};
const stop=(reason:string,status:ImportResult['status']='indeterminate')=>({...result,status,reason});
const rule=importExemptionRules.find(r=>r.id===row.treatment);
if(!rule)return stop('Escolha o ramo aplicável; referência desconhecida ou em falta não concede isenção.');
if(currency!=='MZN'||row.currency!==currency)return stop('Moeda fora do âmbito MZN ou divergente do projecto; valores conservados, sem conversão.');
if(!/^2026-(0[1-9]|1[0-2])$/.test(row.period))return stop('Indique o período da importação em2026; não é inferido do mês do orçamento.');
if(!/^2026-(0[1-9]|1[0-2])$/.test(row.period))return stop(isImportService(row.treatment)?'Indique o período do serviço em2026; a importação é identificada separadamente, sem inferir o mês do orçamento.':'Indique o período da importação em2026; não é inferido do mês do orçamento.');
if(!['projected','effective'].includes(row.origin))return stop('Indique dados efectivos ou pressupostos de projecção.');
const failed=rule.conditions.filter(([key])=>row.conditions[key]==='no');
if(failed.length)return stop('Não se aplica esta isenção: '+failed.map(([,label])=>label).join('; ')+'. O IVA fora deste fundamento permanece por enquadrar, sem taxa presumida.','not_applicable');
const missing=rule.conditions.filter(([key])=>row.conditions[key]!=='yes');
if(missing.length)return stop('Condições por indicar: '+missing.map(([,label])=>label).join('; ')+'. Desconhecido não significa não aplicável nem IVA zero.');
if(!row.description.trim()||!row.importer.trim()||!row.documentRef.trim()||!row.evidence.trim())return stop('Identifique bens, importador, documento aduaneiro ou previsão e o fundamento factual das condições; não é exigida segunda validação da fonte oficial.');
if(isImportService(row.treatment)&&(!row.serviceImportRef?.trim()||!row.serviceBaseRef?.trim()||!row.declaredValue.trim()||(row.treatment==='accessory_service_first_destination'&&!row.serviceFirstDestination?.trim())))return stop('Identifique a importação de bens, a referência da base que inclui o valor do serviço e o montante desta linha; no serviço acessório, indique o primeiro destino e o fundamento documental ou pressuposto.');
if(!row.description.trim()||!row.importer.trim()||!row.documentRef.trim()||!row.evidence.trim())return stop(isImportService(row.treatment)?'Identifique o serviço, importador destinatário, documento/previsão e fundamento factual da inclusão; sem exigir segunda validação da fonte oficial.':'Identifique bens, importador, documento aduaneiro ou previsão e o fundamento factual das condições; não é exigida segunda validação da fonte oficial.');
if(!validDate(row.documentDate))return stop('Data do documento/previsão em falta ou inválida; o período da importação é indicado separadamente.');
if(row.declaredValue.trim()&&!/^\d{1,15}(\.\d{1,6})?$/.test(row.declaredValue.trim()))return stop('Valor declarado/previsional inválido; conserve o original e reveja, sem presumir base aduaneira ou imposto normal.');
if(isImportService(row.treatment))return {...result,status:'exempt',serviceVat:'0.00',reason:`Isenção do serviço ${rule.article} nas condições ${row.origin==='projected'?'expressamente assumidas na projecção':'efectivas declaradas'}. IVA do serviço:0.00MZN; não é o IVA dos bens importados, não soma novamente o serviço à base nem corrige factura ou dedução.`};
return {...result,status:'exempt',importVat:'0.00',reason:`Isenção ${rule.article} nas condições ${row.origin==='projected'?'expressamente assumidas na projecção':'efectivas declaradas'}. IVA na importação:0.00MZN por isenção, sem inferir direitos aduaneiros, certificação pela AT ou dedução.`};
}
function key(draft:Record<string,unknown>,book:ImportExemptionBook):string {
const base={version:importExemptionVersion,scope:acquisitionScope(draft),location:draft.selection_labels??{},currency:draft.currency,rows:book.rows};
return JSON.stringify({...base,...(book.rows.some(row=>beneficiaryImport(row.treatment))?{beneficiaryVersion:importBeneficiaryVersion}:{}),...(book.rows.some(row=>assetImport(row.treatment))?{assetVersion:importAssetVersion}:{}),...(book.rows.some(row=>customsImport(row.treatment))?{customsVersion:importCustomsVersion}:{}),...(book.rows.some(row=>specialRegimeImport(row.treatment))?{specialRegimeVersion:importSpecialRegimeVersion}:{}),...(book.rows.some(row=>supplyImport(row.treatment))?{supplyVersion:importSupplyVersion}:{})});
return JSON.stringify({...base,...(book.rows.some(row=>beneficiaryImport(row.treatment))?{beneficiaryVersion:importBeneficiaryVersion}:{}),...(book.rows.some(row=>assetImport(row.treatment))?{assetVersion:importAssetVersion}:{}),...(book.rows.some(row=>customsImport(row.treatment))?{customsVersion:importCustomsVersion}:{}),...(book.rows.some(row=>specialRegimeImport(row.treatment))?{specialRegimeVersion:importSpecialRegimeVersion}:{}),...(book.rows.some(row=>supplyImport(row.treatment))?{supplyVersion:importSupplyVersion}:{}),...(book.rows.some(row=>isImportService(row.treatment))?{serviceVersion:importServiceVersion}:{})});
}
export function importExemptionCurrent(draft:Record<string,unknown>,book:ImportExemptionBook):boolean {return !!book.result&&book.result.sourceKey===key(draft,book);}
export function recordImportExemptions(draft:Record<string,unknown>,book:ImportExemptionBook):ImportExemptionBook {
@@ -101,6 +110,6 @@ export function importExemptionReportRows(draft:Record<string,unknown>):Array<[s
try {
const book=importExemptionBook(draft),result=book.result;if(!result)return [];
const context=`${result.opportunity}; ${result.location}; ${importExemptionCurrent(draft,book)?'Resultado actual':'Entradas alteradas; resultado histórico conservado'}; versão${result.revision}; ${result.calculatedAt}; ${result.source}`;
return result.inputs.map((row,index)=>['IVA mensal',`Importação ${importExemptionRules.find(r=>r.id===row.treatment)?.branch??row.treatment} — ${row.period} — ${row.id}`,`${row.description}; IVA na importação: ${result.rows[index].importVat??'Indeterminado'} ${row.currency}; ${result.rows[index].reason}`,`${context}; ${row.origin==='projected'?'Projecção':'Dados efectivos declarados'}; importador ${row.importer}; valor declarado/previsional ${row.declaredValue||'Não informado'} ${row.currency}, sem fixar base aduaneira; código ${row.goodsCode||'não indicado'}; origem ${row.originCountry||'não indicada'}; documento ${row.documentRef},${row.documentDate}; condições ${(importExemptionRules.find(rule=>rule.id===row.treatment)?.conditions??[]).map(([key,label])=>label+': '+(row.conditions[key]==='yes'?'Sim':row.conditions[key]==='no'?'Não':'Por indicar')).join('; ')}; fundamento factual ${row.evidence}; campo11 e totaisModeloA não preenchidos por esta classificação.`]);
return result.inputs.map((row,index)=>['IVA mensal',`${isImportService(row.treatment)?'Serviço associado à importação':'Importação'} ${importExemptionRules.find(r=>r.id===row.treatment)?.branch??row.treatment} — ${row.period} — ${row.id}`,`${row.description}; ${isImportService(row.treatment)?'IVA do serviço':'IVA na importação'}: ${(isImportService(row.treatment)?result.rows[index].serviceVat:result.rows[index].importVat)??'Indeterminado'} ${row.currency}; ${result.rows[index].reason}`,`${context}; ${row.origin==='projected'?'Projecção':'Dados efectivos declarados'}; importador ${row.importer}; valor declarado/previsional ${row.declaredValue||'Não informado'} ${row.currency}, sem fixar base aduaneira; ${isImportService(row.treatment)?`importação associada ${row.serviceImportRef||'por indicar'}; base com inclusão do serviço ${row.serviceBaseRef||'por indicar'}; prestador ${row.serviceProvider||'por indicar'}${row.treatment==='accessory_service_first_destination'?`; primeiro destino ${row.serviceFirstDestination||'por indicar'}`:''}`:`código ${row.goodsCode||'não indicado'}; origem ${row.originCountry||'não indicada'}`}; documento ${row.documentRef},${row.documentDate}; condições ${(importExemptionRules.find(rule=>rule.id===row.treatment)?.conditions??[]).map(([key,label])=>label+': '+(row.conditions[key]==='yes'?'Sim':row.conditions[key]==='no'?'Não':'Por indicar')).join('; ')}; fundamento factual ${row.evidence}; campo11 e totaisModeloA não preenchidos por esta classificação.`]);
}catch {return [['IVA mensal','Importações','Dados não recuperáveis','Originais preservados; sem substituir por zero.']];}
}
@@ -0,0 +1,56 @@
<?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 IvaImportServicesDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_import_classification_recovers_privately_without_changing_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-g6f-imports.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:336', $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['importExemptions']['opportunity:336'];
$this->assertCount(2, $book['rows']);
$this->assertSame(['service_in_customs_value','accessory_service_first_destination'], array_column($book['rows'], 'treatment'));
$this->assertSame(array_fill(0, 2, '0.00'), array_column($book['result']['rows'], 'serviceVat'));
$this->assertSame([null,null], array_column($book['result']['rows'], 'importVat'));
$this->assertSame('SYNTHETIC QA importação DU-A,2026-12', $book['rows'][0]['serviceImportRef']);
$this->assertSame('SYNTHETIC QA base DU-A,linha serviço5000MZN', $book['rows'][0]['serviceBaseRef']);
$this->assertSame(array_fill(0, 2, 'exempt'), array_column($book['result']['rows'], 'status'));
$this->assertSame('2026-12', $book['rows'][0]['period']);
$this->assertSame('5000', $book['rows'][0]['declaredValue']);
$this->assertSame('SYNTHETIC QA G6F service_in_customs_value', $book['rows'][0]['documentRef']);
$this->assertStringContainsString('12(1)(e)', $book['result']['source']);
$frozen = json_decode(file_get_contents(base_path('tests/frontend/fixtures/iva-offer-paths-inputs.json')), true, flags: JSON_THROW_ON_ERROR);
$prior = json_decode($frozen['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
$this->assertSame($prior['salesOperations'], $store['salesOperations']);
$this->assertSame($prior['books'], $store['books']);
$this->assertSame($prior['importExemptions']['opportunity:330'], $store['importExemptions']['opportunity:330']);
$this->assertSame($prior['importExemptions']['opportunity:331'], $store['importExemptions']['opportunity:331']);
$this->assertSame($prior['importExemptions']['opportunity:332'], $store['importExemptions']['opportunity:332']);
$this->assertCount(2, $book['history']);
$this->assertSame($prior['importExemptions'], array_diff_key($store['importExemptions'], ['opportunity:336' => true]));
$this->assertSame($prior['offerPreparations'], $store['offerPreparations']);
$this->assertNull($book['history'][0]['rows'][0]['importVat']);
$this->assertNull($store['books']['opportunity:329']['result']['periods'][0]['fields']['11']);
$this->assertNull($store['books']['opportunity:329']['result']['periods'][0]['fields']['17']);
$this->assertStringContainsString('BR89', $store['books']['opportunity:329']['result']['sources']);
file_put_contents(base_path('outputs/iva-l1-g6f-services-20261024/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,28 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { newImportExemption,evaluateImportExemption,recordImportExemptions,saveImportExemptionBook,importExemptionBook,importExemptionCurrent,importExemptionRules,importServiceSource } from '../../resources/js/lib/iva-import-exemptions.ts';
import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts';
const frozen=JSON.parse(fs.readFileSync(new URL('./fixtures/iva-offer-paths-inputs.json',import.meta.url),'utf8'));
const rules=importExemptionRules.filter(r=>r.branch==='P050');
const row=rule=>({...newImportExemption('g6f-'+rule.id,'MZN'),treatment:rule.id,description:'SYNTHETIC QA '+rule.label,period:'2026-12',declaredValue:'5000',importer:'SYNTHETIC QA importador destinatário',documentRef:'SYNTHETIC QA G6F '+rule.id,documentDate:'2026-11-14',evidence:'SYNTHETIC QA serviço e montante integral incluídos na base identificada, conforme artigo16; pressuposto expresso, não documento real',serviceImportRef:'SYNTHETIC QA importação DU-A,2026-12',serviceBaseRef:'SYNTHETIC QA base DU-A,linha serviço5000MZN',serviceProvider:'SYNTHETIC QA prestador',...(rule.id==='accessory_service_first_destination'?{serviceFirstDestination:'SYNTHETIC QA Manica,primeiro destino no documento de transporte'}:{}),conditions:Object.fromEntries(rule.conditions.map(([key])=>[key,'yes']))});
const draft=()=>({...structuredClone(frozen),context_opportunity:'opportunity:336',project_title:'SYNTHETIC QA — G6F / Serviços na base da importação'});
test('P050 two treatments exempt only service VAT;every missing/failed condition leaves service and imported-goods tax indeterminate',()=>{
assert.equal(rules.length,2);assert.match(importServiceSource,/16\(2\)/);
for(const rule of rules){const input=row(rule),ok=evaluateImportExemption(input,'MZN');assert.equal(ok.serviceVat,'0.00');assert.equal(ok.importVat,null);assert.match(ok.reason,/IVA do serviço/);
for(const [key] of rule.conditions)for(const value of ['','unknown','no']){const r=evaluateImportExemption({...input,conditions:{...input.conditions,[key]:value}},'MZN');assert.equal(r.status,value==='no'?'not_applicable':'indeterminate');assert.equal(r.serviceVat,null);assert.equal(r.importVat,null);}
for(const patch of [{serviceImportRef:''},{serviceBaseRef:''},{declaredValue:''},{documentRef:''},{period:'2025-12'},{currency:'USD'},{documentDate:'2026-02-30'}])assert.equal(evaluateImportExemption({...input,...patch},'MZN').serviceVat,null);
}
});
test('first-destination reference is required only for added accessory service;switching keeps facts but cannot inherit own eligibility',()=>{
const inValue=row(rules[0]),accessory=row(rules[1]);assert.equal(evaluateImportExemption({...accessory,serviceFirstDestination:''},'MZN').status,'indeterminate');assert.equal(evaluateImportExemption(inValue,'MZN').status,'exempt');const changed={...inValue,treatment:rules[1].id};assert.equal(evaluateImportExemption(changed,'MZN').status,'indeterminate');assert.equal(changed.documentRef,inValue.documentRef);assert.ok(!rules[0].conditions.some(([key])=>key==='serviceToFirstDestination'));
const beyond={...accessory,conditions:{...accessory.conditions,serviceToFirstDestination:'no'}};assert.equal(evaluateImportExemption(beyond,'MZN').status,'not_applicable');assert.equal(evaluateImportExemption({...inValue,conditions:{...inValue.conditions,serviceFullValueIncluded:'no'}},'MZN').serviceVat,null);
});
test('save/recovery/report preserve27fiscalbooks and946fc089scenarios/requests/history exactly;service is not added to base or deduction',()=>{
const d=draft(),prior=JSON.parse(frozen.modelo_a_snapshot);let book=recordImportExemptions(d,{rows:rules.map(rule=>({...row(rule),conditions:{}}))});book=recordImportExemptions(d,{...book,rows:rules.map(rule=>({...row(rule),conditions:{...row(rule).conditions,serviceFullValueIncluded:'no'}}))});book=recordImportExemptions(d,{...book,rows:rules.map(row)});assert.equal(book.history.length,2);assert.ok(book.history.every(h=>h.rows.every(r=>r.serviceVat===null&&r.importVat===null)));d.modelo_a_snapshot=saveImportExemptionBook(d,book);assert.deepEqual(importExemptionBook(JSON.parse(JSON.stringify(d))),book);assert.match(book.result.sourceKey,/import-included-services\/1/);const next=JSON.parse(d.modelo_a_snapshot);for(const key of ['books','salesOperations','offerPreparations'])assert.deepEqual(next[key],prior[key]);for(const [scope,previous] of Object.entries(prior.importExemptions)){assert.deepEqual(next.importExemptions[scope],previous);assert.ok(importExemptionCurrent({...d,context_opportunity:scope},previous));}assert.equal(next.books['opportunity:329'].result.periods[0].fields['11'],null);
const report=JSON.stringify(createProjectDossierReport(d,{selectedSections:new Set(['finance']),version:'QA G6F'}));for(const token of ['Serviço associado à importação P050','IVA do serviço: 0.00','opportunity:336','Manica','2026-12','SYNTHETIC QA G6F','12(1)(e)','primeiro destino','campo11'])assert.ok(report.includes(token),token);assert.ok(!report.includes('IVA na importação: 0.00'));if(process.env.IVA_G6F_CALCULATED)fs.writeFileSync(process.env.IVA_G6F_CALCULATED,JSON.stringify(d));
});
test('old goods rows keep own tax representation and malformed optional service references cannot overwrite saved originals',()=>{
const d=draft(),prior=JSON.parse(d.modelo_a_snapshot);for(const book of Object.values(prior.importExemptions))assert.ok(book.result.rows.every(row=>!Object.hasOwn(row,'serviceVat')));
assert.throws(()=>saveImportExemptionBook(d,{rows:[{...row(rules[0]),serviceBaseRef:42}]}),/Referências do serviço/);
});