feat(iva): classify UN development imports with proportional relief

This commit is contained in:
2026-10-24 21:29:22 +02:00
parent 163e090ea5
commit 69b6c49a60
6 changed files with 162 additions and 8 deletions
@@ -1,5 +1,5 @@
import { useState } from 'react';
import { importExemptionBook,importExemptionCurrent,importExemptionReportRows,importExemptionRules,importRuleSource,isImportService,newImportExemption,recordImportExemptions,saveImportExemptionBook,type ImportExemptionBook,type ImportExemptionRow } from '@/lib/iva-import-exemptions';
import { importExemptionBook,importExemptionCurrent,importExemptionReportRows,importExemptionRules,importRuleSource,isImportService,isDevelopmentImport,importDevelopmentFields,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('');
@@ -9,7 +9,7 @@ export function SimulationImportExemptions({draft,onChange}:{draft:Record<string
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 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, equipamento classe K para empreendimento autorizado, 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>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, equipamento classe K para empreendimento autorizado, materiais/equipamentos para projecto de desenvolvimento ONU 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>}
@@ -28,6 +28,16 @@ export function SimulationImportExemptions({draft,onChange}:{draft:Record<string
<label>Autorização do empreendimento / pressuposto expresso<input className="block rounded border p-2" value={row.investmentAuthorizationRef??''} onChange={e=>change(row.id,{investmentAuthorizationRef:e.target.value})}/></label>
<label>Âmbito aplicável: bens, operação, período e condições<input className="block rounded border p-2" value={row.investmentScope??''} onChange={e=>change(row.id,{investmentScope:e.target.value})}/></label>
</>}
{isDevelopmentImport(row.treatment)&&<>
<p className="md:col-span-2">Pedido apresentado não equivale a despacho favorável nem a concessão aduaneira. Reutilize as referências dos documentos; numa projecção, identifique os pressupostos. O âmbito do despacho, lista e plano conserva-se nesta importação, sem o estender a outros bens ou operações.</p>
{importDevelopmentFields.map(([field,label])=><label key={field}>{label}<input className="block rounded border p-2" value={row[field]??''} onChange={e=>change(row.id,{[field]:e.target.value})}/></label>)}
{row.treatment==='un_development_reduction'&&<>
<p className="md:col-span-2">A redução de IVA acompanha a percentagem de redução de direitos, que não é a taxa aduaneira. Informe o IVA sem benefício já determinado ou expressamente assumido; o portal não presume taxa nem base aduaneira. Resultado decimal exacto, sem arredondamento fiscal presumido.</p>
<label>Redução dos direitos (% entre0e100, exclusiva)<input className="block rounded border p-2" value={row.developmentReductionPercent??''} onChange={e=>change(row.id,{developmentReductionPercent:e.target.value})}/></label>
<label>IVA desta importação sem benefício (moeda indicada)<input className="block rounded border p-2" value={row.developmentNormalVat??''} onChange={e=>change(row.id,{developmentNormalVat:e.target.value})}/></label>
<label>Referência documental / pressuposto do IVA sem benefício<input className="block rounded border p-2" value={row.developmentNormalVatRef??''} onChange={e=>change(row.id,{developmentNormalVatRef: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>}
+48 -6
View File
@@ -16,9 +16,31 @@ 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 importInvestmentVersion = 'import-investment-equipment/1';
export const importDevelopmentVersion = 'import-un-development/1';
export const importDevelopmentSource = 'CIVA republicado Lei13/2016,12(2)(c),PDF11/p.1426(269): materiais/equipamentos de projectos de desenvolvimento financiados pelas agências e instituições especializadas das Nações Unidas,devidamente acreditadas junto do Governo Moçambicano,destinados exclusivamente à implementação dos projectos;isenção ou redução de IVA na mesma proporção da redução de direitos. Art12(4),PDF12/p.1426(270): despacho favorável do Ministro que superintende Finanças,requerimento prévio da entidade promotora com lista discriminada e plano de importações;concessão pelos serviços aduaneiros segundo o plano e após confronto com a lista aprovada. Lei22/2022PDF2–3 eLei10/2025PDF13 mantêm12(2)/4 por remissão. Cobertura2026/MZN;sem calcular direitos/base aduaneira/taxa normal ou dedução/campo11;pedido não equivale a concessão.';
export const importDevelopmentFields = [
['developmentProject','Projecto de desenvolvimento identificado'],
['developmentAgency','Agência ou instituição especializada das Nações Unidas e referência da acreditação'],
['developmentDecisionRef','Despacho favorável / pressuposto expresso da projecção'],
['developmentListRef','Lista discriminada aprovada / referência da previsão'],
['developmentPlanRef','Plano de importações / referência da previsão'],
['developmentScope','Âmbito nesta importação: bens, operação, período e condições abrangidos'],
['developmentRightsRef','Fundamento da isenção ou redução de direitos / pressuposto expresso'],
] as const;
const developmentConditions = [
['developmentMaterials','Os bens desta importação são materiais ou equipamentos do projecto de desenvolvimento identificado'],
['developmentUNFunding','O projecto é financiado pela agência ou instituição especializada das Nações Unidas identificada; não basta financiamento externo genérico'],
['developmentAccredited','A agência ou instituição especializada identificada está devidamente acreditada junto do Governo Moçambicano'],
['developmentExclusiveUse','Os materiais ou equipamentos destinam-se exclusivamente à implementação deste projecto'],
['developmentFavourableDecision','Existe despacho favorável do Ministro que superintende a área de Finanças para este âmbito, ou esse despacho é pressuposto expresso da projecção; requerimento apresentado não equivale a decisão favorável'],
['developmentPriorRequest','A entidade promotora apresentou requerimento prévio acompanhado de lista discriminada dos bens a importar e respectivo plano de importações, ou estes elementos são pressupostos expressos da projecção'],
['developmentCustomsConformity','A concessão pelos serviços aduaneiros para esta importação segue o plano e o confronto com a lista aprovada no despacho, ou tal conformidade é pressuposto expresso da projecção; não se estende a outros bens ou operações'],
] as const;
export const importInvestmentSource = 'CIVA republicado Lei13/2016,12(1)(k),PDF11/p.1426(269): importações de bens de equipamento classificados na classe K da Pauta Aduaneira destinados a investimentos em empreendimentos autorizados ao abrigo da Lei de Investimentos e respectivo Regulamento. Lei22/2022,PDF2/p.2252(26),e Lei10/2025,PDF13/p.2180(13),mantêm k por remissão. Cobertura2026/MZN; classificação do IVA desta importação nas condições próprias,sem conceder autorização de investimento,classificar automaticamente mercadorias,estender o âmbito da autorização,calcular direitos/base aduaneira ou dedução/campo11.';
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:'un_development_exemption',branch:'P060',label:'Projecto de desenvolvimento ONU — isenção integral',article:'12(2)(c)/12(4)',conditions:[...developmentConditions,['developmentFullRightsExemption','Esta importação beneficia de isenção integral de direitos no fundamento identificado; uma redução parcial não equivale a isenção integral de IVA']]},
{id:'un_development_reduction',branch:'P060',label:'Projecto de desenvolvimento ONU — redução proporcional',article:'12(2)(c)/12(4)',conditions:[...developmentConditions,['developmentRightsReduction','Esta importação beneficia da redução parcial de direitos na percentagem identificada; não é uma taxa aduaneira nem a fracção de direitos ainda a pagar']]},
{id:'authorized_investment_equipment',branch:'P056',label:'Equipamento classe K para empreendimento autorizado',article:'12(1)(k)',conditions:[['investmentEquipment','Os bens importados são bens de equipamento; não basta uma compra de materiais, consumíveis ou serviços para um investimento'],['investmentClassK','Os bens de equipamento identificados estão classificados na classe K da Pauta Aduaneira; uma descrição genérica ou código isolado não é classificação automática pelo portal'],['investmentAuthorized','O empreendimento está autorizado ao abrigo da Lei de Investimentos e respectivo Regulamento; a referência identifica a autorização aplicável ou o pressuposto expresso da projecção, não apenas um pedido apresentado'],['investmentDestination','Estes bens destinam-se ao investimento no empreendimento autorizado identificado; a operação, período e condições correspondem ao âmbito aplicável da autorização, sem o estender automaticamente a outros bens ou operações']]},
{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"]]},
@@ -51,16 +73,17 @@ 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 = 'investmentEquipment'|'investmentClassK'|'investmentAuthorized'|'investmentDestination'|'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';
type Condition = 'developmentMaterials'|'developmentUNFunding'|'developmentAccredited'|'developmentExclusiveUse'|'developmentFavourableDecision'|'developmentPriorRequest'|'developmentCustomsConformity'|'developmentFullRightsExemption'|'developmentRightsReduction'|'investmentEquipment'|'investmentClassK'|'investmentAuthorized'|'investmentDestination'|'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 isImportService(id:string):boolean {return ['service_in_customs_value','accessory_service_first_destination'].includes(id);}
export function importRuleSource(id:string):string {return id==='authorized_investment_equipment'?importInvestmentSource: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;investmentAuthorizationRef?:string;investmentScope?:string};
type ImportResult = {id:string;status:'exempt'|'not_applicable'|'indeterminate';importVat:string|null;serviceVat?:string|null;reason:string};
export function isDevelopmentImport(id:string):boolean {return ['un_development_exemption','un_development_reduction'].includes(id);}
export function importRuleSource(id:string):string {return isDevelopmentImport(id)?importDevelopmentSource:id==='authorized_investment_equipment'?importInvestmentSource: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;investmentAuthorizationRef?:string;investmentScope?:string} & Partial<Record<typeof importDevelopmentFields[number][0]|'developmentReductionPercent'|'developmentNormalVat'|'developmentNormalVatRef',string>>;
type ImportResult = {id:string;status:'exempt'|'reduced'|'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};
@@ -69,6 +92,7 @@ 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&&[...importDevelopmentFields.map(([key])=>key),'developmentReductionPercent','developmentNormalVat','developmentNormalVatRef'].some(key=>row[key as keyof ImportExemptionRow]!==undefined&&typeof row[key as keyof ImportExemptionRow]!=='string')))throw new Error('Referências do projecto de desenvolvimento não recuperáveis; originais preservados.');
if(Array.isArray(book?.rows)&&book.rows.some(row=>row&&['investmentAuthorizationRef','investmentScope'].some(key=>row[key as keyof ImportExemptionRow]!==undefined&&typeof row[key as keyof ImportExemptionRow]!=='string')))throw new Error('Referências do investimento não recuperáveis; originais preservados.');
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.');
@@ -93,17 +117,35 @@ export function evaluateImportExemption(row:ImportExemptionRow,currency:unknown)
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(isDevelopmentImport(row.treatment)&&importDevelopmentFields.some(([key])=>!row[key]?.trim()))return stop('Identifique projecto, agência/acreditação, despacho favorável, lista aprovada, plano, âmbito desta importação e fundamento aduaneiro, ou os pressupostos expressos da projecção. Reutilize as referências documentais disponíveis; não é exigido novo certificado.');
if(row.treatment==='authorized_investment_equipment'&&(!row.investmentAuthorizationRef?.trim()||!row.investmentScope?.trim()))return stop('Identifique a autorização aplicável ao empreendimento e o seu âmbito nesta importação, ou os pressupostos expressos da projecção. Um pedido em preparação ou apresentado não equivale a autorização.');
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(row.treatment==='un_development_reduction') {
const normal=developmentDecimal(row.developmentNormalVat),percent=developmentDecimal(row.developmentReductionPercent);
if(percent===null)return stop('Indique a percentagem da redução de direitos identificada no fundamento; não é inferida da taxa aduaneira, do financiador ou do valor dos bens.');
if(percent===0n)return stop('Redução de direitos de0% não concede benefício proporcional de IVA neste fundamento; não se presume o imposto normal.','not_applicable');
if(percent>=100000000n)return stop('A redução parcial deve ser superior a0% e inferior a100%; para isenção integral use o tratamento próprio e as suas condições.');
if(normal===null||!row.developmentNormalVatRef?.trim())return stop('Indique o IVA desta importação sem benefício e a sua referência documental ou pressuposto; montante desconhecido não é zero e o portal não presume taxa/base aduaneira.');
const vat=developmentExactMoney(normal*(100000000n-percent));
return {...result,status:'reduced',importVat:vat,reason:`Redução proporcional ${rule.article} nas condições ${row.origin==='projected'?'expressamente assumidas na projecção':'efectivas declaradas'}. IVA informado sem benefício ${row.developmentNormalVat}MZN × (1 − ${row.developmentReductionPercent}/100) = ${vat}MZN. Proporção da redução de direitos, não taxa de IVA; aritmética decimal exacta, sem arredondamento fiscal presumido. Não calcula direitos/base aduaneira nem dedução.`};
}
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 developmentDecimal(raw:unknown):bigint|null {
if(typeof raw!=='string'||!/^\d{1,15}(\.\d{1,6})?$/.test(raw.trim()))return null;
const [whole,decimal='']=raw.trim().split('.');return BigInt(whole)*1000000n+BigInt(decimal.padEnd(6,'0'));
}
function developmentExactMoney(numerator:bigint):string {
const scale=100000000000000n;
return `${numerator/scale}.${(numerator%scale).toString().padStart(14,'0').replace(/0+$/,'').padEnd(2,'0')}`;
}
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=>row.treatment==='authorized_investment_equipment')?{investmentVersion:importInvestmentVersion}:{}),...(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}:{})});
return JSON.stringify({...base,...(book.rows.some(row=>isDevelopmentImport(row.treatment))?{developmentVersion:importDevelopmentVersion}:{}),...(book.rows.some(row=>row.treatment==='authorized_investment_equipment')?{investmentVersion:importInvestmentVersion}:{}),...(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 {
@@ -115,6 +157,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',`${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'}`}${row.treatment==='authorized_investment_equipment'?`; autorização/pressuposto ${row.investmentAuthorizationRef||'por indicar'}; âmbito aplicável ${row.investmentScope||'por indicar'}`:''}; 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'}`}${row.treatment==='authorized_investment_equipment'?`; autorização/pressuposto ${row.investmentAuthorizationRef||'por indicar'}; âmbito aplicável ${row.investmentScope||'por indicar'}`:''}${isDevelopmentImport(row.treatment)?`; ${importDevelopmentFields.map(([key,label])=>label+': '+(row[key]||'por indicar')).join('; ')}${row.treatment==='un_development_reduction'?`; redução dos direitos ${row.developmentReductionPercent||'por indicar'}%; IVA informado sem benefício ${row.developmentNormalVat||'por indicar'} ${row.currency}; referência do IVA sem benefício ${row.developmentNormalVatRef||'por indicar'}`:''}`:''}; 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 IvaImportDevelopmentDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_development_scope_and_proportional_result_recover_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-g6h-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:338', $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:338'];
$this->assertCount(2, $book['rows']);
$this->assertSame(['un_development_exemption','un_development_reduction'], array_column($book['rows'], 'treatment'));
$this->assertSame(['0.00','400.00'], array_column($book['result']['rows'], 'importVat'));
$this->assertSame(['exempt','reduced'], array_column($book['result']['rows'], 'status'));
$this->assertSame('2026-12', $book['rows'][0]['period']);
$this->assertSame('5000', $book['rows'][0]['declaredValue']);
foreach (['developmentProject','developmentAgency','developmentDecisionRef','developmentListRef','developmentPlanRef','developmentScope','developmentRightsRef'] as $field) {
$this->assertStringContainsString('SYNTHETIC QA', $book['rows'][0][$field]);
}
$this->assertSame('50', $book['rows'][1]['developmentReductionPercent']);
$this->assertSame('800', $book['rows'][1]['developmentNormalVat']);
$this->assertStringContainsString('IVA sem benefício informado', $book['rows'][1]['developmentNormalVatRef']);
$this->assertStringContainsString('12(4)', $book['result']['source']);
$frozen = json_decode(file_get_contents(base_path('tests/frontend/fixtures/iva-g6h-preserved-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'], array_diff_key($store['importExemptions'], ['opportunity:338' => true]));
$this->assertSame($prior['offerPreparations'], $store['offerPreparations']);
$this->assertCount(2, $book['history']);
$this->assertNull($book['history'][0]['rows'][0]['importVat']);
$this->assertSame('0.00', $store['importExemptions']['opportunity:337']['result']['rows'][0]['importVat']);
$this->assertSame('0.00', $store['importExemptions']['opportunity:336']['result']['rows'][0]['serviceVat']);
$this->assertNull($store['importExemptions']['opportunity:336']['result']['rows'][0]['importVat']);
$this->assertNull($store['books']['opportunity:329']['result']['periods'][0]['fields']['11']);
file_put_contents(base_path('outputs/iva-l1-g6h-development-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,44 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {newImportExemption,evaluateImportExemption,recordImportExemptions,saveImportExemptionBook,importExemptionBook,importExemptionCurrent,importExemptionRules,importDevelopmentSource,importDevelopmentFields} 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-g6h-preserved-inputs.json',import.meta.url),'utf8'));
const rules=importExemptionRules.filter(r=>r.branch==='P060');
const row=rule=>({...newImportExemption('g6h-'+rule.id,'MZN'),treatment:rule.id,description:'SYNTHETIC QA materiais/equipamentos projecto ONU',period:'2026-12',declaredValue:'5000',importer:'SYNTHETIC QA entidade promotora',documentRef:'SYNTHETIC QA G6H '+rule.id,documentDate:'2026-11-14',evidence:'SYNTHETIC QA pressupostos expressos; concessão aduaneira no plano e lista aprovada; não documento real',...Object.fromEntries(importDevelopmentFields.map(([key,label])=>[key,'SYNTHETIC QA '+label+';operação2026-12/bens A;pressuposto expresso'])),...(rule.id==='un_development_reduction'?{developmentReductionPercent:'50',developmentNormalVat:'800',developmentNormalVatRef:'SYNTHETIC QA IVA sem benefício informado, não calculado da base pelo portal'}:{}),conditions:Object.fromEntries(rule.conditions.map(([key])=>[key,'yes']))});
const draft=()=>({...structuredClone(frozen),context_opportunity:'opportunity:338',project_title:'SYNTHETIC QA — G6H / Projecto de desenvolvimento'});
test('P060 own funding,accreditation,destination,decision,list/plan and customs conditions;unknown/no never produce exempt zero',()=>{
assert.equal(rules.length,2);assert.match(importDevelopmentSource,/12\(4\)/);
for(const rule of rules){const input=row(rule),ok=evaluateImportExemption(input,'MZN');assert.equal(ok.importVat,rule.id==='un_development_exemption'?'0.00':'400.00');assert.equal(ok.status,rule.id==='un_development_exemption'?'exempt':'reduced');assert.equal(ok.serviceVat,undefined);
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.importVat,null);}
for(const [key] of importDevelopmentFields)assert.equal(evaluateImportExemption({...input,[key]:''},'MZN').importVat,null);
for(const patch of [{period:'2025-12'},{documentDate:'2026-02-30'},{currency:'USD'},{documentRef:''},{evidence:''}])assert.equal(evaluateImportExemption({...input,...patch},'MZN').importVat,null);
}
});
test('partial benefit is exact proportion of supplied VAT,not a VAT/customs rate or full exemption;missing is not zero',()=>{
const input=row(rules[1]);
for(const [normal,percent,expected] of [['800','25','600.00'],['800','75','200.00'],['1.234567','33.333333','0.82304467078189'],['999999999999999.999999','99.999999','9999999.99999999999999'],['0','50','0.00']]){
const r=evaluateImportExemption({...input,developmentNormalVat:normal,developmentReductionPercent:percent},'MZN');assert.equal(r.status,'reduced');assert.equal(r.importVat,expected);assert.ok(!r.reason.includes('%/100'));
}
for(const patch of [{developmentReductionPercent:''},{developmentReductionPercent:'-1'},{developmentReductionPercent:'101'},{developmentReductionPercent:'100'},{developmentReductionPercent:'0.0000001'},{developmentNormalVat:''},{developmentNormalVat:'-1'},{developmentNormalVat:'NaN'},{developmentNormalVatRef:''}])assert.equal(evaluateImportExemption({...input,...patch},'MZN').importVat,null);
assert.equal(evaluateImportExemption({...input,developmentReductionPercent:'0'},'MZN').status,'not_applicable');
const full=row(rules[0]);assert.equal(evaluateImportExemption({...full,developmentNormalVat:'',developmentReductionPercent:'50'},'MZN').importVat,'0.00','hidden stale partial fields do not override full-rights condition');
assert.equal(evaluateImportExemption({...full,treatment:rules[1].id},'MZN').status,'indeterminate','switch does not inherit own rights-reduction eligibility');
});
test('save/recovery/report keep77 delivered branches and partial P084/P085 exact;new contexts/history and reference edits isolated',()=>{
const d=draft(),prior=JSON.parse(d.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,developmentFavourableDecision:'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.importVat===null)));assert.ok(importExemptionCurrent(d,book));assert.match(book.result.sourceKey,/import-un-development\/1/);
for(const field of ['developmentDecisionRef','developmentListRef','developmentPlanRef','developmentScope','developmentRightsRef','developmentReductionPercent','developmentNormalVat'])assert.ok(!importExemptionCurrent(d,{...book,rows:book.rows.map(r=>({...r,[field]:'changed'}))}));
assert.ok(!importExemptionCurrent({...d,context_opportunity:'opportunity:339'},book));d.modelo_a_snapshot=saveImportExemptionBook(d,book);assert.deepEqual(importExemptionBook(JSON.parse(JSON.stringify(d))),book);const next=JSON.parse(d.modelo_a_snapshot);assert.deepEqual({...next,importExemptions:prior.importExemptions},prior);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.importExemptions['opportunity:337'].result.rows[0].importVat,'0.00');assert.equal(next.importExemptions['opportunity:336'].result.rows[0].serviceVat,'0.00');assert.equal(next.importExemptions['opportunity:336'].result.rows[0].importVat,null);assert.equal(next.books['opportunity:329'].result.periods[0].fields['11'],null);
const report=JSON.stringify(createProjectDossierReport(d,{selectedSections:new Set(['finance']),version:'QA G6H'}));for(const token of ['P060','12(2)(c)','12(4)','400.00','opportunity:338','Manica','2026-12','Lista discriminada aprovada','Plano de importações','Redução proporcional','campo11'])assert.ok(report.includes(token),token);
if(process.env.IVA_G6H_CALCULATED)fs.writeFileSync(process.env.IVA_G6H_CALCULATED,JSON.stringify(d));
});
test('optional fields retain documents when switching,while malformed references cannot overwrite saved originals',()=>{
const d=draft(),original=d.modelo_a_snapshot;
for(const key of [...importDevelopmentFields.map(([key])=>key),'developmentReductionPercent','developmentNormalVat','developmentNormalVatRef'])assert.throws(()=>saveImportExemptionBook(d,{rows:[{...row(rules[1]),[key]:42}]}),/Referências do projecto de desenvolvimento/);
assert.equal(d.modelo_a_snapshot,original);const changed={...row(rules[1]),treatment:'un_development_exemption'};d.modelo_a_snapshot=saveImportExemptionBook(d,{rows:[changed]});assert.equal(importExemptionBook(d).rows[0].developmentReductionPercent,'50');assert.equal(importExemptionBook(d).rows[0].developmentPlanRef,changed.developmentPlanRef);assert.equal(evaluateImportExemption(changed,'MZN').status,'indeterminate');
});