create(); config(['portal.managers' => [$user->email]]); $this->actingAs($user); } private function cluster(string $code): EconomicCluster { return EconomicCluster::create(['code' => $code, 'name' => $code, 'sector' => 'Energia', 'status' => 'planned']); } private function chain(EconomicCluster $cluster, string $code): ValueChain { return ValueChain::create(['cluster_id' => $cluster->id, 'code' => $code, 'name' => $code, 'stage' => 'input']); } public function test_links_are_idempotent_and_reject_self_foreign_and_deleted_stages(): void { $this->manager(); $cluster = $this->cluster('C1'); $origin = $this->chain($cluster, 'Input'); $destination = $this->chain($cluster, 'Production'); $foreign = $this->chain($this->cluster('C2'), 'Foreign'); $url = '/management/clusters/'.$cluster->id.'/chain-links'; $data = ['from_value_chain_id' => $origin->id, 'to_value_chain_id' => $destination->id, 'link_type' => 'supplies']; $this->post($url, $data)->assertSessionHasNoErrors(); $this->post($url, $data)->assertSessionHasNoErrors(); $this->assertDatabaseCount('value_chain_links', 1); $this->assertSame(1, DB::table('audit_events')->where('entity_type', 'value_chain_links')->count()); $this->post($url, [...$data, 'to_value_chain_id' => $origin->id])->assertSessionHasErrors('to_value_chain_id'); $this->post($url, [...$data, 'to_value_chain_id' => $foreign->id])->assertSessionHasErrors('to_value_chain_id'); $destination->delete(); $this->post($url, $data)->assertSessionHasErrors('to_value_chain_id'); } public function test_removal_preserves_history_and_cannot_target_another_cluster(): void { $this->manager(); $cluster = $this->cluster('C1'); $other = $this->cluster('C2'); $data = ['from_value_chain_id' => $this->chain($cluster, 'A')->id, 'to_value_chain_id' => $this->chain($cluster, 'B')->id, 'link_type' => 'supports']; $url = '/management/clusters/'.$cluster->id.'/chain-links'; $this->post($url, $data)->assertSessionHasNoErrors(); $link = ValueChainLink::firstOrFail(); $this->delete('/management/clusters/'.$other->id.'/chain-links/'.$link->id)->assertNotFound(); $this->assertNotSoftDeleted($link); $this->delete($url.'/'.$link->id)->assertSessionHasNoErrors(); $this->assertSoftDeleted($link); $this->assertSame(2, DB::table('audit_events')->where('entity_id', $link->id)->count()); $this->post($url, $data)->assertSessionHasNoErrors(); $this->assertSame(1, ValueChainLink::count()); $this->assertDatabaseCount('value_chain_links', 2); } public function test_gap_requires_details_and_stays_within_cluster(): void { $this->manager(); $cluster = $this->cluster('C1'); $other = $this->cluster('C2'); $stage = $this->chain($cluster, 'Logistics'); $foreign = $this->chain($other, 'Foreign'); $data = ['value_chain_id' => $stage->id, 'title' => 'Armazenagem', 'location' => 'Distrito de teste', 'required_capacity' => '500 toneladas', 'rationale' => 'Falta capacidade de armazenamento.', 'status' => 'identified', 'call_to_action' => 'Apresentar proposta de investimento.']; $url = '/management/clusters/'.$cluster->id.'/gaps'; $this->post($url, [...$data, 'required_capacity' => ''])->assertSessionHasErrors('required_capacity'); $this->post($url, [...$data, 'value_chain_id' => $foreign->id])->assertSessionHasErrors('value_chain_id'); $this->post($url, $data)->assertSessionHasNoErrors(); $gap = InvestmentGap::firstOrFail(); $this->put('/management/clusters/'.$other->id.'/gaps/'.$gap->id, [...$data, 'value_chain_id' => $foreign->id])->assertNotFound(); $this->put($url.'/'.$gap->id, [...$data, 'status' => 'resolved'])->assertSessionHasNoErrors(); $this->assertSame('resolved', $gap->fresh()->status); $event = DB::table('audit_events')->where('entity_id', $gap->id)->where('action', 'created')->first(); $this->assertStringNotContainsString($data['rationale'], $event->after_data); $this->get('/management/clusters/'.$cluster->id.'/edit')->assertInertia(fn (AssertableInertia $page) => $page ->component('structures/editor')->has('gaps', 1)->where('gaps.0.status', 'resolved')->has('chainLinks', 0)); } public function test_anonymous_and_unauthorised_users_cannot_write_network_or_gaps(): void { $cluster = $this->cluster('C1'); $this->post('/management/clusters/'.$cluster->id.'/gaps', [])->assertRedirect('/login'); $this->actingAs(User::factory()->create()); $this->post('/management/clusters/'.$cluster->id.'/gaps', [])->assertForbidden(); $this->post('/management/clusters/'.$cluster->id.'/chain-links', [])->assertForbidden(); $this->assertDatabaseCount('investment_gaps', 0); } }