feat(iva): add conditional cultural and membership operations in QA

This commit is contained in:
2026-10-24 14:38:55 +02:00
parent dacda11c07
commit 122f4f04b9
7 changed files with 139 additions and 4 deletions
@@ -5455,5 +5455,47 @@
"http-diagnostic.json": "1066f9994232bf29ded0e056a06aee0262524f66ccb98eb85474ffaea500d7c1",
"manifest-served.json": "b9f7c6d05c78754dd1f9efb71bf5ff59cdfaa1167e136a71138c0fd194b19a33"
}
},
"iva_l1_g2_cultural_qa_20261024": {
"candidate": "CANDIDATE_G2C",
"base_installed": "490527b9",
"base_receipt": "dacda11c",
"status": "demonstrated_QA_not_installed",
"group": "G2C within G2",
"branches": [
"AUD-SIM-L1-P013",
"AUD-SIM-L1-P014",
"AUD-SIM-L1-P015"
],
"treatments": 5,
"scope": "2026/MZN;9(2)e–g;10(1–2);19(1);field07",
"tests": {
"frontend": 2,
"api_tests": 1,
"api_assertions": 14,
"UI_to_API_to_report": true,
"types": "passed",
"build": "passed"
},
"preservation": "Three accepted frozen books unchanged/current without recalculation;12delivered rows unchanged;acceptedWord/PDFreused",
"counts": {
"delivered": 12,
"demonstrated_QA": 3,
"functional_pending": 117,
"documentary_pending": 1,
"total": 133
},
"homologation_unchanged": true,
"P001_blocks_independent": false,
"L1_complete": false,
"artifacts": {
"api-ui-tests.xml": "1a3c1599b2eaf361055c0ac209043bcd5ec88c730d51f4c3b3fb82d71f12f12b",
"cultural-model.json": "f382dd695dbca05bd139e300976489834a08443b5da4c0f80eb6caa66a6762e8",
"cultural-report.html": "26fdad18d3624ef71af77135469d2fae923af6fba55c5141ff49c6d97f8641ec",
"recovered.json": "50b26096f64ea9f96927b219b0f47342a72a8ab87f0574d561aea27a001ace33",
"report-qa.png": "ff0109940bd416c9cea91ea207c46d908fa82f9342220afa6f2e91dbfa294612",
"source-9.png": "97bf79b8ac52dfd9068deca630afbd87094da8f030e3565e9fc71cb1fc33a1bf",
"ui-calculated.json": "0a1899f5a1d439403869977b80da259d87572d31ce3b2f2fb91606531467a3ce"
}
}
}
@@ -1,5 +1,26 @@
# Rastreabilidade, utilização e formação
## 24/10/2026 — G2C cultural/associativo executável em QA
**Três ramos P013–P015, cinco tratamentos condicionais, candidata CANDIDATE_G2C; L1 aberto.** Continuação do agrupamento G2 existente, sobre a entrega490527b9/recibo dacda11c. Os12ramos entregues permanecem encerrados; não foram recalculados nem repetidos os seus ensaios. A homologação não foi alterada.
| ID | Operação e condições específicas | Variantes / resultado |
| --- | --- | --- |
| P013 | Visitas guiadas ou não a museus, galerias, monumentos, parques, perímetros florestais, jardins botânicos/zoológicos e similares; pertença a entidade elegível; prestação pela própria entidade e próprios agentes; autorização do organismo sem finalidade lucrativa quando aplicável; bens apenas estreitamente conexos | Pública / sem finalidade lucrativa; campo07 |
| P014 | Cedência de pessoal por instituição religiosa **ou filosófica**, para actividade isenta no CIVA com fundamento identificado **ou assistência espiritual** | Pública / sem finalidade lucrativa; campo07 |
| P015 | Serviços no interesse colectivo dos associados; fins enumerados em9(2)(g); única contraprestação quota fixada nos estatutos | Sem finalidade lucrativa; campo07 |
**Fonte e período:** CIVA republicado pela Lei13/2016, PDF9, página1426(267), artigo9(2)(e–g); PDF15, página1426(273),19(1). Lei22/2022, artigo1, PDF1/p.2252(25), mantém estas alíneas; PDF2/p.2252(26),10(1–2), define entidade pública e as quatro condições cumulativas dos organismos sem finalidade lucrativa. Reutilizado o cotejo registado das alterações posteriores: Lei16/2020 altera9(13), não este recorte; Lei10/2025 não altera9/10 nem19(1). Aplicação técnica limitada a Janeiro–Dezembro2026/MZN, sem cálculo histórico presumido. Originais já existentes no arquivo de fontes; não se importou nem publicou um documento novo.
**Dados e execução:** reutilizam-se contexto da oportunidade, localização, moeda, montante, representação do preço e período; os campos existentes recolhem apenas natureza/entidade, condições específicas e documento ou pressuposto. Não foi reconstruído o simulador nem alterado schema, API, permissão ou regra fiscal de servidor. As quatro condições10(2) reutilizadas são cumulativas: lucros/gestão, escrituração, preços e não concorrência. Ausência de condição ou fundamento dá resultado indeterminado, nunca isenção presumida. Isenção sem direito à dedução nesta operação não constitui isenção geral da entidade nem dedução automática de compras.
**Provas dirigidas:** dois testesfrontend novos cobrem cinco variantes, preços líquidos/incluídos, ausência individual de cada condição, fundamento vazio, moeda/período fora da cobertura, soma500,00MZN no07 e isolamento do relatório. Fixture congelada da prova G2B preserva integralmente os três livros anteriores e respectivas chaves actuais, sem recalcular resultados aceites. API privada POST→GET passou na base QA isolada `simulator_fiscal_delta_20261021_test:55441`, com transacção revertida,1teste/14asserções e guarda do destino. Interface real emQA: retirar quota estatutária→indeterminado; repor→calcular/gravar revisão3; guardar payload real→API→recuperar→relatórioHTML real. O relatório confere cinco linhas,100,00MZN cada,IVA0,00,07=500,00,opportunity:312/Manica,2026-04,origem/artigos/versão. Tipos e compilação passaram. As provas Word/PDF aceites não foram regeneradas: este incremento não altera renderizadores nem representação de fracções; não se declara nova prova de paginação desses formatos.
**Saldo sem fusão de IDs:**133=12entregues+3demonstradosQA+117funcionaispendentes+P001documental. G2 tem32ramos:8entregues+3QA+21pendentes. No conjunto do motor há30tratamentos condicionais (dez iniciais+vinte dos grupos) e duas opções indeterminadas. P001 permanece documental; P016/festas de angariação e restantes grupos continuam nas respectivas dependências, sem bloquear G2C. Estatísticas/IA adiadas.
**Entrega futura:** delta funcional limitado ao catálogo `iva-operations.ts`, activado com o componente existente, sem migração/importação. Reversão deste incremento pelo Git pode retirar as cinco opções conservando leitores, snapshot e histórico; resultados já guardados exigem conservar o catálogo para continuar a ser lidos como actuais, pelo que contenção preferida é suspender entradas por alteração posterior explícita, não eliminar as regras usadas. Não se executou reversão nem nova disponibilização. Artefactos privados deQA em `outputs/iva-l1-g2-cultural-20261024/`.
## 24/10/2026 — entrega conjunta L1 instalada e delta conferido na homologação
**Fecho parcial utilizável instalado em `490527b9`**, candidata funcional `4a35d63c`. Conjunto: motor/classificação L1 inicial, fracções exactas e leitores, G1, G2A e G2B. A activação ocorreu pelo Git/webhook/instalador existentes, sem migrações, importações, publicação/activação de regras fiscais de servidor, alteração de permissões ou de consulta pública. Não equivale ao apuramento integral do IVA nem ao fecho de L1.
File diff suppressed because one or more lines are too long
+11
View File
@@ -22,7 +22,18 @@ const nonprofitConditions = [
function socialTreatment(id:string,label:string,article:string,conditions:string[],nonprofit:boolean):OperationTreatment {
return {id,label,field:'07',rate:0,article:`CIVA arts.9(2)(${article}),10(${nonprofit?'2':'1'}),19(1)`,source:'CIVA republicado Lei13/2016, PDF9, p.1426(267), art.9(2)(a/c/d); PDF15, p.1426(273),19(1); Lei22/2022 art.1, PDF2, p.2252(26),10(1–2). Âmbito técnico2026/MZN',conditions:[internalOperation,...(nonprofit?nonprofitConditions:['Entidade pública: órgão ou instituição da Administração Pública que desempenha funções administrativas do Estado, nos termos da legislação aplicável']),...conditions]};
}
const culturalSource = 'CIVA republicado Lei13/2016, PDF9, p.1426(267), art.9(2)(e–g); PDF15, p.1426(273),19(1); Lei22/2022 art.1, PDF1, p.2252(25), mantém9(2)(e–g), e PDF2, p.2252(26),10(1–2). Âmbito técnico2026/MZN';
const culturalTreatments:OperationTreatment[] = ([false,true] as const).flatMap(nonprofit=>{
const suffix=nonprofit?'nonprofit':'public',entity=nonprofit?'organismo sem finalidade lucrativa':'entidade pública';
const make=(id:string,label:string,article:string,conditions:string[]):OperationTreatment=>({...socialTreatment(id,label,article,conditions,nonprofit),source:culturalSource});
return [
make(`cultural_visits_${suffix}`,`Visitas culturais/naturais — ${entity}`,'e',['Visita, guiada ou não, a museu, galeria de arte, monumento, parque, perímetro florestal, jardim botânico/zoológico ou similar pertencente ao Estado, a entidade pública ou a entidade sem finalidade lucrativa','Serviço efectuado pela própria entidade, por intermédio dos seus próprios agentes; organismo sem finalidade lucrativa devidamente autorizado quando aplicável','Se houver bens, são transmissões estreitamente conexas com essa visita; não venda autónoma nem serviço de intermediário']),
make(`religious_personnel_${suffix}`,`Cedência de pessoal religiosa/filosófica — ${entity}`,'f',['Cedência de pessoal efectuada por instituição religiosa ou filosófica identificada','Destino identificado: actividade isenta nos termos do CIVA, com fundamento específico, ou assistência espiritual; não qualquer fornecimento de mão-de-obra']),
];
});
culturalTreatments.push({id:'statutory_membership',label:'Serviços colectivos aos associados por quota estatutária',field:'07',rate:0,article:'CIVA arts.9(2)(g),10(2),19(1)',source:culturalSource,conditions:[internalOperation,...nonprofitConditions,'Serviços efectuados no interesse colectivo dos próprios associados por organismo sem finalidade lucrativa','Objectivos de natureza política, sindical, religiosa, patriótica, filantrópica, recreativa, desportiva, cultural, cívica ou de representação de interesses económicos, identificados','A única contraprestação é uma quota fixada nos respectivos estatutos; não pagamentos adicionais por serviços individuais']});
export const operationTreatments:OperationTreatment[] = [
...culturalTreatments,
{id:'general',label:'Operação interna geral',field:'01',rate:16,article:'CIVA arts.1,6,15(1),17(1–2); art.34 (Lei10/2025)',source:`${source2016}; ${source2022}; ${source2025}`,conditions:['Operação localizada e tributável em Moçambique, no regime normal','Contraprestação completa; sem base especial do art.15(2), isenção ou taxa reduzida']},
{id:'private_health',label:'Serviços médicos e sanitários privados',field:'03',rate:5,article:'CIVA arts.15(10),17-A(a)',source:source2022,conditions:['Serviço médico/sanitário ou operação estreitamente conexa','Prestador hospitalar privado, clínica, dispensário ou similar']},
{id:'private_education',label:'Ensino privado reconhecido',field:'03',rate:5,article:'CIVA arts.15(10),17-A(b)',source:source2022,conditions:['Ensino ou bens/serviços conexos prestados pelo estabelecimento','Estabelecimento privado integrado no Sistema Nacional de Ensino e reconhecido pelo Ministério competente']},
+32
View File
@@ -0,0 +1,32 @@
<?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 IvaCulturalDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_cultural_context_preserves_accepted_opportunities(): 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(getenv('IVA_G2C_INPUT') ?: base_path('outputs/iva-l1-g2-cultural-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']);
$book = json_decode($recovered['modelo_a_snapshot'], true)['salesOperations']['opportunity:312'];
$this->assertCount(5, $book['result']['rows']);
$this->assertSame('500.00', $book['result']['periods'][0]['fields']['07']);
$this->assertSame(json_decode($draft['modelo_a_snapshot'], true)['salesOperations']['opportunity:310'], json_decode($recovered['modelo_a_snapshot'], true)['salesOperations']['opportunity:310']);
$this->assertSame(json_decode($draft['modelo_a_snapshot'], true)['salesOperations']['opportunity:309'], json_decode($recovered['modelo_a_snapshot'], true)['salesOperations']['opportunity:309']);
foreach ($book['result']['rows'] as $row) $this->assertSame('0.00', $row['vat']);
if (getenv('IVA_G2C_RECOVERED')) file_put_contents(getenv('IVA_G2C_RECOVERED'), json_encode($recovered, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
}
File diff suppressed because one or more lines are too long
+28
View File
@@ -0,0 +1,28 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import fs from 'node:fs';
import {operationTreatments,newSalesOperation,evaluateSalesOperation,recordSales,saveSalesBook,salesBook,salesCurrent} from '../../resources/js/lib/iva-operations.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
const ids=['cultural_visits_public','cultural_visits_nonprofit','religious_personnel_public','religious_personnel_nonprofit','statutory_membership'];
function seed(){return JSON.parse(fs.readFileSync(new URL('./fixtures/iva-social.json',import.meta.url),'utf8'));}
function fixture(){const draft=seed();draft.context_opportunity='opportunity:312';draft.project_title='SYNTHETIC QA — G2C social';
let book={currency:'MZN',rows:ids.map((id,i)=>({...newSalesOperation(`g2c-${i}`,'2026-04','MZN'),description:`SYNTHETIC QA ${id}`,establishment:'SYNTHETIC QA equipamento Manica',amount:'100',basis:i%2?'included':'net',treatment:id,confirmed:operationTreatments.find(t=>t.id===id).conditions.map((_,n)=>n),evidence:'SYNTHETIC QA — natureza, entidade, reconhecimento e condições cumulativas pressupostos documentados; sem certificação'})),periods:[{period:'2026-04',complete:true}]};book=recordSales(draft,book);draft.modelo_a_snapshot=saveSalesBook(draft,book);return {draft,book};}
test('G2C three branches have five entity-specific treatments and cumulative nonprofit conditions',()=>{
const {book}=fixture();for(const row of book.rows){const rule=operationTreatments.find(t=>t.id===row.treatment),r=evaluateSalesOperation(row,'MZN');assert.equal(r.field,'07');assert.equal(r.net,'100.00');assert.equal(r.vat,'0.00');assert.equal(r.gross,'100.00');assert.match(r.article,/10\(/);
assert.ok(rule.conditions.length>=4); if(row.treatment.endsWith('_nonprofit')||row.treatment==='statutory_membership')assert.ok(rule.conditions.length>=7);
for(const missing of row.confirmed)assert.equal(evaluateSalesOperation({...row,confirmed:row.confirmed.filter(n=>n!==missing)},'MZN').vat,null);
assert.equal(evaluateSalesOperation({...row,evidence:''},'MZN').vat,null); assert.equal(evaluateSalesOperation({...row,period:'2025-04'},'MZN').vat,null); assert.equal(evaluateSalesOperation(row,'USD').vat,null);
}
assert.equal(book.result.periods[0].fields['07'],'500.00');
});
test('new cultural context round-trips and reports only its operations, preserving three accepted books without recalculation',()=>{
const {draft,book}=fixture(),original=seed();const old=JSON.parse(original.modelo_a_snapshot).salesOperations;
const recovered=JSON.parse(JSON.stringify(draft)),store=JSON.parse(recovered.modelo_a_snapshot).salesOperations;
for(const id of ['opportunity:309','opportunity:310','opportunity:311']){assert.deepEqual(store[id],old[id]);assert.equal(salesCurrent({...recovered,context_opportunity:id},store[id]),true);}
assert.equal(salesCurrent(recovered,salesBook(recovered)),true);
const model=JSON.stringify(createProjectDossierReport(recovered,{selectedSections:new Set(['finance'])}));
for(const token of [...ids,'500.00','opportunity:312','Manica','2026-04','MZN'])assert.ok(model.includes(token),token);
for(const token of ['financial_stamp','patient_transport','social_assistance_public'])assert.equal(model.includes(token),false);
assert.equal(book.result.rows.length,5);assert.equal(draft.currency,original.currency);assert.deepEqual(draft.selection_labels,original.selection_labels);
});
if(process.env.IVA_G2C_OUT)fs.writeFileSync(process.env.IVA_G2C_OUT,JSON.stringify(fixture().draft,null,2));