Adapt land status to saved investment land need

This commit is contained in:
2026-10-23 14:13:58 +02:00
parent f2150b1912
commit 5bda74aedc
10 changed files with 192 additions and 3 deletions
@@ -30,6 +30,39 @@ class InvestmentCorridorContextTest extends TestCase
$entry=collect(app(CorridorAgribusinessOpportunities::class)->entries())->firstWhere('corridor',config('investment_corridors.'.$corridor.'.name'));
return Opportunity::where('management_creation_key',$entry['key'])->firstOrFail();
}
public function test_land_status_uses_the_real_decision_preserves_inactive_values_and_allows_incomplete_drafts(): void
{
$id=$this->postJson('/investimentos',['title'=>'QA situação do terreno','client_key'=>(string)Str::uuid(),'reuse_consent'=>true])->assertCreated()->json('id');
$setNeed=function(string $need) use($id): array {
$version=$this->getJson('/investimentos/'.$id.'/contexto')->json('case.version');
$this->postJson('/investimentos/'.$id.'/aplicabilidade',['version'=>$version,'land_needed'=>$need,'external_funding'=>'unknown','viability'=>'unknown','reason'=>'Cenário QA '.$need])->assertOk();
return $this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->json();
};
$view=$setNeed('yes');
$body=['context'=>$id,'personal_snapshot'=>[],'economic_snapshot'=>['purpose'=>'simulate_only','context_applicability_snapshot'=>$view['prefill']['simulator']['context_applicability_snapshot'],'land_status'=>'duat','selection_labels'=>['land_status'=>'DUAT']]];
$created=$this->postJson('/api/v1/simulation-drafts',$body,['Idempotency-Key'=>(string)Str::uuid()])->assertCreated();
$draft=$created->json('data.id');$etag=$created->headers->get('ETag');
foreach(['no','unknown'] as $need){
$view=$setNeed($need);
$this->assertSame($need,json_decode($view['prefill']['simulator']['context_applicability_snapshot'],true)['land_preparation']['need']);
$update=$this->putJson('/api/v1/simulation-drafts/'.$draft,['personal_snapshot'=>[],'economic_snapshot'=>['purpose'=>'simulate_only','context_applicability_snapshot'=>$body['economic_snapshot']['context_applicability_snapshot']]],['If-Match'=>$etag])->assertOk();
$recovered=$this->getJson('/api/v1/simulation-drafts/'.$draft.'?context='.$id)->assertOk();
// Omitting context cannot bypass the real dossier decision. Hidden values are retained.
$this->assertSame('duat',$recovered->json('data.economic_snapshot.land_status'));
$this->assertSame('DUAT',$recovered->json('data.economic_snapshot.selection_labels.land_status'));
$this->assertSame($need,json_decode($recovered->json('data.economic_snapshot.context_applicability_snapshot'),true)['land_preparation']['need']);
$etag=$update->headers->get('ETag');
}
$setNeed('yes');
$this->flushSession();$this->actingAs($this->investor);
$this->getJson('/api/v1/simulation-drafts/'.$draft.'?context='.$id)->assertOk()->assertJsonPath('data.economic_snapshot.land_status','duat');
$updated=$this->putJson('/api/v1/simulation-drafts/'.$draft,[...$body,'economic_snapshot'=>['purpose'=>'simulate_only','land_status'=>'leased']],['If-Match'=>$etag])->assertOk();
$recovered=$this->getJson('/api/v1/simulation-drafts/'.$draft.'?context='.$id)->assertOk()->assertJsonPath('data.economic_snapshot.land_status','leased');
$this->assertSame('yes',json_decode($recovered->json('data.economic_snapshot.context_applicability_snapshot'),true)['land_preparation']['need']);
$this->putJson('/api/v1/simulation-drafts/'.$draft,[...$body,'economic_snapshot'=>['purpose'=>'simulate_only','land_status'=>'invalid']],['If-Match'=>$updated->headers->get('ETag')])->assertUnprocessable();
$empty=app(\App\Services\InvestorContext::class)->prefill(['fields'=>['site_tenure'=>'lease'],'revision'=>0,'provenance'=>[],'land_preparation'=>['need'=>'yes','reason'=>'QA']]);
$this->assertSame('leased',$empty['simulator']['land_status']);
}
public function test_simulator_reuses_names_for_the_exact_chosen_territory_and_sector_without_creating_a_location(): void
{
$id=$this->postJson('/investimentos',['title'=>'QA continuidade simulador','client_key'=>(string)Str::uuid(),'reuse_consent'=>true])->assertCreated()->json('id');
@@ -0,0 +1,54 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { investmentLandPreparation, landStatusVisible } from '../../resources/js/lib/investment-land-preparation.ts';
import { simulationFieldGroups, validateSimulationDraft, partitionSimulationDraft, restoreSimulationDraft } from '../../resources/js/lib/simulation-form.ts';
import { inheritCompatibleFields } from '../../resources/js/lib/context-inheritance.ts';
import { buildProjectDossierRows, exportProjectDossierCsv } from '../../resources/js/lib/project-dossier.ts';
import { createProjectDossierReport, createProjectDossierPrintHtml } from '../../resources/js/lib/project-dossier-report.ts';
const snapshot = (need) => JSON.stringify({version:1,context_revision:2,land_preparation:{need,reason:'Decisão guardada no dossier'}});
const draft = (need) => ({purpose:'simulate_only',journey_type:'investor',land_status:'duat',land_status_label:'DUAT',context_applicability_snapshot:snapshot(need)});
test('no hides land status and yes restores the same saved value; unknown is a pending decision without a required answer', () => {
const field=simulationFieldGroups.location.find(field=>field.key==='land_status');
for(const need of ['no','yes','unknown']) {
const input=draft(need);const before=JSON.stringify(input);
assert.equal(field.showWhen(input),need==='yes');
assert.equal(Boolean(validateSimulationDraft({...input,land_status:''},'location').land_status),false);
const saved=partitionSimulationDraft(input);const recovered=restoreSimulationDraft(saved.personal_snapshot,saved.economic_snapshot);
assert.equal(recovered.land_status,'duat');assert.equal(recovered.land_status_label,'DUAT');
assert.equal(investmentLandPreparation(recovered.context_applicability_snapshot).need,need);
assert.equal(JSON.stringify(input),before);
}
});
test('old snapshots use S10 and absent or malformed decisions never imply yes or no', () => {
for(const [applicability,need] of [['applicable','yes'],['not_needed_for_preparation','no'],['undetermined','unknown']])
assert.equal(investmentLandPreparation(JSON.stringify({version:1,path:{steps:[{code:'S10',applicability}]}})).need,need);
for(const raw of ['', 'invalid', '{}', '{"version":1,"land_preparation":{"need":"invalid"}}'])
assert.equal(investmentLandPreparation(raw).need,'unknown');
assert.equal(landStatusVisible({}),true); // Standalone simulator preserves its previous behaviour.
});
test('current dossier assessment refreshes after server recovery without overwriting the saved land answer', () => {
const scope='dossier:A:simulator';
const inherited=inheritCompatibleFields(scope,{}, {context_applicability_snapshot:snapshot('yes'),land_status:'duat'},null);
const recovered={...inherited.patch,context_reuse:inherited.metadata,context_applicability_snapshot:snapshot('no')};
const restored=inheritCompatibleFields(scope,recovered,{context_applicability_snapshot:snapshot('yes')},recovered.context_reuse);
assert.equal(restored.patch.context_applicability_snapshot,snapshot('yes'));
assert.equal(Object.hasOwn(restored.patch,'land_status'),false);
});
test('report, CSV and print reflect yes, no and unknown without treating historical DUAT as current when hidden', async () => {
for(const need of ['yes','no','unknown']) {
const input=draft(need);const before=JSON.stringify(input);
const rows=buildProjectDossierRows(input);
assert.equal(rows.some(row=>row[1]==='Situação do terreno'),need==='yes');
const report=createProjectDossierReport(input,{selectedSections:new Set(['location'])});
const print=await createProjectDossierPrintHtml(report,[],false);
const csv=exportProjectDossierCsv(input);
if(need==='yes'){assert.match(print,/DUAT/);assert.match(csv,/DUAT/);}
else {assert.doesNotMatch(print,/DUAT/);assert.doesNotMatch(csv,/DUAT/);assert.match(print,need==='no'?/valores anteriores conservados/:/Definir se precisa de terra/);}
assert.equal(JSON.stringify(input),before);
}
});