forceFill(['id'=>(string)Str::uuid(),'version_no'=>2]); $service=app(\App\Services\SimulationFormDrafts::class); $guard=new \ReflectionMethod($service,'requireMatch'); $tag=$service->etag($draft); foreach (['W/'.$tag, str_replace('-v2','-v1',$tag)] as $invalid) { $request=\Illuminate\Http\Request::create('/','PUT'); $request->headers->set('If-Match',$invalid); try { $guard->invoke($service,$request,$draft); $this->fail('A mismatching version must be rejected'); } catch (\Symfony\Component\HttpKernel\Exception\HttpException $error) { $this->assertSame(412,$error->getStatusCode()); } } $request=\Illuminate\Http\Request::create('/','PUT'); $request->headers->set('If-Match',$tag); $this->assertNull($guard->invoke($service,$request,$draft)); } public function test_beira_documentary_membership_opens_the_same_published_opportunities_without_inferring_by_province(): void { $portfolio = InvestmentPortfolioController::visible()->where('content->code', 'COR-02')->firstOrFail(); $service = app(CorridorAgribusinessOpportunities::class); $ids = $service->portfolioOpportunityIds($portfolio); $this->assertCount(7, $ids); $this->get('/carteira/'.$portfolio->id)->assertOk()->assertInertia(fn(Assert $page) => $page->has('opportunities', 7)); $catalogueIds=app(\App\Services\InvestmentCatalogue::class)->query(['corridor_id'=>$portfolio->corridor_id])->pluck('opportunities.id')->all(); $this->assertSame([], array_values(array_diff($ids,$catalogueIds))); $opportunity=Opportunity::findOrFail($ids[0]); $this->get('/oportunidades/'.$opportunity->slug.'?from_dossier='.$portfolio->id)->assertOk(); $origins=app(InvestmentOpportunityChoices::class)->portfolioOrigins([$portfolio->id],$opportunity->id); $this->assertSame($portfolio->id,$origins[0]['id']); $this->assertSame('corridor',$origins[0]['kind']); $other=Opportunity::published()->whereNotIn('id',$ids)->firstOrFail(); $this->get('/oportunidades/'.$other->slug.'?from_dossier='.$portfolio->id)->assertNotFound(); $this->assertSame([], $service->portfolioOpportunityIds(InvestmentPortfolioController::visible()->whereNotNull('project_id')->firstOrFail())); } private User $actor; protected function setUp(): void { parent::setUp(); $this->withoutVite(); $db=DB::selectOne('select current_database() as name,inet_server_port() as port'); $this->assertSame('simulator_fiscal_delta_20261021_test',$db->name); $this->assertSame(55441,(int)$db->port); config(['investor-path.enabled'=>true,'suitecrm.enabled'=>false,'investmoz_ai.enabled'=>false]); $this->actor=User::factory()->create(); $this->actingAs($this->actor); } private function records(): array { $entries=collect(app(CorridorAgribusinessOpportunities::class)->entries())->where('corridor',config('investment_corridors.beira.name'))->take(2); $opportunities=Opportunity::whereIn('management_creation_key',$entries->pluck('key'))->orderBy('id')->get(); $project=InvestmentPortfolioController::visible()->whereNotNull('project_id')->firstOrFail(); $cluster=InvestmentPortfolioController::visible()->whereNotNull('cluster_id')->firstOrFail(); foreach($opportunities as $opportunity) { // Synthetic links exist only inside this rolled-back QA transaction. DB::table('opportunity_projects')->insertOrIgnore(['opportunity_id'=>$opportunity->id,'project_id'=>$project->project_id]); DB::table('opportunity_clusters')->insertOrIgnore(['opportunity_id'=>$opportunity->id,'cluster_id'=>$cluster->cluster_id]); } return [$opportunities,$project,$cluster]; } public function test_origin_and_location_survive_entry_draft_confirmation_and_simulator_without_resetting_deliberate_values(): void { [$records,$origin,$cluster]=$this->records(); $opportunity=$records[0]; $province=collect(app(InvestmentCorridorContext::class)->provinces('beira'))->firstWhere('source_code','07')->id; $query='?source=opportunity:'.$opportunity->id.'&from_dossier='.$origin->id.'&province_id='.$province; auth()->logout(); $this->get('/investimentos/iniciar'.$query)->assertRedirect(route('login')); parse_str(parse_url(session('url.intended'),PHP_URL_QUERY),$intended); $this->assertSame($origin->id,$intended['from_dossier']);$this->assertSame($province,$intended['province_id']); $this->actingAs($this->actor); $this->get('/investimentos/iniciar'.$query)->assertOk()->assertInertia(fn(Assert $p)=>$p->where('opportunityChoices.0.portfolio_origins.0.id',$origin->id)->where('opportunityChoices.0.territory.id',$province)->where('entryQuery.from_dossier',$origin->id)); $id=$this->postJson('/investimentos',['title'=>'QA título deliberado','client_key'=>(string)Str::uuid(),'reuse_consent'=>true])->assertCreated()->json('id'); $body=['version'=>$this->getJson('/investimentos/'.$id.'/contexto')->json('case.version'),'selection'=>[],'opportunity_choices'=>[ ['source'=>'opportunity:'.$opportunity->id,'territory'=>'territory:'.$province,'portfolio_origins'=>[$origin->id],'activities'=>['trade']], ['source'=>'opportunity:'.$records[1]->id,'territory'=>'territory:'.$province,'portfolio_origins'=>[$origin->id,$cluster->id]], ]]; $this->postJson('/investimentos/'.$id.'/contexto/rascunho',[...$body,'draft_sequence'=>1])->assertOk(); $this->flushSession();$this->actingAs($this->actor); $this->get('/investimentos/'.$id.'/contexto/editar')->assertOk()->assertInertia(fn(Assert $p)=>$p->where('requestedChoices.0.portfolio_origins.0.id',$origin->id)->where('requestedChoices.0.territory.id',$province)); $preview=$this->postJson('/investimentos/'.$id.'/contexto/previa',$body)->assertOk()->json(); $this->assertGreaterThan(0,$preview['opportunity_choices'][0]['portfolio_origins'][0]['version']); $this->assertSame('QA título deliberado',$preview['fields']['project_title']); $this->postJson('/investimentos/'.$id.'/contexto/confirmar',[...$body,'preview_hash'=>$preview['preview_hash'],'operation_key'=>(string)Str::uuid(),'confirm'=>true])->assertOk(); $view=$this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->json(); $this->assertSame($view['context']['opportunity_choices'],json_decode($view['prefill']['simulator']['context_applicability_snapshot'],true)['opportunity_choices']); $this->assertSame($province,$view['prefill']['simulator']['province_id']); $this->assertSame('QA título deliberado',$view['prefill']['simulator']['project_title']); $created=$this->postJson('/api/v1/simulation-drafts',['context'=>$id,'personal_snapshot'=>[],'economic_snapshot'=>['purpose'=>'simulate_only','project_title'=>'QA edição deliberada no simulador','context_applicability_snapshot'=>$view['prefill']['simulator']['context_applicability_snapshot']]],['Idempotency-Key'=>(string)Str::uuid()])->assertCreated(); $this->flushSession();$this->actingAs($this->actor); $simulation=$this->getJson('/api/v1/simulation-drafts/'.$created->json('data.id').'?context='.$id)->assertOk()->assertJsonPath('data.economic_snapshot.project_title','QA edição deliberada no simulador')->json('data.economic_snapshot'); $this->assertSame($view['context']['opportunity_choices'],json_decode($simulation['context_applicability_snapshot'],true)['opportunity_choices']); $manica=collect(app(InvestmentCorridorContext::class)->provinces('beira'))->firstWhere('source_code','06')->id; $this->get('/investimentos/'.$id.'/contexto/editar?source=opportunity:'.$opportunity->id.'&from_dossier='.$cluster->id.'&province_id='.$manica) ->assertOk()->assertInertia(fn(Assert $p)=>$p->where('requestedChoices.0.territory.id',$province)->where('requestedChoices.0.activities.0','trade')->has('requestedChoices',2)->has('requestedChoices.0.portfolio_origins',2)->has('entryLocationDifferences',1)); $body['version']=$this->getJson('/investimentos/'.$id.'/contexto')->json('case.version'); $body['opportunity_choices'][0]['portfolio_origins'][]=$cluster->id; $associations=$this->postJson('/investimentos/'.$id.'/contexto/previa',$body)->assertOk()->json(); $this->assertFalse($associations['calculation_inputs_changed']);$this->assertSame([],$associations['review_steps']); $this->postJson('/investimentos/'.$id.'/contexto/confirmar',[...$body,'preview_hash'=>$associations['preview_hash'],'operation_key'=>(string)Str::uuid(),'confirm'=>true])->assertOk(); $this->getJson('/investimentos/'.$id.'/contexto')->assertJsonPath('context.dependency_hash',$view['context']['dependency_hash'])->assertJsonCount(2,'context.opportunity_choices.0.portfolio_origins'); } public function test_unrelated_or_private_origins_are_rejected_in_entry_draft_and_preview(): void { [$records,$origin]=$this->records(); DB::table('opportunity_projects')->where('opportunity_id',$records[0]->id)->where('project_id',$origin->project_id)->delete(); $this->get('/investimentos/iniciar?source=opportunity:'.$records[0]->id.'&from_dossier='.$origin->id)->assertUnprocessable(); $id=$this->postJson('/investimentos',['title'=>'QA origem inválida','client_key'=>(string)Str::uuid(),'reuse_consent'=>true])->json('id'); $body=['version'=>$this->getJson('/investimentos/'.$id.'/contexto')->json('case.version'),'selection'=>[],'opportunity_choices'=>[['source'=>'opportunity:'.$records[0]->id,'portfolio_origins'=>[$origin->id]]]]; $this->postJson('/investimentos/'.$id.'/contexto/rascunho',[...$body,'draft_sequence'=>1])->assertUnprocessable(); $this->postJson('/investimentos/'.$id.'/contexto/previa',$body)->assertUnprocessable(); $origin->publication_status='draft';$origin->save(); $this->get('/investimentos/iniciar?source=opportunity:'.$records[1]->id.'&from_dossier='.$origin->id)->assertNotFound(); } }