feat: preserve exact included VAT fractions and enumerate L1 remainder

This commit is contained in:
2026-10-24 13:20:32 +02:00
parent d89912e579
commit 689e7cbe8b
12 changed files with 1595 additions and 19 deletions
+38
View File
@@ -0,0 +1,38 @@
<?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 IvaRationalDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_periodic_results_and_accepted_first_opportunity_round_trip_exactly(): void
{
$identity = DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test', $identity->name);
$this->assertSame(55441, (int) $identity->port);
config(['investor-path.enabled' => true, 'suitecrm.enabled' => false]);
$this->actingAs(User::factory()->create(['email_verified_at' => now()]));
$path = getenv('IVA_RATIONAL_INPUT') ?: base_path('tests/frontend/fixtures/iva-rational.json');
$draft = json_decode(file_get_contents($path), true, flags: JSON_THROW_ON_ERROR);
$store = json_decode($draft['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
$first = $store['salesOperations']['opportunity:309'];
$id = $this->postJson('/api/v1/simulation-drafts', ['personal_snapshot' => [], 'economic_snapshot' => $draft], ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id');
$economic = $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk()->json('data.economic_snapshot');
$this->assertSame($draft['modelo_a_snapshot'], $economic['modelo_a_snapshot']);
$saved = json_decode($economic['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
$this->assertSame($first, $saved['salesOperations']['opportunity:309']);
$second = $saved['salesOperations']['opportunity:310']['result'];
$this->assertSame('iva-operations-l1/2', $second['version']);
$this->assertSame(['numerator' => '20', 'denominator' => '21'], $second['rows'][0]['exact']['net']);
$this->assertSame(['numerator' => '1', 'denominator' => '21'], $second['rows'][0]['exact']['vat']);
$this->assertSame('0.047619', $second['rows'][0]['vat']);
if (getenv('IVA_RATIONAL_RECOVERED')) file_put_contents(getenv('IVA_RATIONAL_RECOVERED'), json_encode($economic, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
}
@@ -0,0 +1,9 @@
import fs from 'node:fs';
import {newSalesOperation,recordSales,saveSalesBook} from '../../../resources/js/lib/iva-operations.ts';
export function l1DeltaFixture(){
const draft=JSON.parse(fs.readFileSync(new URL('iva-operations.json',import.meta.url),'utf8'));
draft.context_opportunity='opportunity:310';draft.project_title='SYNTHETIC QA — L1 precisão racional';
const context=JSON.parse(draft.context_applicability_snapshot);context.opportunity_choices.push({opportunity:{key:'opportunity:310',title:'SYNTHETIC QA — segunda oportunidade racional'}});draft.context_applicability_snapshot=JSON.stringify(context);
let book={currency:'MZN',rows:['1','100','0.02'].map((amount,i)=>({...newSalesOperation(`rational-${i}`,'2026-01','MZN'),amount,description:`SYNTHETIC QA periódico ${i+1}`,establishment:'SYNTHETIC QA estabelecimento',basis:'included',treatment:'private_health',confirmed:[0,1],evidence:'SYNTHETIC QA — pressupostos já delimitados do tratamento, preço incluído'})),periods:[{period:'2026-01',complete:true}]};
book=recordSales(draft,book);draft.modelo_a_snapshot=saveSalesBook(draft,book);return {draft,book};
}
File diff suppressed because one or more lines are too long
+40
View File
@@ -0,0 +1,40 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import fs from 'node:fs';
import {l1DeltaFixture} from './fixtures/iva-l1-delta-fixture.mjs';
import {calculateSales,evaluateSalesOperation,modeloA06Correspondence,salesBook,salesCurrent,saveSalesBook} from '../../resources/js/lib/iva-operations.ts';
import {calculateAcquisitions,saveAcquisitionBook} from '../../resources/js/lib/iva-acquisitions.ts';
import {addFractions,decimalFraction,fraction,fractionLabel,fractionPresentation} from '../../resources/js/lib/fiscal-rational.ts';
import {createProjectDossierReport} from '../../resources/js/lib/project-dossier-report.ts';
test('periodic included5% retains exact rational amounts; only presentation is rounded',()=>{
const {book}=l1DeltaFixture(),r=book.result.rows[0];
assert.deepEqual(r.exact.net,{numerator:'20',denominator:'21'});assert.deepEqual(r.exact.vat,{numerator:'1',denominator:'21'});
assert.equal(r.net,'0.952381');assert.equal(r.vat,'0.047619');assert.equal(r.status,'calculated');assert.match(r.reason,/apresentação técnica/);
assert.deepEqual(addFractions([r.exact.net,r.exact.vat]),r.exact.gross);
assert.deepEqual(book.result.rows[1].exact.vat,{numerator:'100',denominator:'21'});
});
test('aggregation uses exact fractions, not21 rounded values',()=>{
const {draft,book}=l1DeltaFixture();book.rows=Array.from({length:21},(_,i)=>({...book.rows[0],id:`q${i}`}));const r=calculateSales(draft,book);
assert.equal(r.rows.every(row=>row.vat==='0.047619'),true);assert.equal(r.periods[0].fields['04'],'1.00');assert.deepEqual(r.periods[0].exactFields['04'],{numerator:'1',denominator:'1'});assert.equal(r.periods[0].fields['03'],'20.00');
});
test('new version round trips by opportunity, keeping accepted first snapshot untouched',()=>{
const accepted=JSON.parse(fs.readFileSync(new URL('./fixtures/iva-operations.json',import.meta.url),'utf8'));const original=JSON.parse(accepted.modelo_a_snapshot).salesOperations['opportunity:309'];
const {draft,book}=l1DeltaFixture();const saved=JSON.parse(JSON.stringify({...draft,modelo_a_snapshot:saveSalesBook(draft,book)}));
assert.deepEqual(JSON.parse(saved.modelo_a_snapshot).salesOperations['opportunity:309'],original);assert.equal(salesCurrent(saved,salesBook(saved)),true);assert.deepEqual(salesBook(saved).result.rows[2].exact,book.result.rows[2].exact);
});
test('conditional14-18 and report carry exact precision rather than rounded line amounts',()=>{
const {draft,book}=l1DeltaFixture();const a={currency:'MZN',rows:[],periods:['2026-01','2026-02','2026-03','2026-04'].map(period=>({period,complete:true,absent:['03','04','05','06','07','11','12','13']}))};
const result=calculateAcquisitions(draft,a);assert.equal(fractionLabel(result.periods[0].exactFields['17']),'5051/1050');assert.equal(result.periods[0].fields['17'],'4.810476');
a.result=result;draft.modelo_a_snapshot=saveAcquisitionBook(draft,a);const report=JSON.stringify(createProjectDossierReport(draft,{selectedSections:new Set(['finance'])}));assert.match(report,/5051\/1050/);assert.match(report,/20\/21/);assert.match(report,/opportunity:310/);
assert.match(report,/apresentação técnica/);assert.equal(book.result.rows.length,3);
});
test('missing inputs still block only the affected result; invalid fractions do not become zero',()=>{
const {book}=l1DeltaFixture();assert.equal(evaluateSalesOperation({...book.rows[0],amount:''},'MZN').vat,null);
assert.equal(evaluateSalesOperation({...book.rows[0],basis:''},'MZN').vat,null);assert.equal(evaluateSalesOperation({...book.rows[0],amount:'0'},'MZN').vat,'0.00');
const {draft}=l1DeltaFixture();book.rows[0].sourceAmount='1';const stale=calculateSales(draft,book).rows[0];assert.equal(stale.vat,null);assert.equal(stale.exact,undefined);
assert.throws(()=>fractionPresentation({numerator:'1',denominator:'0'}));assert.equal(fractionPresentation(fraction(1n,21n)).approximate,true);assert.equal(fractionPresentation(decimalFraction('105')).approximate,false);
});
test('form06 is not automatically inferred from a foreign operation or old stock instructions',()=>{
const {book}=l1DeltaFixture();const r=evaluateSalesOperation({...book.rows[0],treatment:'foreign_operation'},'MZN');assert.equal(r.field,null);assert.equal(r.vat,null);assert.match(modeloA06Correspondence,/existências/);assert.match(modeloA06Correspondence,/separe05\/06/);
});
if(process.env.IVA_L1_DELTA_OUT)fs.writeFileSync(process.env.IVA_L1_DELTA_OUT,JSON.stringify(l1DeltaFixture().draft,null,2));
+2 -2
View File
@@ -20,13 +20,13 @@ test('each enumerated treatment calculates only with its conditions, article and
}
const by=Object.fromEntries(book.rows.map(r=>[r.treatment,evaluateSalesOperation(r,'MZN')]));assert.equal(by.private_health.vat,'5.00');assert.equal(by.export_goods.field,'05');assert.match(by.export_goods.reason,/não é taxa zero/);assert.equal(by.housing_rent.field,'07');
});
test('VAT-included general uses statutory unit rounding, conserves gross; 5 percent included is explicitly unresolved',()=>{
test('VAT-included general uses statutory unit rounding, conserves gross; included5percent presentation is covered by the rational delta',()=>{
const row=operationsFixture().book.rows[0];const r=evaluateSalesOperation(row,'MZN');assert.equal(r.net,'1001.00');assert.equal(r.vat,'160.00');assert.equal(r.gross,'1161.00');
assert.equal(evaluateSalesOperation({...row,amount:'1160'},'MZN').net,'1000.00');
assert.equal(evaluateSalesOperation({...row,basis:'net',amount:'1000.03'},'MZN').vat,'160.0048');
assert.equal(evaluateSalesOperation({...row,basis:'',amount:'0'},'MZN').vat,null);
assert.equal(evaluateSalesOperation({...row,amount:'0'},'MZN').vat,'0.00');
const reduced=operationsFixture().book.rows[1];assert.match(evaluateSalesOperation({...reduced,basis:'included'},'MZN').reason,/arredondamento/);
const reduced=operationsFixture().book.rows[1];assert.equal(evaluateSalesOperation({...reduced,basis:'included'},'MZN').vat,'4.761905');
assert.equal(evaluateSalesOperation({...reduced,basis:'included',amount:'105'},'MZN').net,'100.00');
assert.equal(evaluateSalesOperation({...reduced,basis:'included',amount:'105'},'MZN').vat,'5.00');
});