feat(iva): add own-production exemptions and dated tax options in QA

This commit is contained in:
2026-10-24 15:31:27 +02:00
parent 7696af6814
commit fe69a16eea
9 changed files with 225 additions and 8 deletions
+36
View File
@@ -0,0 +1,36 @@
<?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 IvaProductionDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_production_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_G2I_INPUT') ?: base_path('outputs/iva-l1-g2-production-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:318'];
$this->assertCount(7, $book['result']['rows']);
$this->assertSame('300.00', $book['result']['periods'][0]['fields']['07']);
$this->assertSame('100.00', $book['result']['periods'][0]['fields']['05']);
$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']);
$this->assertSame('300.00', $book['result']['periods'][0]['fields']['01']);
$this->assertSame('48.00', $book['result']['periods'][0]['fields']['02']);
$this->assertSame('2024-12-15', $book['rows'][2]['taxOption']['declarationDate']);
$this->assertSame('2026-10-15', $book['rows'][2]['taxOption']['operationDate']);
if (getenv('IVA_G2I_RECOVERED')) file_put_contents(getenv('IVA_G2I_RECOVERED'), json_encode($recovered, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
}
File diff suppressed because one or more lines are too long
+50
View File
@@ -0,0 +1,50 @@
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=['production_agriculture', 'production_forestry', 'production_forestry_opted', 'production_livestock', 'production_livestock_opted', 'production_fishing', 'production_fishing_opted'];
function seed(){return JSON.parse(fs.readFileSync(new URL('./fixtures/iva-listed.json',import.meta.url),'utf8'));}
function fixture(){const draft=seed();draft.context_opportunity='opportunity:318';draft.project_title='SYNTHETIC QA — G2I social';
let book={currency:'MZN',rows:ids.map((id,i)=>({...newSalesOperation(`g2i-${i}`,'2026-10','MZN'),description:`SYNTHETIC QA ${id}`,establishment:'SYNTHETIC QA equipamento Manica',amount:id.endsWith('_opted')&&i%3===2?'116':'100',basis:i%3===2?'included':'net',treatment:id,...(id.endsWith('_opted')?{taxOption:{mode:'annual',declarationDate:'2024-12-15',startupDate:'',returnStatus:'none',returnDeclarationDate:'',operationDate:'2026-10-15'}}:{}),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-10',complete:true}]};book=recordSales(draft,book);draft.modelo_a_snapshot=saveSalesBook(draft,book);return {draft,book};}
test('G2I four production branches with seven exemption or opted treatments require their own operation and eligibility 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,row.treatment.endsWith('_opted')?'01':row.treatment==='production_agriculture'?'05':'07');assert.equal(r.net,'100.00');assert.equal(r.vat,row.treatment.endsWith('_opted')?'16.00':'0.00');assert.equal(r.gross,row.treatment.endsWith('_opted')?'116.00':'100.00');assert.match(r.article,/9\(7\)/);
assert.ok(rule.conditions.length>=3);
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,treatment:'listed_banana'},'MZN').vat,null); assert.equal(evaluateSalesOperation({...row,period:'2025-10'},'MZN').vat,null); assert.equal(evaluateSalesOperation(row,'USD').vat,null);
}
assert.equal(book.result.periods[0].fields['07'],'300.00');assert.equal(book.result.periods[0].fields['05'],'100.00');assert.equal(book.result.periods[0].fields['01'],'300.00');assert.equal(book.result.periods[0].fields['02'],'48.00');
});
test('new production context round-trips and reports only its operations, preserving nine 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','opportunity:312','opportunity:313','opportunity:314','opportunity:315','opportunity:316','opportunity:317']){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,'48.00','opportunity:318','Manica','2026-10','MZN'])assert.ok(model.includes(token),token);
for(const token of ['financial_stamp','patient_transport','social_assistance_public','statutory_membership','author_rights','industrial_feed','special_tax_games','public_security','listed_maize'])assert.equal(model.includes(token),false);
assert.equal(book.result.rows.length,7);assert.equal(draft.currency,original.currency);assert.deepEqual(draft.selection_labels,original.selection_labels);
});
test('article11 distinguishes annual effects, startup, five-year minimum and effective return without automatic expiry',()=>{
const {draft,book}=fixture(),row=book.rows.find(r=>r.treatment==='production_forestry_opted');
const check=(patch)=>evaluateSalesOperation({...row,taxOption:{...row.taxOption,...('returnDeclarationDate' in patch?{returnStatus:patch.returnDeclarationDate?'declared':'none'}:{}),...patch}},'MZN');
assert.equal(check({declarationDate:'2026-01-01'}).vat,null);
assert.match(check({declarationDate:'2026-01-01'}).reason,/2027-01-01/);
assert.equal(check({mode:'startup',declarationDate:'2026-09-20',startupDate:'2026-10-01'}).vat,'16.00');
assert.equal(check({mode:'startup',declarationDate:'2026-10-02',startupDate:'2026-10-01'}).vat,null);
assert.equal(check({declarationDate:'2020-01-01',returnDeclarationDate:'2025-12-31'}).vat,null);
assert.equal(check({declarationDate:'2019-12-31',returnDeclarationDate:'2025-01-01'}).vat,null);
assert.match(check({declarationDate:'2019-12-31',returnDeclarationDate:'2025-01-01'}).reason,/2026-01-01/);
assert.equal(check({declarationDate:'2020-01-01',returnDeclarationDate:'2026-01-01'}).vat,'16.00');
assert.equal(check({declarationDate:'2019-12-31',returnDeclarationDate:''}).vat,'16.00');
for(const patch of [{mode:''},{returnStatus:''},{returnStatus:'declared',returnDeclarationDate:''},{returnStatus:'none',returnDeclarationDate:'2026-01-01'},{declarationDate:''},{declarationDate:'2026-99-99'},{declarationDate:'2024-02-30'},{operationDate:'2026-11-01'},{operationDate:''},{returnDeclarationDate:'not-a-date'}])assert.equal(check(patch).vat,null);
assert.equal(check({mode:'startup',declarationDate:'2020-02-28',startupDate:'2020-02-29',returnDeclarationDate:'2025-02-28'}).vat,null);
assert.match(check({mode:'startup',declarationDate:'2020-02-28',startupDate:'2020-02-29',returnDeclarationDate:'2025-03-01'}).reason,/regresso.*2026-01-01/);
assert.equal(check({mode:'startup',declarationDate:'2020-02-28',startupDate:'2020-02-29',returnDeclarationDate:'2026-03-01'}).vat,'16.00');
assert.equal(operationTreatments.some(t=>t.id==='production_agriculture_opted'),false);
const changed={...book,rows:book.rows.map(r=>r.id===row.id?{...r,taxOption:{...r.taxOption,declarationDate:'2025-12-31'}}:r)};
assert.equal(salesCurrent(draft,changed),false);assert.deepEqual(changed.result,book.result);
const report=JSON.stringify(createProjectDossierReport(draft,{selectedSections:new Set(['finance'])}));
for(const token of ['2024-12-15','2025-01-01','2026-10-15','ausência de declaração de regresso'])assert.ok(report.includes(token),token);
});
if(process.env.IVA_G2I_OUT)fs.writeFileSync(process.env.IVA_G2I_OUT,JSON.stringify(fixture().draft,null,2));