feat(iva): add G4C linked export services with explicit operation reference

This commit is contained in:
2026-10-24 17:53:35 +02:00
parent 117f2a70a8
commit b1c8e0e343
6 changed files with 123 additions and 2 deletions
@@ -3,8 +3,9 @@ import { fundraisingActivities } from '@/lib/iva-fundraising';
import { pharmaAnnexEntries, pharmaAnnexEntry, pharmaAnnexReference } from '@/lib/iva-pharma-annex';
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 { emptyProductionTaxOption, type ProductionTaxOption } from '@/lib/iva-production-option';
import { continueTemporaryOperation, continueAnnexIOperation, modeloA06Correspondence, newSalesOperation, operationTreatments, recordSales, reuseSales, salesBook, salesCurrent, salesReportRows, saveSalesBook, splitSales, type SalesBook, type SalesOperation } from '@/lib/iva-operations';
import { referenceExportOperation, continueTemporaryOperation, continueAnnexIOperation, modeloA06Correspondence, newSalesOperation, operationTreatments, recordSales, reuseSales, salesBook, salesCurrent, salesReportRows, saveSalesBook, splitSales, type SalesBook, type SalesOperation } from '@/lib/iva-operations';
export function SimulationOperations({draft,onChange}:{draft:Record<string,unknown>;onChange:(field:string,value:string)=>void}) {
const [error,setError]=useState('');
@@ -34,6 +35,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>
{isExportService(row.treatment)&&<fieldset className="space-y-2 rounded border p-2"><legend>Referência à operação relacionada</legend>
<p>Se a operação já consta desta oportunidade,pode referenciá-la sem preencher de novo a descrição. A escolha conserva o preço,moeda,período e fundamento deste serviço;limpa as confirmações para revisão. Não copia o valor dos bens para o preço do serviço nem comprova automaticamente a isenção. Para operação documentada fora deste livro,use o fundamento abaixo.</p>
<label>Operação já preenchida<select value={row.relatedExport?.id??''} onChange={e=>run(()=>{if(!e.target.value){save({...book,rows:book.rows.map(item=>item.id===row.id?{...item,relatedExport:undefined,confirmed:[]}:item)});return;}const related=book.rows.find(item=>item.id===e.target.value);if(!related)throw new Error('Operação relacionada já não disponível; dados conservados.');save({...book,rows:book.rows.map(item=>item.id===row.id?referenceExportOperation(item,related):item)});})}><option value="">Sem referência interna — identificar no fundamento</option>{book.rows.filter(item=>item.id!==row.id).map(item=><option key={item.id} value={item.id}>{item.description||item.id} — {item.period}</option>)}</select></label>
{row.relatedExport&&<><p>Referência guardada: {row.relatedExport.description}; ID {row.relatedExport.id}; {row.relatedExport.treatment}.</p>{book.rows.some(item=>item.id===row.relatedExport!.id)&&<button type="button" onClick={()=>run(()=>{const related=book.rows.find(item=>item.id===row.relatedExport!.id)!;save({...book,rows:book.rows.map(item=>item.id===row.id?referenceExportOperation(item,related):item)});})}>Actualizar referência escolhida e rever condições</button>}</>}
</fieldset>}
{temporaryBenefit(row.treatment)&&<fieldset className="space-y-2 rounded border p-2"><legend>Benefício temporário — prazo e continuidade</legend>
<p>{temporaryBenefitNotice(row.treatment,row.period)}</p>
<p>{temporaryBenefit(row.treatment)!.historicalScope}</p>
+20
View File
@@ -0,0 +1,20 @@
import type { OperationTreatment } from './iva-operations.ts';
const source='CIVA republicado Lei13/2016,13(1)(m/n/s),PDF12/p.1426(270);remissões12(1)(c),PDF11/p.1426(269),14,PDF13/p.1426(271),19(1)(b)(i),PDF15/p.1426(273). Lei22/2022art.1 eLei10/2025art.1 mantêm13 e19(1)(b)(i);Lei10/2025PDF12–13/p.2180(12)–(13). Âmbito2026/MZN;classificação do serviço,sem certificar regime/documentos ou deduzir compras automaticamente.';
const scope='O próprio serviço está localizado e abrangido pelo CIVA em Moçambique, com territorialidade identificada; cliente estrangeiro ou operação subjacente no estrangeiro não determinam, por si, a localização deste serviço';
const make=(id:string,label:string,letter:string,conditions:string[]):OperationTreatment=>({id,label,field:'05',rate:0,article:`CIVA arts.13(1)(${letter}),19(1)(b)(i)`,source,logicVersion:'export-linked-services/1',conditions:[scope,...conditions]});
export const exportServiceTreatments:OperationTreatment[]=[
make('export_customs_related_services','Serviços ligados a bens nos regimes indicados no artigo13(1)(m)','m',[
'Serviço directamente relacionado com trânsito, exportação ou importação dos bens identificados, com ligação concreta descrita',
'Os bens estão isentos por admissão temporária, draubaque ou trânsito nos termos de12(1)(c), ou entraram em depósitos de regime aduaneiro ou livre de outras áreas do14; regime e documento/pressuposto identificados, não apenas localização do projecto numa zona',
'O serviço não está entre os serviços referidos no artigo9; a exclusão foi analisada para esta operação concreta, não presumida por o cliente exportar',
]),
make('export_dispatch_services','Serviços ligados à expedição de bens para o estrangeiro','n',[
'Serviço identificado e relacionado com expedição de bens destinados ao estrangeiro; bens, destino e ligação concreta do serviço identificados no documento ou pressuposto',
'O serviço não está entre os serviços referidos no artigo9; não basta uma venda a cliente estrangeiro nem um serviço genérico ao exportador',
]),
make('export_disclosed_intermediation','Intermediação em nome e por conta de outrem — artigo13(1)(s)','s',[
'Prestação de intermediação em nome e por conta de outrem, com mandante e mandato/pressuposto identificados; não actuação em nome próprio',
'O intermediário intervém numa operação descrita no artigo13 ou realizada fora do território nacional, com operação e fundamento/localização identificados; a referência a outra linha não comprova automaticamente esses factos',
]),
];
export function isExportService(id:string):boolean{return exportServiceTreatments.some(item=>item.id===id);}
+12 -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 { internationalAssetTreatments } from './iva-international-assets.ts';
import { exportServiceTreatments, isExportService } from './iva-export-services.ts';
import { monthlySalesFromDraft, readMonthlyIvaSettings } from './monthly-iva.ts';
import { addFractions, decimalFraction, fraction, fractionLabel, fractionPresentation, type FiscalFraction } from './fiscal-rational.ts';
@@ -60,6 +61,7 @@ const listedGoodsTreatments:OperationTreatment[] = [
['insecticides','Insecticidas','Bem transmitido identificado como insecticida; não qualquer produto químico'],
].map(([id,label,condition])=>({id:`listed_${id}`,label:`Lista legal — ${label}`,field:'05',rate:0,article:'CIVA arts.9(10),19(1)(b)(v), redacção Lei22/2022',source:'Lei22/2022 art.1, PDF2, p.2252(26),9(10); CIVA republicado Lei13/2016, PDF15, p.1426(273),19(1)(b)(v). Âmbito técnico2026/MZN',conditions:[internalOperation,'Transmissão do próprio bem enumerado, identificado; não um serviço autónomo nem extensão por sector, marca ou destino',condition]}));
export const operationTreatments:OperationTreatment[] = [
...exportServiceTreatments,
...internationalAssetTreatments,
...directExportTreatments,
...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})),
@@ -120,7 +122,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; 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}; 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};
@@ -136,6 +138,10 @@ export function continueTemporaryOperation(row:SalesOperation,targetId:string):S
if(!benefit||!target||temporaryBenefit(targetId))throw new Error('Escolha um enquadramento actual distinto do benefício terminado; dados conservados.');
return {...row,treatment:targetId,confirmed:[],temporaryBenefitOrigin:benefit.id,source:`${row.source}; continuidade expressamente escolhida de ${benefit.id}/${benefit.branch}, CIVA art.${benefit.article}, termo31/12/2025, ${temporaryBenefitVersion}, para ${target.label}; ${benefit.source}; montante/moeda/período conservados; rever condições, sem cálculo automático`};
}
export function referenceExportOperation(row:SalesOperation,related:SalesOperation):SalesOperation {
if(!isExportService(row.treatment)||row.id===related.id)throw new Error('Escolha outra operação para referenciar neste serviço; dados conservados.');
return {...row,relatedExport:{id:related.id,description:related.description,treatment:related.treatment},confirmed:[]};
}
export function salesBook(draft:Record<string,unknown>):SalesBook {
const store=readAcquisitionStore(draft.modelo_a_snapshot) as Store;
if(store.salesOperations!==undefined&&(!store.salesOperations||typeof store.salesOperations!=='object'||Array.isArray(store.salesOperations)))throw new Error('Formato das operações IVA não recuperável; dados preservados, sem substituição.');
@@ -209,6 +215,10 @@ export function calculateSales(draft:Record<string,unknown>,book:SalesBook,now=n
const start=readMonthlyIvaSettings(String(draft.monthly_iva_settings??'')).start;
const rows=book.rows.map(r=>{
const result=evaluateSalesOperation(r,book.currency);
if(isExportService(r.treatment)&&r.relatedExport!==undefined){
const ref=r.relatedExport,target=ref&&typeof ref==='object'?book.rows.find(item=>item.id===ref.id&&item.id!==r.id):undefined;
if(!target||ref.description!==target.description||ref.treatment!==target.treatment)return {...result,net:null,vat:null,gross:null,status:'indeterminate' as const,reason:'A operação relacionada foi removida ou alterada. Actualize a referência e reveja as condições; preço do serviço e resultado anterior conservados.'};
}
const fundraiser=r.fundraiser;
if(r.treatment.startsWith('fundraising_')&&fundraiser&&book.rows.some(v=>v.id!==r.id&&v.treatment.startsWith('fundraising_')&&v.period.slice(0,4)===r.period.slice(0,4)&&String(v.fundraiser?.entityReference??'').trim()===String(fundraiser.entityReference??'').trim()&&v.fundraiser?.annualEvents!==fundraiser.annualEvents))return {...result,net:null,vat:null,gross:null,status:'indeterminate' as const,reason:'Totais anuais divergentes para a mesma entidade; resolva a divergência sem substituir dados de outra linha.'};
if(r.splitSource) {
@@ -245,6 +255,7 @@ export function salesReportRows(draft:Record<string,unknown>):Array<[string,stri
const out:Array<[string,string,string,string]>=[['IVA mensal','Operações IVA L1',salesCurrent(draft,book)?'Resultado correspondente às entradas':'Histórico desactualizado; não aplicar à projecção actual',context],['IVA mensal','Método e limites L1',r.rounding,'Sem apuramento integral, dedução automática ou submissão AT']];
for(const row of r.inputs){const value=r.rows.find(v=>v.id===row.id)!;out.push(['IVA mensal',`${row.period} — ${row.description} — ${row.establishment}`,`Declarado ${row.amount||'em falta'} ${row.sourceCurrency}; ${row.basis==='included'?'IVA incluído':row.basis==='net'?'sem IVA':'base desconhecida'}; líquido ${value.net??'Indeterminado'}; IVA ${value.vat??'Indeterminado'}; bruto ${value.gross??'Indeterminado'}; campo ${value.field??'não determinado'}; ${value.reason}`,`${context}; ${value.article}; ${value.source}; origem: ${row.source}; pressuposto/documento: ${row.evidence||'em falta'}; condições confirmadas: ${row.confirmed.join(',')||'nenhuma'}`]);}
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 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.`]);
@@ -0,0 +1,47 @@
<?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 IvaExportServiceDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_linked_export_services_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-g4-services-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:325', $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:325'];
$this->assertSame('opportunity:325', $book['result']['opportunity']);
$this->assertSame('MZN', $book['result']['currency']);
$this->assertSame('2026-12', $book['result']['periods'][0]['period']);
$this->assertSame('1300.00', $book['result']['periods'][0]['fields']['05']);
$this->assertCount(4, $book['rows']);
$this->assertSame('g4c-goods', $book['rows'][2]['relatedExport']['id']);
$this->assertSame('100', $book['rows'][2]['amount']);
$this->assertSame('1000', $book['rows'][0]['amount']);
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-g4b-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-g4-services-20261024/recovered.json'), json_encode($recovered, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,36 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { exportServiceTreatments } from '../../resources/js/lib/iva-export-services.ts';
import { newSalesOperation,operationTreatments,evaluateSalesOperation,referenceExportOperation,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-g4b-inputs.json',import.meta.url),'utf8'));
const row=(rule,i=0)=>({...newSalesOperation(`g4c-${i}`,'2026-12','MZN'),description:`SYNTHETIC QA — ${rule.label}`,establishment:'SYNTHETIC QA estabelecimento',amount:'100',basis:i%2?'included':'net',treatment:rule.id,confirmed:rule.conditions.map((_,n)=>n),evidence:'SYNTHETIC QA — documento/pressuposto externo identifica bens,regime/localização,mandante e ligação do serviço;operação do intermediário localizada emMoçambique;sem certificação'});
function fixture(){
const draft={...structuredClone(frozen),context_opportunity:'opportunity:325',project_title:'SYNTHETIC QA — G4C / Serviços ligados'};
const goods={...row(operationTreatments.find(r=>r.id==='export_goods'),9),id:'g4c-goods',description:'SYNTHETIC QA — mercadorias expedidas pelo vendedor',amount:'1000'};
const services=exportServiceTreatments.map(row);services[1]=referenceExportOperation(services[1],goods);services[1].confirmed=exportServiceTreatments[1].conditions.map((_,i)=>i);
const book=recordSales(draft,{currency:'MZN',rows:[goods,...services],periods:[{period:'2026-12',complete:true}]});draft.modelo_a_snapshot=saveSalesBook(draft,book);return{draft,book};
}
test('three services require each legal condition,local service scope and evidence;no automatic classification from client or sector',()=>{
assert.equal(exportServiceTreatments.length,3);assert.match(exportServiceTreatments[0].conditions[2],/admissão temporária/);assert.match(exportServiceTreatments[0].conditions[3],/artigo9/);assert.match(exportServiceTreatments[1].conditions[2],/artigo9/);assert.match(exportServiceTreatments[2].conditions[1],/não actuação em nome próprio/);
for(const [i,rule]of exportServiceTreatments.entries()){const input=row(rule,i);assert.equal(evaluateSalesOperation(input,'MZN').field,'05');assert.equal(evaluateSalesOperation(input,'MZN').vat,'0.00');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:''}])assert.equal(evaluateSalesOperation({...input,...patch},'MZN').vat,null);}
});
test('reference reuse keeps service price/identity/evidence and requires deliberate confirmation of new relationship',()=>{
const service=row(exportServiceTreatments[1]),related={...row(operationTreatments.find(r=>r.id==='export_goods'),8),amount:'1000'},linked=referenceExportOperation(service,related);
assert.deepEqual(linked.relatedExport,{id:related.id,description:related.description,treatment:related.treatment});assert.deepEqual(linked.confirmed,[]);
for(const field of ['id','amount','basis','period','description','evidence','establishment','sourceAmount','sourcePeriod','sourceCurrency','source'])assert.deepEqual(linked[field],service[field],field);
assert.equal(linked.amount,'100');assert.equal(evaluateSalesOperation(linked,'MZN').vat,null);assert.throws(()=>referenceExportOperation(service,service));assert.throws(()=>referenceExportOperation(related,service));
});
test('deleted/changed linked operation blocks current calculation,keeps history and requires explicit reference update',()=>{
const{draft,book}=fixture();assert.equal(book.result.periods[0].fields['05'],'1300.00');
for(const rows of [book.rows.filter(r=>r.id!=='g4c-goods'),book.rows.map(r=>r.id==='g4c-goods'?{...r,description:'Changed goods'}:r)]){const changed=recordSales(draft,{...book,rows});assert.equal(changed.result.rows.find(r=>r.id==='g4c-1').vat,null);assert.match(changed.result.rows.find(r=>r.id==='g4c-1').reason,/removida ou alterada/);assert.equal(changed.result.periods[0].fields['05'],null);assert.deepEqual(changed.history.at(-1),book.result);assert.equal(changed.rows.find(r=>r.id==='g4c-1').amount,'100');}
const changedGoods={...book.rows[0],description:'Reviewed goods'};const linked=referenceExportOperation(book.rows[2],changedGoods);linked.confirmed=exportServiceTreatments[1].conditions.map((_,i)=>i);const next=recordSales(draft,{...book,rows:[changedGoods,book.rows[1],linked,book.rows[3]]});assert.equal(next.result.periods[0].fields['05'],'1300.00');
const invalid=recordSales(draft,{...book,rows:book.rows.map(r=>r.id==='g4c-1'?{...r,relatedExport:{id:r.id,description:r.description,treatment:r.treatment}}:r)});assert.equal(invalid.result.periods[0].fields['05'],null);
});
test('stored service reference survives report/recovery;all16 prior books remain unchanged/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,16);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 G4C'}));for(const token of ['opportunity:325','Manica','2026-12','1300.00','13(1)(m)','13(1)(n)','13(1)(s)','g4c-goods','Operação relacionada'])assert.ok(report.includes(token),token);
if(process.env.IVA_G4C_CALCULATED)fs.writeFileSync(process.env.IVA_G4C_CALCULATED,JSON.stringify(draft));
});