feat(iva): add G4E export buyer and beneficiary conditions in QA

This commit is contained in:
2026-10-24 18:20:04 +02:00
parent 3be371c823
commit 38faeac685
5 changed files with 113 additions and 0 deletions
@@ -0,0 +1,26 @@
import type { OperationTreatment } from './iva-operations.ts';
const source = 'CIVA republicado Lei13/2016, BR156/10.º suplemento,13(1)(b/j/k/l),PDF12/p.1426(270);19(1)(b)(i),PDF15/p.1426(273). Alterações Lei22/2022art.1 e Lei10/2025art.1 não alteram13 e mantêm19(1)(b)(i). Âmbito2026/MZN. Exige enquadramento/documento ou pressuposto expresso da projecção; não certifica reconhecimento, acordo, despacho ou exportaçã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-beneficiaries/1',conditions:[
'Operação abrangida pelo CIVA em Moçambique, com territorialidade identificada; não importação nem operação localizada no estrangeiro',
...conditions,
]});
export const exportBeneficiaryTreatments:OperationTreatment[] = [
make('export_nonresident_buyer','Exportação por adquirente sem residência ou estabelecimento nacional','b',[
'Transmissão de bens expedidos ou transportados com destino ao estrangeiro pelo adquirente sem residência ou estabelecimento em território nacional, ou por terceiro por conta deste; adquirente, interveniente e saída identificados',
'Os bens não se destinam ao abastecimento de barcos desportivos ou de recreio, aviões de turismo ou qualquer outro meio de transporte de uso privado; destino concreto identificado, sem presumir pela nacionalidade do comprador',
'Enquadramento na regulamentação própria prevista em13(1)(b) identificado no fundamento/documento ou como pressuposto expresso da projecção; sem essa identificação não confirmar a condição nem presumir despacho ou isenção',
]),
make('export_diplomatic_mission','Fornecimentos a entidades diplomáticas ou consulares','j',[
'Transmissão de bens ou prestação de serviços destinada à entidade diplomática ou consular identificada; não basta destinatário estrangeiro',
'A isenção desta operação resulta de acordo ou convénio internacional celebrado por Moçambique, identificado com disposição, âmbito e período aplicáveis no fundamento; uma entidade diplomática por si não comprova a isenção',
]),
make('export_international_organisation','Fornecimentos a organismos internacionais ou seus membros elegíveis','k',[
'Transmissão de bens ou prestação de serviços destinada a organismo internacional reconhecido por Moçambique ou a membro desse organismo; organismo, reconhecimento e qualidade do destinatário identificados',
'A operação cumpre os limites e condições do acordo ou convénio internacional celebrado por Moçambique identificado no fundamento, incluindo disposição, âmbito e período; não presumir isenção geral de todas as compras ou de qualquer membro',
]),
make('export_humanitarian_organisation','Bens para exportação por organismo humanitário, caritativo ou educativo','l',[
'Transmissão de bens a organismo devidamente reconhecido, que os exporta para o estrangeiro no âmbito das suas actividades humanitárias, caritativas ou educativas; organismo, bens, saída e actividade identificados, não serviço nem simples intenção genérica de exportar',
'Prévio reconhecimento do direito à isenção para esta operação, na forma determinada em decreto regulamentar, identificado no fundamento/documento ou como pressuposto expresso da projecção; não confundir reconhecimento do organismo com reconhecimento do direito',
]),
];
+2
View File
@@ -5,6 +5,7 @@ import { annexIEntry, annexIReference, annexISource, annexIVersion } from './iva
import { productionOptionCheck, productionOptionVersion, type ProductionTaxOption } from './iva-production-option.ts';
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 { 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';
@@ -66,6 +67,7 @@ export const operationTreatments:OperationTreatment[] = [
...onboardSupplyTreatments,
...internationalAssetTreatments,
...directExportTreatments,
...exportBeneficiaryTreatments,
...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']},
@@ -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 IvaExportBeneficiaryDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_export_beneficiaries_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-beneficiaries-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:327', $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:327'];
$this->assertSame('opportunity:327', $book['result']['opportunity']);
$this->assertSame('MZN', $book['result']['currency']);
$this->assertSame('2026-12', $book['result']['periods'][0]['period']);
$this->assertSame('400.00', $book['result']['periods'][0]['fields']['05']);
$this->assertCount(4, $book['rows']);
$this->assertSame(['export_nonresident_buyer','export_diplomatic_mission','export_international_organisation','export_humanitarian_organisation'], array_column($book['rows'], 'treatment'));
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-g4d-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-beneficiaries-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,39 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { exportBeneficiaryTreatments as rules } from '../../resources/js/lib/iva-export-beneficiaries.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-g4d-inputs.json',import.meta.url),'utf8'));
const row=(rule,i)=>({...newSalesOperation(`g4e-${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 — pressuposto expresso de projecção: intervenientes, destino/saída e regulamentação própria (b); acordo aplicável, disposição, âmbito/período e reconhecimento (j/k); reconhecimento do organismo e prévio direito (l). Sem certificação real.'});
function fixture(){
const draft={...structuredClone(frozen),context_opportunity:'opportunity:327',project_title:'SYNTHETIC QA — G4E / Exportações e beneficiários'};
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('four distinct branches require every applicable condition; missing treaty/recognition/private-use exclusion never produces zero',()=>{
assert.equal(rules.length,4);
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');
for(const missing of input.confirmed){const incomplete=evaluateSalesOperation({...input,confirmed:input.confirmed.filter(n=>n!==missing)},'MZN');assert.equal(incomplete.vat,null);assert.equal(incomplete.net,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(rules[0].conditions.join(' '),/uso privado/);assert.match(rules[0].conditions.join(' '),/regulamentação própria/);
for(const rule of rules.slice(1,3))assert.match(rule.conditions.join(' '),/acordo ou convénio/);
assert.match(rules[3].conditions.join(' '),/Prévio reconhecimento do direito/);
});
test('editing legal support preserves price/context/history and makes totals indeterminate until conditions are reviewed',()=>{
const{draft,book}=fixture();assert.equal(book.result.periods[0].fields['05'],'400.00');
for(let i=0;i<4;i++){
const changed=recordSales(draft,{...book,rows:book.rows.map((r,n)=>n===i?{...r,evidence:'Documento/pressuposto revisto; condições por reconfirmar',confirmed:[]}:r)});
assert.equal(changed.rows[i].amount,'100');assert.equal(changed.rows[i].sourceCurrency,'MZN');assert.equal(changed.rows[i].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 sources,assumptions,opportunity;18 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,18);
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 G4E'}));
for(const token of ['opportunity:327','Manica','2026-12','400.00','13(1)(b)','13(1)(j)','13(1)(k)','13(1)(l)','pressuposto expresso','QA G4E'])assert.ok(report.includes(token),token);
if(process.env.IVA_G4E_CALCULATED)fs.writeFileSync(process.env.IVA_G4E_CALCULATED,JSON.stringify(draft));
});