feat(iva): distinguish offer requests emergency drafts and conditional scenarios
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
<?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 IvaOfferPathsDraftTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function test_import_classification_recovers_privately_without_changing_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-offer-paths.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:334', $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['offerPreparations']['opportunity:334'];
|
||||
$this->assertCount(2, $book['rows']);
|
||||
$this->assertSame(['P084','P085'], array_column($book['rows'], 'branch'));
|
||||
$this->assertSame(['import','acquisition'], array_column($book['rows'], 'operation'));
|
||||
$this->assertArrayNotHasKey('result', $book);
|
||||
$this->assertCount(3, $book['history']);
|
||||
$this->assertSame('SYNTHETIC QA finalidade editada deliberadamente', $book['saved']['rows'][0]['purpose']);
|
||||
$this->assertSame('SYNTHETIC QA cheias', $book['rows'][1]['disaster']);
|
||||
$frozen = json_decode(file_get_contents(base_path('tests/frontend/fixtures/iva-g6e-inputs.json')), true, flags: JSON_THROW_ON_ERROR);
|
||||
$prior = json_decode($frozen['modelo_a_snapshot'], true, flags: JSON_THROW_ON_ERROR);
|
||||
$this->assertSame($prior['salesOperations'], $store['salesOperations']);
|
||||
$this->assertSame($prior['books'], $store['books']);
|
||||
$this->assertSame($prior['importExemptions']['opportunity:330'], $store['importExemptions']['opportunity:330']);
|
||||
$this->assertSame($prior['importExemptions']['opportunity:331'], $store['importExemptions']['opportunity:331']);
|
||||
$this->assertSame($prior['importExemptions']['opportunity:332'], $store['importExemptions']['opportunity:332']);
|
||||
$this->assertSame($prior['importExemptions'], $store['importExemptions']);
|
||||
$this->assertSame('presented', $book['rows'][0]['request']['state']);
|
||||
$this->assertSame('granted', $book['rows'][0]['requestRecords'][0]['request']['state']);
|
||||
$this->assertSame('SYNTHETIC QA apenas bens A', $book['rows'][0]['requestRecords'][0]['request']['goods']);
|
||||
$this->assertSame('2026-11', $book['rows'][0]['requestRecords'][0]['request']['periodTo']);
|
||||
$this->assertSame('SYNTHETIC QA assistência imediata às vítimas das cheias', $book['rows'][1]['humanitarianPurpose']);
|
||||
$this->assertSame('800', $book['rows'][0]['normalVat']);
|
||||
$this->assertSame('yes', $book['rows'][1]['conditionalAssumption']);
|
||||
$this->assertArrayNotHasKey('appliedBenefit', $book);
|
||||
$this->assertNull($store['books']['opportunity:329']['result']['periods'][0]['fields']['11']);
|
||||
$this->assertNull($store['books']['opportunity:329']['result']['periods'][0]['fields']['17']);
|
||||
$this->assertStringContainsString('BR89', $store['books']['opportunity:329']['result']['sources']);
|
||||
file_put_contents(base_path('outputs/iva-offer-paths-20261024/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,33 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import { offerPreparationBook,saveOfferPreparationBook,recordOfferRequest,emptyOfferRequest,offerScenarios,offerPathReport,recordOfferPreparations } from '../../resources/js/lib/iva-offer-preparations.ts';
|
||||
import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts';
|
||||
const frozen=JSON.parse(fs.readFileSync(new URL('./fixtures/iva-g6e-inputs.json',import.meta.url),'utf8'));
|
||||
const draft=()=>({...structuredClone(frozen),context_opportunity:'opportunity:334',project_title:'SYNTHETIC QA — Offer paths'});
|
||||
const row84=()=>({...offerPreparationBook(draft()).rows[0],normalVat:'800',normalVatSource:'SYNTHETIC QA pressuposto expresso, não taxa inferida',conditionalAssumption:'yes'});
|
||||
test('presented request never grants benefit;normal amount requires own source and conditional scenario explicit material assumptions',()=>{
|
||||
const row={...row84(),request:{...emptyOfferRequest(),state:'presented',reference:'SYNTHETIC QA pedido',presentedOn:'2026-11-20'}};
|
||||
assert.deepEqual(offerScenarios(row),{normalVat:'800',conditionalVat:'0.00',conditionalReason:offerScenarios(row).conditionalReason,applied:false});assert.match(offerScenarios(row).conditionalReason,/Não é benefício concedido/);
|
||||
for(const patch of [{conditionalAssumption:''},{goodsSuitable:''},{goodsSuitable:'no'},{period:'2027-01'},{currency:'USD'}])assert.equal(offerScenarios({...row,...patch}).conditionalVat,null);
|
||||
for(const patch of [{normalVat:''},{normalVatSource:''},{normalVat:'-10'},{normalVat:'desconhecido'}])assert.equal(offerScenarios({...row,...patch}).normalVat,null);
|
||||
assert.match(offerPathReport(row),/Pedido Apresentado/);assert.match(offerPathReport(row),/apresentar pedido não concede isenção/);
|
||||
});
|
||||
test('P084 recorded decisions preserve goods/operation/period/conditions and previous decision when current inputs change',()=>{
|
||||
let row={...row84(),request:{...emptyOfferRequest(),state:'granted',decisionRef:'SYNTHETIC QA decisão parcial',decisionOn:'2026-11-25',goods:'SYNTHETIC QA só bens A',operation:'import',periodFrom:'2026-12',periodTo:'2026-12',conditions:'SYNTHETIC QA exclusivamente entrega ao destinatário A'}};
|
||||
row=recordOfferRequest(row);assert.equal(row.requestRecords.length,1);const accepted=structuredClone(row.requestRecords[0]);
|
||||
row={...row,description:'Bens B posteriores',operation:'acquisition',period:'2027-01',request:{...row.request,state:'denied',decisionRef:'SYNTHETIC QA decisão B',goods:'Bens B',periodFrom:'2027-01',periodTo:'2027-01',operation:'acquisition',conditions:'SYNTHETIC QA apenas pedido B'}};row=recordOfferRequest(row);assert.deepEqual(row.requestRecords[0],accepted);assert.equal(row.requestRecords.length,2);assert.equal(offerScenarios(row).applied,false);assert.equal(offerScenarios(row).conditionalVat,null);assert.match(offerPathReport(row),/não estender decisão/);
|
||||
for(const key of ['goods','operation','periodFrom','periodTo','conditions'])assert.throws(()=>recordOfferRequest({...row,request:{...row.request,[key]:''}}),/âmbito da decisão/);
|
||||
const exact=recordOfferRequest({...row,request:{...row.request,periodFrom:'2027-01-10',periodTo:'2027-01-15'}});assert.equal(exact.requestRecords.at(-1).request.periodFrom,'2027-01-10');assert.equal(exact.requestRecords.at(-1).request.periodTo,'2027-01-15');assert.throws(()=>recordOfferRequest({...row,request:{...row.request,periodFrom:'2027-02-30',periodTo:'2027-03-01'}}),/âmbito da decisão/);
|
||||
assert.equal(row.requestRecords[0].request.operation,'import');assert.equal(row.operation,'acquisition');
|
||||
});
|
||||
test('P085 humanitarian purpose/emergency/documents are distinct;missing inputs stay indeterminate and no P084 state is imposed',()=>{
|
||||
const base={...offerPreparationBook(draft()).rows[1],period:'2026-12',beneficiary:'SYNTHETIC QA destinatário',purpose:'SYNTHETIC QA atenuar cheias',humanitarianPurpose:'SYNTHETIC QA água e alimentação para deslocados',availableDocuments:'SYNTHETIC QA lista de bens\nSYNTHETIC QA guia disponível',normalVat:'160',normalVatSource:'SYNTHETIC QA previsão explícita',conditionalAssumption:'yes'};
|
||||
assert.equal(offerScenarios(base).conditionalVat,'0.00');assert.equal(offerScenarios({...base,humanitarianPurpose:''}).conditionalVat,null);assert.equal(offerScenarios({...base,mitigation:'no'}).conditionalVat,null);assert.throws(()=>recordOfferRequest(base),/P084/);assert.match(offerPathReport(base),/finalidade humanitária/);assert.match(offerPathReport(base),/procedimento de emergência\/doação por corresponder/);assert.equal(base.request,undefined);
|
||||
});
|
||||
test('save/recover/report retain old drafts/histories and all27fiscalbooks;normal and conditional are labelled and field11 stays unchanged',()=>{
|
||||
const d=draft(),prior=JSON.parse(frozen.modelo_a_snapshot),old=offerPreparationBook(d);const priorDecision=recordOfferRequest({...row84(),request:{...emptyOfferRequest(),state:'granted',decisionRef:'SYNTHETIC QA decisão parcial anterior',goods:'SYNTHETIC QA apenas bens A',operation:'import',periodFrom:'2026-11',periodTo:'2026-11',conditions:'SYNTHETIC QA só a operação anterior A'}});const p84=recordOfferRequest({...priorDecision,request:{...emptyOfferRequest(),state:'presented',reference:'SYNTHETIC QA pedido em apreciação'}});const p85={...old.rows[1],period:'2026-12',beneficiary:'SYNTHETIC QA destinatário',purpose:'SYNTHETIC QA resposta humanitária',humanitarianPurpose:'SYNTHETIC QA assistência imediata às vítimas das cheias',availableDocuments:'SYNTHETIC QA bens e destinatário',normalVat:'160',normalVatSource:'SYNTHETIC QA previsão sem taxa inferida',conditionalAssumption:'yes'};
|
||||
const book=recordOfferPreparations({...old,rows:[p84,p85]});assert.deepEqual(book.history.at(-1),old.saved);d.modelo_a_snapshot=saveOfferPreparationBook(d,book);assert.deepEqual(offerPreparationBook(JSON.parse(JSON.stringify(d))),book);const next=JSON.parse(d.modelo_a_snapshot);for(const key of ['books','salesOperations','importExemptions'])assert.deepEqual(next[key],prior[key]);assert.equal(next.books['opportunity:329'].result.periods[0].fields['11'],null);
|
||||
const report=JSON.stringify(createProjectDossierReport(d,{selectedSections:new Set(['finance']),version:'QA offer paths'}));for(const token of ['Pedido Apresentado','Cenário normal','Cenário condicionado à isenção','hipótese','finalidade humanitária','benefício não aplicado','800','160','apresentar pedido não concede isenção','SYNTHETIC QA decisão parcial anterior','SYNTHETIC QA apenas bens A','2026-11'])assert.ok(report.includes(token),token);
|
||||
if(process.env.IVA_OFFER_PATHS)fs.writeFileSync(process.env.IVA_OFFER_PATHS,JSON.stringify(d));
|
||||
});
|
||||
Reference in New Issue
Block a user