feat(iva): classify goods territoriality for L1 P105 in QA

This commit is contained in:
2026-10-24 23:29:03 +02:00
parent 21ce03f65c
commit 0682ef74e2
8 changed files with 193 additions and 0 deletions
@@ -0,0 +1,34 @@
import {useState} from 'react';
import {newGoodsLocation,switchGoodsTreatment,goodsLocationBook,goodsLocationConditions,ownGoodsConditions,goodsLocationCurrent,goodsLocationReportRows,goodsLocationSource,goodsLocationTreatments,recordGoodsLocations,saveGoodsLocationBook,type GoodsLocationBook,type GoodsLocationRow} from '@/lib/iva-goods-location';
export function SimulationGoodsLocation({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}){
const[error,setError]=useState('');let book:GoodsLocationBook;
try{book=goodsLocationBook(draft);}catch(cause){return<p role="alert">{String(cause)}</p>;}
const save=(next:GoodsLocationBook)=>{try{onChange('modelo_a_snapshot',saveGoodsLocationBook(draft,next));setError('');}catch(cause){setError(String(cause));}};
const change=(id:string,patch:Partial<GoodsLocationRow>)=>save({...book,rows:book.rows.map(row=>row.id===id?{...row,...patch}:row)});
return<details className="space-y-3 rounded border p-3">
<summary>Localização — transmissões de bens e antes da importação</summary>
<p>Oportunidade: {String(draft.context_opportunity||'entrada geral')}. Identifique os bens e os factos da transmissão. Com transporte,indique onde se encontram ao iniciá-lo;sem transporte,onde são postos à disposição. O endereço ou nacionalidade do cliente não substituem estes factos.</p>
<p>Na transmissão pelo importador ou subsequente antes da importação nacional de bens vindos do estrangeiro,confira a regra própria6(2). A localização física anterior no estrangeiro não a afasta por si. Fora do âmbito territorial não equivale a isenção ou IVA zero;esta classificação não calcula imposto.</p>
<p>{goodsLocationSource}</p>{error&&<p role="alert">{error}</p>}
<button type="button" className="underline" onClick={()=>save({...book,rows:[...book.rows,newGoodsLocation(crypto.randomUUID())]})}>Acrescentar localização de transmissão</button>
{book.rows.map(row=><details key={row.id} className="rounded border p-3"><summary>{row.description||'Operação por identificar'} — {row.period||'mês por indicar'}</summary>
<div className="grid gap-3 md:grid-cols-2">
<label>Percurso de localização dos bens<select className="block rounded border p-2" value={row.treatment} onChange={e=>change(row.id,switchGoodsTreatment(row,e.target.value))}><option value="">Por indicar</option>{goodsLocationTreatments.map(t=><option key={t.id} value={t.id}>{t.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 GoodsLocationRow['origin']})}><option value="projected">Projectados / pressupostos</option><option value="effective">Efectivos declarados</option></select></label>
{([['description','Bens e transmissão identificados'],['operationRef','Referência da operação / pressuposto'],['operationDate','Data da transmissão / prevista'],['period','Mês da transmissão'],['documentRef','Documento da operação / referência da previsão'],['documentDate','Data do documento / prevista'],['scope','Âmbito: bens, operação, período e condições'],['evidence','Fundamento factual / pressupostos']] as const).map(([field,label])=><label key={field}>{label}<input className="block rounded border p-2" type={['operationDate','documentDate'].includes(field)?'date':field==='period'?'month':'text'} value={row[field]} onChange={e=>change(row.id,{[field]:e.target.value})}/></label>)}
{row.treatment&&row.treatment!=='goods_before_import'&&<>
<label>{row.treatment==='goods_with_transport'?'Local dos bens no início do transporte/expedição':'Local dos bens quando postos à disposição sem transporte'}<input className="block rounded border p-2" value={row.goodsPlace} onChange={e=>change(row.id,{goodsPlace:e.target.value})}/></label>
<label>Situação desse local face ao território nacional<select className="block rounded border p-2" value={row.placeCountry} onChange={e=>change(row.id,{placeCountry:e.target.value})}><option value="">Por indicar</option><option value="MZ">Em Moçambique</option><option value="foreign">Fora de Moçambique</option></select></label>
<label>Referência da localização dos bens / pressuposto<input className="block rounded border p-2" value={row.placeRef} onChange={e=>change(row.id,{placeRef:e.target.value})}/></label>
</>}
{['goods_with_transport','goods_before_import'].includes(row.treatment)&&<label>Referência do transporte/expedição / pressuposto<input className="block rounded border p-2" value={row.transportRef} onChange={e=>change(row.id,{transportRef:e.target.value})}/></label>}
{row.treatment==='goods_before_import'&&<><label>Referência da importação nacional / pressuposto<input className="block rounded border p-2" value={row.importRef} onChange={e=>change(row.id,{importRef:e.target.value})}/></label><label>Importador/cadeia e posição desta transmissão<input className="block rounded border p-2" value={row.chainRef} onChange={e=>change(row.id,{chainRef:e.target.value})}/></label></>}
</div>
<p>Mudar percurso conserva os locais e referências,mas exige rever os factos próprios. Campos sem aplicação ficam ocultos e conservados.</p>
<fieldset><legend>Factos próprios deste percurso</legend>{[...goodsLocationConditions,...ownGoodsConditions(row.treatment)].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 as ''|'yes'|'no'}})}><option value="">Por indicar</option><option value="yes">Sim</option><option value="no">Não</option></select></label>)}</fieldset>
</details>)}
{!!book.rows.length&&<button type="button" className="rounded bg-emerald-800 p-2 text-white" onClick={()=>{try{save(recordGoodsLocations(draft,book));}catch(cause){setError(String(cause));}}}>Classificar localização e guardar versão</button>}
{book.result&&<><p role="status">{goodsLocationCurrent(draft,book)?'Localizações correspondentes às entradas guardadas.':'Entradas alteradas;localizações históricas conservadas. Reveja e classifique novamente.'} {book.history?.length??0} versões anteriores. Classificação não determina isenção,IVA ou campo11.</p><dl>{goodsLocationReportRows(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>;
}
@@ -15,6 +15,7 @@ import { SimulationCompositionBase } from './simulation-composition-base';
import { SimulationExclusionBase } from './simulation-exclusion-base';
import { SimulationValueReview } from './simulation-value-review';
import { SimulationServiceNormal } from './simulation-service-normal';
import { SimulationGoodsLocation } from './simulation-goods-location';
import { SimulationOfferPreparations } from './simulation-offer-preparations';
import { SimulationOperations } from './simulation-operations';
@@ -108,6 +109,7 @@ export function SimulationMonthlyIva({draft, busy, onCalculate, onChange}: {
<SimulationExclusionBase draft={draft} onChange={onChange} />
<SimulationValueReview draft={draft} onChange={onChange} />
<SimulationServiceNormal draft={draft} onChange={onChange} />
<SimulationGoodsLocation draft={draft} onChange={onChange} />
<SimulationOfferPreparations draft={draft} onChange={onChange} />
<SimulationModeloA draft={draft} />
</section>;
+61
View File
@@ -0,0 +1,61 @@
import {acquisitionScope,readAcquisitionStore} from './iva-acquisitions.ts';
export const goodsLocationVersion='goods-territoriality/1';
export const goodsLocationSource='CIVA republicado Lei13/2016,6(1)/(2),PDF7/p1426(265):localização dos bens no território nacional ao iniciar transporte/expedição para o adquirente ou,sem transporte/expedição,ao pôr à sua disposição. Não obstante6(1),são também tributáveis transmissões pelo importador e subsequentes de bens transportados/expedidos do estrangeiro,antes da importação. Lei10/2025PDF12mantém6(1)/(2),altera6(7–9)quanto a digitais;recorte2026,bens corpóreos,não digitais. Endereço/nacionalidade do cliente não substituem localização dos bens. Classificação territorial apenas,não isenção/taxa/IVA/dedução/campo11/direitos aduaneiros ou totaisModeloA.';
export const goodsLocationTreatments=[{id:'goods_with_transport',label:'Bens com transporte ou expedição para o adquirente'},{id:'goods_without_transport',label:'Bens sem transporte ou expedição'},{id:'goods_before_import',label:'Transmissão pelo importador ou subsequente,antes da importação'}] as const;
export const goodsLocationConditions=[
['goodsSupply','Está identificada uma transmissão de bens corpóreos,não uma prestação de serviços ou bens digitais abrangidos por regras próprias'],
['factsCorrespond','Os factos,referências e âmbito correspondem a esta operação e período,não apenas ao endereço ou nacionalidade do adquirente'],
] as const;
export const goodsOwnConditions={
goods_with_transport:[['transportToBuyer','Os bens são transportados ou expedidos para o adquirente'],['notBeforeImport','Não corresponde à transmissão pré-importação abrangida pelo artigo6(2)']],
goods_without_transport:[['noTransport','Não há expedição ou transporte dos bens para esta transmissão'],['notBeforeImport','Não corresponde à transmissão pré-importação abrangida pelo artigo6(2)']],
goods_before_import:[['fromAbroad','Os bens são transportados ou expedidos do estrangeiro'],['nationalImport','Está identificada a importação desses bens para o território nacional'],['importerChain','A transmissão é feita pelo importador ou é subsequente na cadeia desses bens'],['beforeImport','Esta transmissão ocorre antes da importação identificada']],
} as const;
export function ownGoodsConditions(treatment:string):ReadonlyArray<readonly[string,string]>{return goodsOwnConditions[treatment as keyof typeof goodsOwnConditions]??[];}
type Choice=''|'yes'|'no';type Condition=typeof goodsLocationConditions[number][0];
export type GoodsLocationRow={id:string;treatment:string;description:string;period:string;origin:'projected'|'effective';operationDate:string;operationRef:string;documentRef:string;documentDate:string;goodsPlace:string;placeCountry:string;placeRef:string;transportRef:string;importRef:string;chainRef:string;scope:string;evidence:string;conditions:Partial<Record<string,Choice>>};
type LineResult={id:string;status:'determined'|'not_applicable'|'indeterminate';territory:'national'|'outside'|null;baseAmount:null;vat:null;rate:null;reason:string};
export type GoodsLocationCalculation={version:string;revision:number;calculatedAt:string;opportunity:string;location:string;sourceKey:string;source:string;inputs:GoodsLocationRow[];rows:LineResult[]};
export type GoodsLocationBook={rows:GoodsLocationRow[];result?:GoodsLocationCalculation;history?:GoodsLocationCalculation[]};
type Store=ReturnType<typeof readAcquisitionStore>&{goodsLocations?:Record<string,GoodsLocationBook>};
export function newGoodsLocation(id:string):GoodsLocationRow{return{id,treatment:'',description:'',period:'',origin:'projected',operationDate:'',operationRef:'',documentRef:'',documentDate:'',goodsPlace:'',placeCountry:'',placeRef:'',transportRef:'',importRef:'',chainRef:'',scope:'',evidence:'',conditions:{}};}
export function switchGoodsTreatment(row:GoodsLocationRow,treatment:string):GoodsLocationRow{return{...row,treatment,conditions:Object.fromEntries(goodsLocationConditions.map(([key])=>[key,row.conditions[key]??'']))};}
function readStore(draft:Record<string,unknown>):Store{
const store=readAcquisitionStore(draft.modelo_a_snapshot) as Store;
if(store.goodsLocations!==undefined&&(!store.goodsLocations||typeof store.goodsLocations!=='object'||Array.isArray(store.goodsLocations)))throw new Error('Bases de contraprestação não recuperáveis;originais preservados.');
const fields=Object.keys(newGoodsLocation('')).filter(key=>key!=='conditions');
for(const book of Object.values(store.goodsLocations??{}))if(!book||!Array.isArray(book.rows)||(book.history!==undefined&&!Array.isArray(book.history))||book.rows.some(row=>!row||fields.some(key=>typeof row[key as keyof GoodsLocationRow]!=='string')||!row.conditions||typeof row.conditions!=='object'||Array.isArray(row.conditions)))throw new Error('Linhas de contraprestação não recuperáveis;originais preservados,sem substituição por lista vazia.');
return store;
}
export function goodsLocationBook(draft:Record<string,unknown>):GoodsLocationBook{return readStore(draft).goodsLocations?.[acquisitionScope(draft)]??{rows:[]};}
export function saveGoodsLocationBook(draft:Record<string,unknown>,book:GoodsLocationBook):string{const current=readStore(draft),encoded=JSON.stringify({...current,goodsLocations:{...current.goodsLocations,[acquisitionScope(draft)]:book}});readStore({...draft,modelo_a_snapshot:encoded});return encoded;}
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 evaluateGoodsLocation(row:GoodsLocationRow):LineResult{
const result:LineResult={id:row.id,status:'indeterminate',territory:null,baseAmount:null,vat:null,rate:null,reason:''};const stop=(reason:string,status:LineResult['status']='indeterminate')=>({...result,status,reason});
const rule=goodsLocationTreatments.find(t=>t.id===row.treatment);if(!rule)return stop('Identifique transporte,ausência de transporte ou transmissão antes da importação;desconhecido não determina território.');
if(!/^2026-(0[1-9]|1[0-2])$/.test(row.period)||!['projected','effective'].includes(row.origin)||!validDate(row.operationDate)||row.operationDate.slice(0,7)!==row.period)return stop('Identifique mês em2026 e data da operação correspondentes,com dados efectivos declarados ou pressupostos expressos de projecção.');
const required=[...goodsLocationConditions,...ownGoodsConditions(row.treatment)];const failed=required.filter(([key])=>row.conditions[key]==='no');if(failed.length)return stop('Este percurso não se aplica: '+failed.map(([,label])=>label).join('; ')+'. Rever o percurso,não classificar automaticamente como fora do território ou isento.','not_applicable');
if(required.some(([key])=>row.conditions[key]!=='yes'))return stop('Confirme os factos próprios deste percurso;desconhecido não é fora do território,isenção ou IVA zero.');
if(!row.description.trim()||!row.operationRef.trim()||!row.documentRef.trim()||!validDate(row.documentDate)||!row.scope.trim()||!row.evidence.trim())return stop('Identifique bens/operação,documento/data,âmbito e factos ou pressupostos expressos;reutilize referências existentes,sem certificado/aprovador novo.');
if(row.treatment==='goods_before_import'){
if(!row.transportRef.trim()||!row.importRef.trim()||!row.chainRef.trim())return stop('Identifique transporte/expedição do estrangeiro,importação nacional e posição da transmissão na cadeia,ou pressupostos expressos;não inferir a condição pelo endereço do adquirente.');
return{...result,status:'determined',territory:'national',reason:`Art6(2):transmissão pelo importador ou subsequente de bens transportados/expedidos do estrangeiro,antes da importação nacional identificada;abrangida territorialmente emMoçambique,${row.origin==='projected'?'nas condições expressamente assumidas na projecção':'nos dados efectivos declarados'}. A localização física anterior no estrangeiro não afasta por si esta regra. Classificação não determina isenção,base,taxa,IVA,dedução/campo11,direitos ou totaisModeloA.`};
}
if(!row.goodsPlace.trim()||!row.placeRef.trim()||!['MZ','foreign'].includes(row.placeCountry))return stop('Identifique onde os bens se encontram no momento relevante e respectiva referência:início do transporte/expedição ou colocação à disposição sem transporte;endereço do cliente não substitui esse facto.');
if(row.treatment==='goods_with_transport'&&!row.transportRef.trim())return stop('Identifique o transporte/expedição correspondente ao início referido,ou pressuposto expresso.');
const territory=row.placeCountry==='MZ'?'national':'outside';return{...result,status:'determined',territory,reason:`Art6(1):bens ${territory==='national'?'situados emMoçambique':'situados fora deMoçambique'} ${row.treatment==='goods_with_transport'?'ao iniciar o transporte/expedição para o adquirente':'ao serem postos à disposição do adquirente,sem transporte/expedição'},${row.origin==='projected'?'nas condições expressamente assumidas na projecção':'nos dados efectivos declarados'}. Factos de6(2)conferidos como não aplicáveis a este percurso. ${territory==='outside'?'Fora deste âmbito territorial não equivale a isenção ou taxa/IVA zero.':'Abrangência territorial não confirma isenção,taxa ou IVA.'} Classificação não determina base,dedução/campo11,direitos ou totaisModeloA.`};
}
function key(draft:Record<string,unknown>,book:GoodsLocationBook):string{return JSON.stringify({version:goodsLocationVersion,scope:acquisitionScope(draft),location:draft.selection_labels??{},rows:book.rows});}
export function goodsLocationCurrent(draft:Record<string,unknown>,book:GoodsLocationBook):boolean{return!!book.result&&book.result.sourceKey===key(draft,book);}
export function recordGoodsLocations(draft:Record<string,unknown>,book:GoodsLocationBook):GoodsLocationBook{
if(!book.rows.length||new Set(book.rows.map(row=>row.id)).size!==book.rows.length)throw new Error('Indique operações com identificadores distintos;originais conservados.');
const result:GoodsLocationCalculation={version:goodsLocationVersion,revision:(book.result?.revision??0)+1,calculatedAt:new Date().toISOString(),opportunity:acquisitionScope(draft),location:JSON.stringify(draft.selection_labels??{}),sourceKey:key(draft,book),source:goodsLocationSource,inputs:structuredClone(book.rows),rows:book.rows.map(row=>evaluateGoodsLocation(row))};
const next={...book,result,history:[...(book.history??[]),...(book.result?[book.result]:[])]};saveGoodsLocationBook(draft,next);return next;
}
export function goodsLocationReportRows(draft:Record<string,unknown>):Array<[string,string,string,string]>{
try{const book=goodsLocationBook(draft),result=book.result;if(!result)return[];return result.inputs.map((row,index)=>['IVA mensal',`Localização P105 — ${row.period} — ${row.id}`,`${row.description};classificação territorial: ${result.rows[index].territory==='national'?'Moçambique':result.rows[index].territory==='outside'?'Fora do território nacional':'Indeterminada'};IVA: Indeterminado;${result.rows[index].reason}`,`${result.opportunity};${result.location};${goodsLocationCurrent(draft,book)?'Resultado actual':'Entradas alteradas;resultado histórico conservado'};versão${result.revision};${result.calculatedAt};${result.source};${row.origin==='projected'?'Projecção':'Dados efectivos declarados'};${goodsLocationTreatments.find(t=>t.id===row.treatment)?.label??'Percurso por indicar'};operação ${row.operationRef},${row.operationDate};${row.treatment==='goods_before_import'?`transporte ${row.transportRef};importação ${row.importRef};cadeia ${row.chainRef}`:`local dos bens ${row.goodsPlace},${row.placeCountry};referência ${row.placeRef}${row.treatment==='goods_with_transport'?`;transporte ${row.transportRef}`:''}`};âmbito ${row.scope};documento ${row.documentRef},${row.documentDate};condições ${[...goodsLocationConditions,...ownGoodsConditions(row.treatment)].map(([key,label])=>label+': '+(row.conditions[key]==='yes'?'Sim':row.conditions[key]==='no'?'Não':'Por indicar')).join('; ')};fundamento factual ${row.evidence};não determina isenção/IVA/campo11/totaisModeloA.`]);}
catch{return[['IVA mensal','Localização de bens','Dados não recuperáveis','Originais preservados;sem classificar por defeito.']];}
}
+2
View File
@@ -14,6 +14,7 @@ import { compositionReportRows } from './iva-composition-base.ts';
import { exclusionReportRows } from './iva-exclusion-base.ts';
import { valueReviewReportRows } from './iva-value-review.ts';
import { serviceNormalReportRows } from './iva-service-normal.ts';
import { goodsLocationReportRows } from './iva-goods-location.ts';
import { offerPreparationReportRows } from './iva-offer-preparations.ts';
import { salesReportRows } from './iva-operations.ts';
import { fiscalQaRows } from './fiscal-qa-preview.ts';
@@ -638,6 +639,7 @@ export function buildProjectDossierRows(
rows.push(...exclusionReportRows(draft));
rows.push(...valueReviewReportRows(draft));
rows.push(...serviceNormalReportRows(draft));
rows.push(...goodsLocationReportRows(draft));
rows.push(...offerPreparationReportRows(draft));
const operationRows = salesReportRows(draft);
rows.push(...operationRows);
@@ -0,0 +1,57 @@
<?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 IvaGoodsLocationDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_goods_locations_recover_privately_preserving_all_delivered_books(): void
{
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test', $target->name);
$this->assertSame(55441, (int) $target->port);
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
$this->actingAs(User::factory()->create(['email_verified_at' => now()]));
$draft = json_decode(file_get_contents(base_path('tests/frontend/fixtures/iva-g8a-bases.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:348', $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['goodsLocations']['opportunity:348'];
$this->assertCount(3, $book['rows']);
$this->assertSame(['goods_with_transport','goods_without_transport','goods_before_import'], array_column($book['rows'], 'treatment'));
$this->assertSame(['national','outside','national'], array_column($book['result']['rows'], 'territory'));
$this->assertSame(['determined','determined','determined'], array_column($book['result']['rows'], 'status'));
$this->assertSame([null,null,null], array_column($book['result']['rows'], 'baseAmount'));
$this->assertSame([null,null,null], array_column($book['result']['rows'], 'vat'));
$this->assertSame([null,null,null], array_column($book['result']['rows'], 'rate'));
$this->assertSame(['MZ','foreign','foreign'], array_column($book['rows'], 'placeCountry'));
$this->assertSame('2026-12-12', $book['rows'][0]['operationDate']);
$this->assertSame('yes', $book['rows'][2]['conditions']['beforeImport']);
$this->assertStringContainsString('importação nacional A', $book['rows'][2]['importRef']);
$this->assertStringContainsString('importador A', $book['rows'][2]['chainRef']);
$this->assertStringContainsString('local físico', $book['rows'][0]['goodsPlace']);
$this->assertStringContainsString('não equivale a isenção', $book['result']['rows'][1]['reason']);
$this->assertStringContainsString('6(1)/(2)', $book['result']['source']);
$this->assertSame($book['rows'], $book['result']['inputs']);
$this->assertCount(2, $book['history']);
$this->assertNull($book['history'][0]['rows'][0]['baseAmount']);
$frozen = json_decode(file_get_contents(base_path('tests/frontend/fixtures/iva-g8a-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, array_diff_key($store, ['goodsLocations' => true]));
$this->assertSame('1250.30', $store['publicAuthorityBases']['opportunity:339']['result']['rows'][1]['baseAmount']);
$this->assertNull($store['publicAuthorityBases']['opportunity:339']['result']['rows'][1]['vat']);
$this->assertSame('400.00', $store['importExemptions']['opportunity:338']['result']['rows'][1]['importVat']);
$this->assertNull($store['books']['opportunity:329']['result']['periods'][0]['fields']['11']);
file_put_contents(base_path('outputs/iva-l1-g8a-goods-location-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,35 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import {newGoodsLocation,switchGoodsTreatment,goodsLocationTreatments,goodsLocationConditions,ownGoodsConditions,evaluateGoodsLocation,recordGoodsLocations,saveGoodsLocationBook,goodsLocationBook,goodsLocationCurrent,goodsLocationReportRows} from '../../resources/js/lib/iva-goods-location.ts';
import {serviceNormalCurrent} from '../../resources/js/lib/iva-service-normal.ts';
import {valueReviewCurrent} from '../../resources/js/lib/iva-value-review.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
const frozen=JSON.parse(fs.readFileSync(new URL('./fixtures/iva-g8a-preserved-inputs.json',import.meta.url),'utf8'));
const row=(treatment='goods_with_transport')=>({...newGoodsLocation('g8a-'+treatment),treatment,description:'SYNTHETIC QA bens A',period:'2026-12',operationDate:'2026-12-12',operationRef:'SYNTHETIC QA operação A',documentRef:'SYNTHETIC QA G8A '+treatment,documentDate:'2026-12-12',goodsPlace:'SYNTHETIC QA local físico dos bens no momento relevante',placeCountry:treatment==='goods_with_transport'?'MZ':'foreign',placeRef:'SYNTHETIC QA referência do local e momento A',transportRef:'SYNTHETIC QA transporte A',importRef:'SYNTHETIC QA importação nacional A',chainRef:'SYNTHETIC QA importador A/transmissão subsequente antes da importação',scope:'SYNTHETIC QA bens A/operação A/dezembro2026/condições A',evidence:'SYNTHETIC QA pressupostos expressos,não documento real',conditions:Object.fromEntries([...goodsLocationConditions,...ownGoodsConditions(treatment)].map(([key])=>[key,'yes']))});
const draft=()=>({...structuredClone(frozen),context_opportunity:'opportunity:348',project_title:'SYNTHETIC QA — G8A / Localização de bens'});
test('P105 each route owns facts/references;unknown/no never defaultoutside/exempt orIVA zero',()=>{
for(const t of goodsLocationTreatments){const input=row(t.id),ok=evaluateGoodsLocation(input);assert.equal(ok.territory,t.id==='goods_without_transport'?'outside':'national');assert.equal(ok.baseAmount,null);assert.equal(ok.vat,null);assert.equal(ok.rate,null);
for(const[key]of [...goodsLocationConditions,...ownGoodsConditions(t.id)])for(const value of ['','unknown','no']){const r=evaluateGoodsLocation({...input,conditions:{...input.conditions,[key]:value}});assert.equal(r.status,value==='no'?'not_applicable':'indeterminate');assert.equal(r.territory,null);assert.equal(r.vat,null);}
for(const patch of [{description:''},{operationRef:''},{documentRef:''},{documentDate:'2026-02-30'},{operationDate:'2026-11-12'},{operationDate:'2026-02-30'},{scope:''},{evidence:''},{period:'2025-12'},{treatment:''}])assert.equal(evaluateGoodsLocation({...input,...patch}).territory,null);
const required=t.id==='goods_before_import'?['transportRef','importRef','chainRef']:['goodsPlace','placeCountry','placeRef',...(t.id==='goods_with_transport'?['transportRef']:[])];for(const field of required)assert.equal(evaluateGoodsLocation({...input,[field]:''}).territory,null);
}
});
test('physical start/disposal determines6(1);preimport6(2)overridesphysicalforeign;no inference fromclient ormoney',()=>{
for(const treatment of ['goods_with_transport','goods_without_transport'])for(const placeCountry of ['MZ','foreign'])assert.equal(evaluateGoodsLocation({...row(treatment),placeCountry}).territory,placeCountry==='MZ'?'national':'outside');
const before=row('goods_before_import');assert.equal(evaluateGoodsLocation({...before,goodsPlace:'',placeCountry:'',placeRef:''}).territory,'national');assert.equal(evaluateGoodsLocation({...before,placeCountry:'foreign'}).territory,'national');assert.equal(evaluateGoodsLocation({...row(),placeCountry:'unknown'}).territory,null);
const switched=switchGoodsTreatment(row('goods_without_transport'),'goods_before_import');for(const field of ['goodsPlace','placeCountry','placeRef','transportRef','importRef','chainRef'])assert.equal(switched[field],row('goods_without_transport')[field]);assert.equal(switched.conditions.beforeImport,undefined);assert.equal(evaluateGoodsLocation(switched).territory,null);
const d=draft(),book=recordGoodsLocations(d,{rows:[row()]});assert.ok(goodsLocationCurrent({...d,currency:'USD',customer_address:'foreign',customer_nationality:'foreign'},book));
});
test('save/recovery/history/report preserve all87 delivered books/currentkeys and territorialclassification distinctfromtax',()=>{
const d=draft(),prior=JSON.parse(d.modelo_a_snapshot);assert.deepEqual(goodsLocationBook(d),{rows:[]});assert.deepEqual(goodsLocationReportRows(d),[]);
let book=recordGoodsLocations(d,{rows:goodsLocationTreatments.map(t=>({...row(t.id),conditions:{}}))});book=recordGoodsLocations(d,{...book,rows:goodsLocationTreatments.map(t=>({...row(t.id),conditions:{...row(t.id).conditions,goodsSupply:'no'}}))});book=recordGoodsLocations(d,{...book,rows:goodsLocationTreatments.map(t=>row(t.id))});assert.equal(book.history.length,2);assert.ok(book.history.every(h=>h.rows.every(r=>r.territory===null&&r.vat===null)));assert.ok(goodsLocationCurrent(d,book));
for(const field of ['treatment','placeCountry','goodsPlace','placeRef','importRef','chainRef','scope','operationDate'])assert.ok(!goodsLocationCurrent(d,{...book,rows:book.rows.map(r=>({...r,[field]:'changed'}))}));for(const patch of [{context_opportunity:'opportunity:349'},{selection_labels:{province_id:'Maputo'}}])assert.ok(!goodsLocationCurrent({...d,...patch},book));
d.modelo_a_snapshot=saveGoodsLocationBook(d,book);assert.deepEqual(goodsLocationBook(JSON.parse(JSON.stringify(d))),book);const next=JSON.parse(d.modelo_a_snapshot);const{goodsLocations,...untouched}=next;assert.deepEqual(untouched,prior);for(const[scope,previous]of Object.entries(prior.serviceNormals))assert.ok(serviceNormalCurrent({...d,context_opportunity:scope},previous));for(const[scope,previous]of Object.entries(prior.valueReviews))assert.ok(valueReviewCurrent({...d,context_opportunity:scope},previous));
assert.equal(next.serviceNormals['opportunity:347'].result.rows[2].baseAmount,'900.75');assert.equal(next.valueReviews['opportunity:346'].result.rows[1].baseAmount,'1400.75');assert.equal(next.considerationExclusions['opportunity:345'].result.rows[0].baseAmount,'799.70');assert.equal(next.books['opportunity:329'].result.periods[0].fields['11'],null);
const report=JSON.stringify(createProjectDossierReport(d,{selectedSections:new Set(['finance']),version:'QA G8A'}));for(const token of ['Localização P105','Fora do território nacional','IVA: Indeterminado','opportunity:348','Manica','2026-12','6(1)','6(2)','importador A','não equivale a isenção','campo11'])assert.ok(report.includes(token),token);assert.deepEqual(goodsLocationReportRows({...d,context_opportunity:'opportunity:349'}),[]);
if(process.env.IVA_G8A_CALCULATED)fs.writeFileSync(process.env.IVA_G8A_CALCULATED,JSON.stringify(d));
});
test('corrupt goods rows/store cannot overwrite originals;duplicateoperationids cannotbe recorded',()=>{
const d=draft(),original=d.modelo_a_snapshot;for(const patch of [{placeCountry:4},{conditions:[]},{chainRef:null}])assert.throws(()=>saveGoodsLocationBook(d,{rows:[{...row(),...patch}]}),/originais preservados/);assert.equal(d.modelo_a_snapshot,original);assert.throws(()=>recordGoodsLocations(d,{rows:[row(),row()]}),/distintos/);assert.throws(()=>recordGoodsLocations(d,{rows:[]}),/distintos/);assert.throws(()=>goodsLocationBook({...d,modelo_a_snapshot:JSON.stringify({...JSON.parse(original),goodsLocations:[]})}),/originais preservados/);
});