feat(iva): add G4C linked export services with explicit operation reference
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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);}
|
||||
@@ -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));
|
||||
});
|
||||
Reference in New Issue
Block a user