feat(iva): add G5A goods and connected services under special regimes

This commit is contained in:
2026-10-24 18:36:01 +02:00
parent 0eb2c485cc
commit 1f7fc13872
6 changed files with 164 additions and 1 deletions
@@ -4,6 +4,7 @@ import { pharmaAnnexEntries, pharmaAnnexEntry, pharmaAnnexReference } from '@/li
import { annexIEntries, annexIEntry, annexIReference } from '@/lib/iva-annex-i';
import { temporaryBenefit, temporaryBenefitNotice } from '@/lib/iva-temporary-benefits';
import { isExportService } from '@/lib/iva-export-services';
import { isSpecialRegime, specialRegimesFor, specialRegimeIssue, type SpecialRegime } from '@/lib/iva-special-regimes';
import { isOnboardSupply, supplyCategories, onboardSupplyIssue, type SupplyCategory } from '@/lib/iva-onboard-supplies';
import { emptyProductionTaxOption, type ProductionTaxOption } from '@/lib/iva-production-option';
import { referenceExportOperation, continueTemporaryOperation, continueAnnexIOperation, modeloA06Correspondence, newSalesOperation, operationTreatments, recordSales, reuseSales, salesBook, salesCurrent, salesReportRows, saveSalesBook, splitSales, type SalesBook, type SalesOperation } from '@/lib/iva-operations';
@@ -36,6 +37,11 @@ export function SimulationOperations({draft,onChange}:{draft:Record<string,unkno
<label>Representação do preço<select className="block rounded border p-2" value={row.basis} onChange={e=>change(row.id,{basis:e.target.value as SalesOperation['basis']})}><option value="">Ainda desconhecida</option><option value="net">Sem IVA</option><option value="included">IVA incluído</option></select></label>
<label>Tratamento da operação<select className="block rounded border p-2" value={row.treatment} onChange={e=>temporaryBenefit(row.treatment)&&!temporaryBenefit(e.target.value)?continueTemporary(row.id,e.target.value):change(row.id,{treatment:e.target.value,confirmed:[]})}>{operationTreatments.map(t=><option value={t.id} key={t.id}>{t.label}</option>)}</select></label>
</div>
{isSpecialRegime(row.treatment)&&<fieldset className="space-y-2 rounded border p-2"><legend>Regime aduaneiro ou fiscal da operação</legend>
<label>Regime do artigo14(1)<select value={specialRegimesFor(row.treatment).some(([value])=>value===row.specialRegime)?row.specialRegime:''} onChange={e=>change(row.id,{specialRegime:e.target.value as SpecialRegime,confirmed:[]})}><option value="">Por identificar</option>{specialRegimesFor(row.treatment).map(([value,label])=><option value={value} key={value}>{label}</option>)}</select></label>
<p>Identifique no fundamento os bens, a operação e o documento ou pressuposto expresso do regime. A escolha conserva preço, moeda e período e pede a revisão das condições; não comprova o enquadramento por estar numa zona. Bens e serviço de transporte ou serviço conexo têm preços próprios, sem duplicar o valor.</p>
{specialRegimeIssue(row.treatment,row.specialRegime)&&<p role="status">{specialRegimeIssue(row.treatment,row.specialRegime)}</p>}
</fieldset>}
{isOnboardSupply(row.treatment)&&<fieldset className="space-y-2 rounded border p-2"><legend>Natureza do abastecimento a bordo</legend>
<label>Categoria do artigo13(3)<select value={row.supplyCategory??''} onChange={e=>change(row.id,{supplyCategory:e.target.value as SupplyCategory,confirmed:[]})}><option value="">Por identificar</option>{supplyCategories.map(([value,label])=><option key={value} value={value}>{label}</option>)}</select></label>
<p>A escolha conserva o preço, a moeda e o período; reveja as condições. Para fornecimento misto, discrimine as parcelas efectivas sem repetir o total em cada linha. Desconhecido não equivale a «sem bebidas».</p>
+6 -1
View File
@@ -6,6 +6,7 @@ import { productionOptionCheck, productionOptionVersion, type ProductionTaxOptio
import { temporaryBenefits, temporaryBenefit, temporaryBenefitNotice, temporaryBenefitVersion } from './iva-temporary-benefits.ts';
import { directExportTreatments } from './iva-export-operations.ts';
import { exportBeneficiaryTreatments } from './iva-export-beneficiaries.ts';
import { specialRegimeTreatments, specialRegimeIssue, specialRegimeOptions, isSpecialRegime, type SpecialRegime } from './iva-special-regimes.ts';
import { internationalAssetTreatments } from './iva-international-assets.ts';
import { exportServiceTreatments, isExportService } from './iva-export-services.ts';
import { onboardSupplyTreatments, onboardSupplyIssue, supplyCategories, isOnboardSupply, type SupplyCategory } from './iva-onboard-supplies.ts';
@@ -68,6 +69,7 @@ export const operationTreatments:OperationTreatment[] = [
...internationalAssetTreatments,
...directExportTreatments,
...exportBeneficiaryTreatments,
...specialRegimeTreatments,
...temporaryBenefits.map(item=>({id:item.id,label:`Benefício terminado31/12/2025 — ${item.label}`,field:null,rate:null,article:`CIVA art.${item.article}`,source:`${item.source} CIVA republicado Lei13/2016,9(14),PDF10/p.1426(268),apenas para9(13)(b/f). Âmbito:cessação nas operações internas2026/MZN;sem cálculo histórico.`,conditions:[],logicVersion:temporaryBenefitVersion})),
{id:'annexI_goods',label:'Equipamento, sementes e outros bens do Anexo I — artigo9(12)(h)',field:'07',rate:0,logicVersion:annexIVersion,article:'CIVA arts.9(12)(h),19(1); Lei22/2022 art.3(1)',source:`CIVA republicado Lei13/2016, PDF10, p.1426(268),9(12)(h), e PDF15, p.1426(273),19(1); rectificação BR89/2017, PDF1, p.569; ${annexISource}. Âmbito técnico2026/MZN`,conditions:[internalOperation,'O bem transmitido corresponde ao código e à designação da posição escolhida no Anexo I; não basta a actividade agrícola, a marca ou uma indicação genérica de equipamento','Qualificadores expressos na posição conferidos e fundamentados, incluindo natureza, destino, certificação, peso, capacidade ou potência quando exigidos nessa linha; não acrescentar condições a linhas que não as exigem']},
{id:'pharma_manufacturing_inputs',label:'Bens do Anexo III para fabrico de medicamentos — estabelecimento integrado no SNS',field:'07',rate:0,logicVersion:pharmaAnnexVersion,article:'CIVA arts.9(1)(g),19(1); Lei22/2022 art.3(1)',source:`CIVA republicado Lei13/2016, PDF9, p.1426(267),9(1)(g), e PDF15, p.1426(273),19(1); rectificação BR89/2017, PDF1, p.569; ${pharmaAnnexSource}. Âmbito técnico2026/MZN`,conditions:[internalOperation,'Transmissão efectuada pelo próprio estabelecimento público ou privado identificado, integrado no Serviço Nacional de Saúde; não basta que o adquirente ou destino seja um hospital','Bens a utilizar como matérias-primas, produtos intermédios ou componentes para o fabrico de medicamentos, com utilização/fundamento identificados; não mera venda de produto acabado ou utilização genérica','Bem corresponde cumulativamente ao código e à designação da linha seleccionada no Anexo III; confirmar as especificações aplicáveis sem corrigir ou completar a pauta por suposição']},
@@ -126,7 +128,7 @@ export const operationTreatments:OperationTreatment[] = [
{id:'foreign_operation',label:'Operação no estrangeiro — analisar territorialidade',field:null,rate:null,article:'CIVA art.6: localização por natureza da operação; art.19(1)(b)(ii) não determina sozinho a caixa06 do formulário',source:source2016,conditions:[]},
{id:'other',label:'Outra operação / enquadramento ainda desconhecido',field:null,rate:null,article:'Fundamento específico ainda necessário; não presumir taxa ou isenção',source:'Sem fonte operacional determinada para esta linha',conditions:[]},
];
export type SalesOperation = {id:string;period:string;description:string;establishment:string;amount:string;basis:''|'net'|'included';treatment:string;confirmed:number[];evidence:string;sourceAmount:string|null;sourcePeriod:string;sourceCurrency:string;source:string;taxOption?:ProductionTaxOption;fundraiser?:FundraisingContext;pharmaAnnexItem?:string;annexIItem?:string;annexIOnly?:boolean;temporaryBenefitOrigin?:string;relatedExport?:{id:string;description:string;treatment:string};supplyCategory?:SupplyCategory; splitSource?:{id:string;amount:string|null;period:string;currency:string;basis:string}};
export type SalesOperation = {id:string;period:string;description:string;establishment:string;amount:string;basis:''|'net'|'included';treatment:string;confirmed:number[];evidence:string;sourceAmount:string|null;sourcePeriod:string;sourceCurrency:string;source:string;taxOption?:ProductionTaxOption;fundraiser?:FundraisingContext;pharmaAnnexItem?:string;annexIItem?:string;annexIOnly?:boolean;temporaryBenefitOrigin?:string;relatedExport?:{id:string;description:string;treatment:string};supplyCategory?:SupplyCategory;specialRegime?:SpecialRegime; splitSource?:{id:string;amount:string|null;period:string;currency:string;basis:string}};
export type SalesBook = {currency:string;rows:SalesOperation[];periods:Array<{period:string;complete:boolean}>; result?:SalesResult; history?:SalesResult[]};
export type SalesLineResult = {id:string;field:string|null;net:string|null;vat:string|null;gross:string|null;rate:number|null;status:'calculated'|'exempt'|'indeterminate';reason:string;article:string;source:string;exact?:{net:FiscalFraction;vat:FiscalFraction;gross:FiscalFraction}};
export type SalesResult = {version:string;revision:number;calculatedAt:string;opportunity:string;location:string;currency:string;key:string;inputs:SalesOperation[];rows:SalesLineResult[];periods:Array<{period:string;fields:Record<string,string|null>;exactFields?:Record<string,FiscalFraction|null>;reasons:string[]}>;rounding:string};
@@ -186,6 +188,8 @@ export function evaluateSalesOperation(row:SalesOperation,currency:string):Sales
if(currency!=='MZN'||row.sourceCurrency!==currency)return stop('Moeda fora de MZN ou divergente da origem; sem conversão silenciosa.');
if(!/^2026-(0[1-9]|1[0-2])$/.test(row.period))return stop('Período fora da cobertura de2026; sem cálculo histórico presumido.');
if(temporaryBenefit(row.treatment))return stop(temporaryBenefitNotice(row.treatment,row.period));
const regimeIssue=specialRegimeIssue(row.treatment,row.specialRegime);
if(regimeIssue)return {...stop(regimeIssue),field:null,rate:null};
const supplyIssue=onboardSupplyIssue(row.treatment,row.supplyCategory);
if(supplyIssue)return {...stop(supplyIssue),field:null,rate:null};
if(!rule||rule.rate===null||!rule.field)return stop('Operação não calculada: delimite incidência, territorialidade e fundamento desta linha. A remissão do campo06 não constitui uma regra de classificação de todas as operações no estrangeiro.');
@@ -263,6 +267,7 @@ export function salesReportRows(draft:Record<string,unknown>):Array<[string,stri
for(const row of r.inputs.filter(v=>v.fundraiser))out.push(['IVA mensal',`Manifestação ocasional — ${row.description}`,`Entidade ${row.fundraiser!.entityReference}; ano ${row.period.slice(0,4)}; total anual declarado ${row.fundraiser!.annualEvents}; manifestação ${row.fundraiser!.eventNumber}; actividade ${row.fundraiser!.activity}`,`${context}; dados declarados para a entidade, incluindo manifestações fora deste dossier; não é contagem automática das vendas.`]);
for(const row of r.inputs.filter(v=>isExportService(v.treatment)&&v.relatedExport))out.push(['IVA mensal',`Operação relacionada — ${row.description}`,`${row.relatedExport!.description}; ID ${row.relatedExport!.id}; tratamento ${row.relatedExport!.treatment}`,`${context}; referência escolhida expressamente nesta oportunidade,sem copiar valor para preço do serviço nem provar automaticamente o enquadramento subjacente`]);
for(const row of r.inputs.filter(v=>isOnboardSupply(v.treatment)))out.push(['IVA mensal',`Natureza do abastecimento — ${row.description}`,supplyCategories.find(([value])=>value===row.supplyCategory)?.[1]??'Por identificar',`${context}; CIVA13(2–3); natureza conservada no cálculo/relatório, sem aplicar dedução ou restituição automaticamente`]);
for(const row of r.inputs.filter(v=>isSpecialRegime(v.treatment)))out.push(['IVA mensal',`Regime especial — ${row.description}`,specialRegimeOptions.find(([value])=>value===row.specialRegime)?.[1]??'Por identificar',`${context}; CIVA14(1)(b/c/d),19(1)(b)(iv); regime declarado/documentado ou pressuposto expresso da projecção; não certifica admissão ou permanência, nem liquida importação ou saída do regime`]);
for(const value of r.rows.filter(v=>v.exact))out.push(['IVA mensal',`Precisão exacta — ${value.id}`,`Líquido ${fractionLabel(value.exact!.net)} MZN; IVA ${fractionLabel(value.exact!.vat)} MZN; bruto ${fractionLabel(value.exact!.gross)} MZN`,`${context}; apresentação decimal a6casas, aproximação quando a fracção não termina; não aplicar à declaração sem a sua regra própria.`]);
for(const p of r.periods){out.push(['IVA mensal',`Operações L1 — ${p.period}`,Object.entries(p.fields).map(([f,v])=>`${f}: ${v??'Indeterminado'}`).join('; '),`${context}; ${p.reasons.join(' ')}`]);
if(p.exactFields)out.push(['IVA mensal',`Precisão exacta do período — ${p.period}`,Object.entries(p.exactFields).map(([f,v])=>`${f}: ${v?fractionLabel(v):'Indeterminado'}`).join('; '),`${context}; totais em fracções MZN, calculados antes da apresentação decimal, sem regra declarativa presumida.`]);
+58
View File
@@ -0,0 +1,58 @@
import type { OperationTreatment } from './iva-operations.ts';
export const specialRegimeOptions = [
['economic_zone', 'Zona económica especial'],
['free_zone', 'Zona franca'],
['free_warehouse', 'Depósito franco'],
['general_free_warehouse', 'Depósito geral franco'],
['customs_warehouse', 'Depósito de regime aduaneiro'],
['duty_free_shop', 'Loja franca'],
['transit', 'Trânsito'],
['drawback', 'Draubaque'],
['temporary_import', 'Importação temporária'],
] as const;
export type SpecialRegime = '' | typeof specialRegimeOptions[number][0];
const zoneRegimes: readonly string[] = specialRegimeOptions.slice(0,6).map(([id])=>id);
const transitRegimes: readonly string[] = ['transit','drawback','temporary_import'];
const source = 'CIVA republicado Lei13/2016, BR156/10.º suplemento,14(1)(b/c/d) e condição inicial de14(1),PDF13/p.1426(271);19(1)(b)(iv),PDF15/p.1426(273). Lei22/2022art.1,PDF1, e Lei10/2025art.1,PDF12–13/p.2180(12)–(13),não alteram14 nem19(1)(b)(iv). Âmbito2026/MZN; não calcula importação14(1)(a), aquisição14(2), saída de regime, direitos aduaneiros ou dedução automática de compras.';
const make = (id:string,label:string,letter:string,conditions:string[]):OperationTreatment => ({id,label,field:'05',rate:0,article:`CIVA arts.14(1)(${letter}),19(1)(b)(iv)`,source,logicVersion:'special-regimes/1',conditions:[
'Transmissão de bens ou prestação de serviços abrangida pelo CIVA em Moçambique, com territorialidade identificada; não o próprio acto de importação nem aquisição pelo beneficiário',
'Os bens a que a operação se refere não têm utilização nem consumo finais nas áreas mencionadas em14(1); destino efectivo ou pressuposto expresso da projecção identificado, sem o presumir pela localização no mapa',
...conditions,
]});
export const specialRegimeTreatments:OperationTreatment[] = [
make('regime_dispatch_goods','Bens expedidos para zona ou depósito elegível','b',[
'Transmissão de bens expedidos ou transportados para zona ou depósito mencionado em14(1)(a); bens, destino e regime identificados no documento ou pressuposto expresso. A indicação de loja franca por si não comprova esse destino: identifique a zona ou depósito aplicável a esta expedição',
'A colocação/introdução dos bens corresponde ao regime escolhido, sob controlo alfandegário e disposições especificamente aplicáveis de14(1)(a); não basta o endereço do destinatário ou o cadastro da zona',
]),
make('regime_dispatch_transport','Transporte directamente conexo à expedição para zona ou depósito','b',[
'Prestação de serviço de transporte directamente conexa à transmissão de bens expedidos ou transportados para zona ou depósito de14(1)(a); transmissão, bens, destino e ligação concreta identificados. A indicação de loja franca por si não comprova esse destino: identifique a zona ou depósito aplicável a esta expedição',
'Regime de destino e disposições especificamente aplicáveis de14(1)(a) identificados; preço desta linha é apenas o transporte, sem duplicar o valor dos bens; não qualquer serviço ao destinatário',
]),
make('regime_internal_goods','Transmissão de bens dentro de regime de zona ou depósito','c',[
'Transmissão de bens efectuada num regime de14(1)(a),com bens, regime e controlo/disposições aplicáveis identificados no documento ou pressuposto expresso',
'Os bens permanecem nessa situação no momento/período da operação; não saída do regime nem venda para consumo final na área',
]),
make('regime_internal_services','Serviço directamente conexo à transmissão de bens em regime','c',[
'Prestação de serviços directamente conexa a transmissão de bens efectuada em regime de14(1)(a); bens, transmissão, regime e ligação concreta identificados, não qualquer serviço executado na zona',
'Os bens permanecem nessa situação no momento/período da operação; preço desta linha é apenas o serviço, sem duplicar o valor dos bens; não saída do regime nem consumo final',
]),
make('regime_transit_goods','Bens em trânsito, draubaque ou importação temporária','d',[
'Transmissão de bens que se encontram no regime escolhido de trânsito, draubaque ou importação temporária; bens e enquadramento identificados no documento ou pressuposto expresso, não o próprio acto de importação',
'A operação continua considerada abrangida pelo regime no momento/período indicado; não regime terminado nem mera passagem geográfica dos bens',
]),
make('regime_transit_transport','Transporte directamente conexo a bens em trânsito, draubaque ou importação temporária','d',[
'Prestação de serviço de transporte directamente conexa à transmissão de bens em trânsito, draubaque ou importação temporária; bens, operação e ligação concreta identificados',
'A operação continua abrangida pelo regime no momento/período indicado; preço desta linha é apenas o transporte, sem duplicar o valor dos bens; não transporte genérico nem regime terminado',
]),
];
export function isSpecialRegime(id:string):boolean { return specialRegimeTreatments.some(rule=>rule.id===id); }
export function specialRegimesFor(id:string):Array<typeof specialRegimeOptions[number]> {
const allowed=id.startsWith('regime_transit_')?transitRegimes:zoneRegimes;
return isSpecialRegime(id)?specialRegimeOptions.filter(([value])=>allowed.includes(value)):[];
}
export function specialRegimeIssue(id:string,regime:unknown):string|null {
if(!isSpecialRegime(id))return null;
if(!specialRegimesFor(id).some(([value])=>value===regime))return 'Identifique o regime aplicável a esta operação do artigo14(1). Regime desconhecido ou de outra alínea não comprova isenção; a localização não o substitui. Conserve preço, moeda e período; apuramento indeterminado.';
return null;
}
@@ -0,0 +1,45 @@
<?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 IvaSpecialRegimeDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_special_regimes_recover_with_the_selected_opportunity_and_previous_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('outputs/iva-l1-g5-regimes-20261024/calculated.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:328', $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['salesOperations']['opportunity:328'];
$this->assertSame('opportunity:328', $book['result']['opportunity']);
$this->assertSame('MZN', $book['result']['currency']);
$this->assertSame('2026-12', $book['result']['periods'][0]['period']);
$this->assertSame('600.00', $book['result']['periods'][0]['fields']['05']);
$this->assertCount(6, $book['rows']);
$this->assertSame(['economic_zone','customs_warehouse','free_zone','duty_free_shop','transit','temporary_import'], array_column($book['rows'], 'specialRegime'));
foreach ($book['result']['rows'] as $row) {
$this->assertSame('05', $row['field']);
$this->assertSame('0.00', $row['vat']);
}
$frozen = json_decode(file_get_contents(base_path('tests/frontend/fixtures/iva-g4e-inputs.json')), true, flags: JSON_THROW_ON_ERROR);
foreach (json_decode($frozen['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR)['salesOperations'] as $context => $original) {
$this->assertSame($original, $store['salesOperations'][$context]);
}
file_put_contents(base_path('outputs/iva-l1-g5-regimes-20261024/recovered.json'), json_encode($recovered, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,48 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { specialRegimeTreatments as rules, specialRegimeOptions, specialRegimesFor, specialRegimeIssue } from '../../resources/js/lib/iva-special-regimes.ts';
import { newSalesOperation, evaluateSalesOperation, recordSales, saveSalesBook, salesBook, salesCurrent } from '../../resources/js/lib/iva-operations.ts';
import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts';
const frozen=JSON.parse(fs.readFileSync(new URL('./fixtures/iva-g4e-inputs.json',import.meta.url),'utf8'));
const regimes=['economic_zone','customs_warehouse','free_zone','duty_free_shop','transit','temporary_import'];
const row=(rule,i)=>({...newSalesOperation(`g5a-${i}`,'2026-12','MZN'),description:`SYNTHETIC QA — ${rule.label}`,establishment:'SYNTHETIC QA estabelecimento',amount:'100',basis:i%2?'included':'net',treatment:rule.id,specialRegime:regimes[i],confirmed:rule.conditions.map((_,n)=>n),evidence:'SYNTHETIC QA — pressuposto expresso de projecção: bens e operação identificados; regime e controlo/documento, destino, permanência no período, ausência de utilização/consumo final e ligação directa do serviço/transporte identificados. Cada linha tem preço próprio, sem duplicar bens. Sem certificação real.'});
function fixture(){
const draft={...structuredClone(frozen),context_opportunity:'opportunity:328',project_title:'SYNTHETIC QA — G5A / Regimes especiais'};
const book=recordSales(draft,{currency:'MZN',rows:rules.map(row),periods:[{period:'2026-12',complete:true}]});draft.modelo_a_snapshot=saveSalesBook(draft,book);return{draft,book};
}
test('three branches/six goods-service treatments require own conditions,no final use and identified regime',()=>{
assert.equal(rules.length,6);
for(const[i,rule]of rules.entries()){
const input=row(rule,i),result=evaluateSalesOperation(input,'MZN');assert.equal(result.field,'05');assert.equal(result.vat,'0.00');assert.equal(result.net,'100.00');assert.equal(result.gross,'100.00');assert.match(result.article,/19\(1\)\(b\)\(iv\)/);
for(const missing of input.confirmed)assert.equal(evaluateSalesOperation({...input,confirmed:input.confirmed.filter(n=>n!==missing)},'MZN').vat,null);
for(const patch of [{amount:''},{evidence:''},{sourceCurrency:'USD'},{period:'2027-01'},{basis:''},{description:''},{establishment:''}])assert.equal(evaluateSalesOperation({...input,...patch},'MZN').vat,null);
assert.match(rule.conditions.join(' '),/não têm utilização nem consumo finais/);
}
});
test('regimes from another paragraph or unknown are indeterminate;destination and service conditions remain distinct',()=>{
for(const[i,rule]of rules.entries()){
for(const[regime]of specialRegimeOptions){const result=evaluateSalesOperation({...row(rule,i),specialRegime:regime},'MZN');const allowed=specialRegimesFor(rule.id).some(([id])=>id===regime);assert.equal(result.vat,allowed?'0.00':null);if(!allowed){assert.equal(result.net,null);assert.equal(result.field,null);}}
for(const regime of [undefined,'',null,'other'])assert.equal(evaluateSalesOperation({...row(rule,i),specialRegime:regime},'MZN').vat,null);
}
assert.match(rules[0].conditions.join(' '),/loja franca por si não comprova/);
assert.equal(evaluateSalesOperation({...row(rules[0],0),specialRegime:'duty_free_shop',confirmed:[0,1,3]},'MZN').vat,null);
assert.equal(specialRegimeIssue('regime_internal_goods','duty_free_shop'),null);
assert.equal(specialRegimeIssue('general',undefined),null);assert.deepEqual(specialRegimesFor('general'),[]);
});
test('change of regime or ending eligibility preserves price,context/history and blocks presumptive totals',()=>{
const{draft,book}=fixture();assert.equal(book.result.periods[0].fields['05'],'600.00');
for(const patch of [{specialRegime:'transit',confirmed:[]},{specialRegime:'',confirmed:[]},{confirmed:[],evidence:'Regime terminado; rever enquadramento'}]){
const changed=recordSales(draft,{...book,rows:book.rows.map((r,i)=>i===0?{...r,...patch}:r)});
assert.equal(changed.rows[0].amount,'100');assert.equal(changed.rows[0].sourceCurrency,'MZN');assert.equal(changed.rows[0].period,'2026-12');assert.equal(changed.result.periods[0].fields['05'],null);assert.deepEqual(changed.history.at(-1),book.result);
const edited={...draft,modelo_a_snapshot:saveSalesBook(draft,{...book,rows:changed.rows})};assert.equal(salesCurrent(edited,salesBook(edited)),false);
}
});
test('snapshot/report retain regime,source and assumptions;all19 previous books are identical/current without recalculation',()=>{
const{draft,book}=fixture();assert.ok(salesCurrent(draft,book));assert.deepEqual(salesBook(JSON.parse(JSON.stringify(draft))),JSON.parse(JSON.stringify(book)));
const before=JSON.parse(frozen.modelo_a_snapshot).salesOperations,after=JSON.parse(draft.modelo_a_snapshot).salesOperations;assert.equal(Object.keys(before).length,19);
for(const[context,original]of Object.entries(before)){assert.deepEqual(after[context],original);assert.ok(salesCurrent({...draft,context_opportunity:context},after[context]));}
const report=JSON.stringify(createProjectDossierReport(draft,{selectedSections:new Set(['finance']),version:'QA G5A'}));
for(const token of ['opportunity:328','Manica','2026-12','600.00','14(1)(b)','14(1)(c)','14(1)(d)','19(1)(b)(iv)','Regime especial','Importação temporária','Loja franca','pressuposto expresso','QA G5A'])assert.ok(report.includes(token),token);
if(process.env.IVA_G5A_CALCULATED)fs.writeFileSync(process.env.IVA_G5A_CALCULATED,JSON.stringify(draft));
});