Implement P131 P132 event timing and conditional P104 draft in QA

This commit is contained in:
2026-10-25 01:01:15 +02:00
parent 7a3ac43224
commit 7e6e462e12
9 changed files with 250 additions and 1 deletions
@@ -0,0 +1,50 @@
<?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 IvaCurrencyEventDraftTest extends TestCase
{
use DatabaseTransactions;
public function test_currencyevents_recover_privately_preserving_all_delivered_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('tests/frontend/fixtures/iva-currency-events.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:357', $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['currencyEvents']['opportunity:357'];
$this->assertCount(15, $book['rows']);
$this->assertSame('conditional',$book['result']['rows'][0]['status']);
$this->assertSame('64500.00',$book['result']['rows'][0]['scenarioAmount']);
$this->assertSame('2026-12-10',$book['result']['rows'][13]['eventDate']);
$this->assertSame(['200.00','800.00'],array_column($book['result']['rows'][14]['dueEvents'],'amount'));
foreach ($book['result']['rows'] as $row) {
$this->assertNull($row['baseAmount']);$this->assertNull($row['vat']);$this->assertNull($row['rate']);
}
$this->assertSame($book['rows'], $book['result']['inputs']);
$this->assertCount(1, $book['history']);
$this->assertSame('64500.00',$book['history'][0]['rows'][0]['scenarioAmount']);
$frozen = json_decode(file_get_contents(base_path('tests/frontend/fixtures/iva-currency-events-preserved.json')), true, flags: JSON_THROW_ON_ERROR);
$prior = json_decode($frozen['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
$this->assertSame($prior, array_diff_key($store, ['currencyEvents' => true]));
$this->assertSame('1250.30', $store['publicAuthorityBases']['opportunity:339']['result']['rows'][1]['baseAmount']);
$this->assertNull($store['publicAuthorityBases']['opportunity:339']['result']['rows'][1]['vat']);
$this->assertSame('400.00', $store['importExemptions']['opportunity:338']['result']['rows'][1]['importVat']);
$this->assertNull($store['books']['opportunity:329']['result']['periods'][0]['fields']['11']);
file_put_contents(base_path('outputs/iva-l1-currency-events-20261025/recovered.json'), json_encode($recovered, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,40 @@
import test from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';
import {newCurrencyEvent,currencyEventTreatments,evaluateCurrencyEvent,recordCurrencyEvents,saveCurrencyEventBook,currencyEventBook,currencyEventCurrent,currencyEventReportRows,switchCurrencyEvent} from '../../resources/js/lib/iva-currency-events.ts';
const frozen=JSON.parse(fs.readFileSync('tests/frontend/fixtures/iva-currency-events-preserved.json','utf8'));
const draft={...frozen,currency:'MZN',context_opportunity:'opportunity:357',project_title:'SYNTHETIC QA — Moeda e eventos / Manica'};
function row(t){return{...newCurrencyEvent(t,'MZN'),treatment:t,description:'SYNTHETIC QA '+t,period:'2026-12',origin:'projected',operationRef:'SYNTHETIC QA operação existente',documentRef:'SYNTHETIC QA documento/pressuposto',documentDate:'2026-12-12',scope:'SYNTHETIC QA conjunto/parte/operação/período',evidence:'SYNTHETIC QA pressupostos expressos',eventDate:'2026-12-10',eventRef:'SYNTHETIC QA evento específico/artigo7,excepções próprias conferidas',otherDate:t==='consignment_deadline'?'2026-06-13':t==='contract_effects'?'2026-12-01':'2026-12-12',reviewDate:'2026-12-31',factsConfirmed:'yes',invoiceRequired:'yes',invoiceState:'issued',invoiceDate:'2026-12-12',invoiceRef:'SYNTHETIC QA factura',legalDeadline:'2026-12-17',deadlineRef:'SYNTHETIC QA prazo/disposição/calendário identificados,não regra vigente presumida',paymentState:t==='advance_payment'?'received':'none',paymentDate:'2026-12-09',paymentRef:'SYNTHETIC QA recebimento identificado',amount:'1000.00',paymentAmount:'200.00',sourceCurrency:'USD',fxRate:'64.5',fxDate:'2026-12-10',fxSource:'SYNTHETIC QA taxa/exemplar',fxRuleRef:'SYNTHETIC QA pressuposto da taxa/data,não base fiscal',fxDirection:'mzn_per_unit'};}
for(const rule of currencyEventTreatments)test(rule.branch+' '+rule.id+' own event and noVAT/base',()=>{
const r=row(rule.id),v=evaluateCurrencyEvent(r);assert.equal(v.status,rule.branch==='P104'?'conditional':'determined');assert.equal(v.baseAmount,null);assert.equal(v.vat,null);assert.equal(v.rate,null);
if(rule.branch==='P104')assert.equal(v.scenarioAmount,'64500.00');else{assert.equal(v.dueEvents.at(-1).date,['import_clearance','import_auction'].includes(rule.id)?'2026-12-10':'2026-12-12');assert.equal(v.eventDate,rule.id==='advance_payment'?null:rule.id==='consignment_deadline'?'2026-12-10':'2026-12-10');}
for(const f of ['operationRef','documentRef','scope','evidence'])assert.equal(evaluateCurrencyEvent({...r,[f]:''}).status,'indeterminate');assert.equal(evaluateCurrencyEvent({...r,factsConfirmed:''}).status,'indeterminate');assert.equal(evaluateCurrencyEvent({...r,factsConfirmed:'no'}).status,'not_applicable');
});
test('P131/P132 invoice intime/late/absent,partial payment and no duplicate remainder',()=>{
const r=row('goods_available');let v=evaluateCurrencyEvent({...r,paymentState:'received'});assert.deepEqual(v.dueEvents.map(x=>[x.date,x.amount]),[['2026-12-09','200.00'],['2026-12-12','800.00']]);
v=evaluateCurrencyEvent({...r,invoiceDate:'2026-12-20'});assert.equal(v.dueEvents[0].date,'2026-12-17');
v=evaluateCurrencyEvent({...r,invoiceState:'not_issued'});assert.equal(v.dueEvents[0].date,'2026-12-17');
v=evaluateCurrencyEvent({...r,invoiceState:'not_issued',reviewDate:'2026-12-11',paymentState:'received'});assert.equal(v.dueEvents.length,1);assert.equal(v.remainingPending,true);
v=evaluateCurrencyEvent({...r,paymentState:'received',paymentDate:'2026-12-19',invoiceDate:'2026-12-20'});assert.deepEqual(v.dueEvents.map(x=>[x.date,x.amount]),[['2026-12-17','1000.00']]);
v=evaluateCurrencyEvent({...r,paymentState:'received',paymentAmount:'1000'});assert.equal(v.dueEvents.length,1);assert.equal(v.dueEvents[0].amount,'1000.00');
for(const patch of [{paymentState:''},{legalDeadline:''},{deadlineRef:''},{invoiceState:''},{paymentState:'received',paymentAmount:'1001'},{paymentState:'received',amount:''}])assert.equal(evaluateCurrencyEvent({...r,...patch}).status,'indeterminate');
assert.equal(evaluateCurrencyEvent({...r,invoiceRequired:'no',amount:''}).dueEvents[0].amount,null);
assert.equal(evaluateCurrencyEvent({...r,eventDate:'2026-12-20',reviewDate:'2026-12-11',invoiceState:'not_issued',legalDeadline:'2026-12-28',paymentState:'received'}).dueEvents[0].date,'2026-12-09');
});
test('own events,consignmentterm,servicecharge,contractexceptions and dates',()=>{
assert.equal(evaluateCurrencyEvent({...row('consignment_deadline'),otherDate:''}).status,'indeterminate');
assert.equal(evaluateCurrencyEvent({...row('consignment_deadline'),invoiceRequired:'no'}).dueEvents[0].date,'2026-12-10');
assert.equal(evaluateCurrencyEvent({...row('service_early_charge'),otherDate:'2026-12-09'}).status,'indeterminate');
assert.equal(evaluateCurrencyEvent({...row('contract_effects'),otherDate:'2026-12-11'}).status,'indeterminate');
for(const field of ['reviewDate','eventDate','documentDate'])assert.equal(evaluateCurrencyEvent({...row('goods_available'),[field]:'2026-02-30'}).status,'indeterminate');
const r=row('own_goods'),sw=switchCurrencyEvent(r,'foreign_scenario');assert.equal(sw.eventDate,r.eventDate);assert.equal(sw.fxSource,r.fxSource);assert.equal(sw.factsConfirmed,'');
});
test('P104 conditionalconversion direction/exactrounding/missingparams never fiscalbase',()=>{
const r=row('foreign_scenario');assert.equal(evaluateCurrencyEvent({...r,fxDirection:'units_per_mzn',fxRate:'2',amount:'100'}).scenarioAmount,'50.00');
assert.equal(evaluateCurrencyEvent({...r,amount:'0.000001',fxRate:'1.5'}).scenarioAmount,'0.000002');
for(const patch of [{fxRate:'0'},{fxRate:'-1'},{fxSource:''},{fxDate:''},{fxRuleRef:''},{fxDirection:''},{sourceCurrency:'MZN'}])assert.equal(evaluateCurrencyEvent({...r,...patch}).status,'indeterminate');assert.equal(evaluateCurrencyEvent(r).baseAmount,null);
});
test('snapshot/history/report preserve every delivered book and contextual scope',()=>{
const rows=currencyEventTreatments.map(t=>row(t.id)),a=recordCurrencyEvents(draft,{rows}),next={...draft,modelo_a_snapshot:saveCurrencyEventBook(draft,a)},b=recordCurrencyEvents(next,a);next.modelo_a_snapshot=saveCurrencyEventBook(next,b);
assert.equal(b.history.length,1);assert.equal(currencyEventCurrent(next,b),true);assert.equal(currencyEventCurrent(next,{...b,rows:rows.slice(1)}),false);assert.equal(currencyEventReportRows(next).length,15);assert.deepEqual(currencyEventBook({...next,context_opportunity:'opportunity:358'}),{rows:[]});
const store=JSON.parse(next.modelo_a_snapshot);delete store.currencyEvents;assert.deepEqual(store,JSON.parse(frozen.modelo_a_snapshot));assert.throws(()=>recordCurrencyEvents(next,{rows:[rows[0],rows[0]]}));assert.throws(()=>currencyEventBook({...next,modelo_a_snapshot:JSON.stringify({...store,currencyEvents:[]})}));
if(process.env.IVA_CURRENCY_EVENTS_FIXTURE)fs.writeFileSync(process.env.IVA_CURRENCY_EVENTS_FIXTURE,JSON.stringify(next,null,2)+'\n');
});