create(); config(['portal.managers' => [$user->email]]); $this->actingAs($user); } public function test_project_validates_money_dates_and_duplicate_codes_without_precision_loss(): void { $this->manager(); $data = ['code' => 'P-001', 'name' => 'Projecto de teste', 'status' => 'planned', 'budget_amount' => '999999999999999999.99', 'budget_currency' => 'MZN', 'start_date' => '2026-10-01', 'end_date' => '2026-11-01']; $this->post('/management/projects', [...$data, 'end_date' => '2026-09-01'])->assertSessionHasErrors('end_date'); $this->post('/management/projects', [...$data, 'budget_currency' => ''])->assertSessionHasErrors('budget_currency'); $this->post('/management/projects', $data)->assertSessionHasNoErrors()->assertRedirect(); $project = Project::firstOrFail(); $this->assertSame($data['budget_amount'], $project->budget_amount); $this->post('/management/projects', $data)->assertSessionHasErrors('code'); $this->put('/management/projects/'.$project->id, [...$data, 'status' => 'active'])->assertSessionHasNoErrors(); $this->get('/management/projects/'.$project->id.'/edit')->assertOk(); $this->get('/management/projects?q=P-001')->assertOk(); $this->assertDatabaseCount('projects', 1); $this->assertSame(2, DB::table('audit_events')->where('entity_type', 'projects')->count()); } public function test_cluster_membership_is_idempotent_and_role_changes_are_audited(): void { $this->manager(); $this->post('/management/clusters', ['code' => 'C-001', 'name' => 'Cluster de teste', 'sector' => 'Energia', 'status' => 'planned'])->assertSessionHasNoErrors(); $cluster = EconomicCluster::firstOrFail(); $project = Project::factory()->create(); $url = '/management/clusters/'.$cluster->id.'/projects'; $association = ['project_id' => $project->id, 'version_no' => 0, 'role' => 'anchor', 'association_type' => 'potential', 'source_reference' => 'Referencia de planeamento sintético', 'evidence_reference' => 'fixture:test:cluster-link', 'valid_from' => today()->toDateString()]; $this->post($url, $association)->assertSessionHasNoErrors(); $this->post($url, $association)->assertStatus(409); $this->assertDatabaseCount('cluster_projects', 1); $this->post($url, [...$association, 'version_no' => 1])->assertSessionHasNoErrors(); $this->assertDatabaseHas('cluster_projects', ['project_id' => $project->id, 'version_no' => 1]); $this->post($url, [...$association, 'version_no' => 1, 'role' => 'supporting'])->assertSessionHasNoErrors(); $this->assertDatabaseHas('cluster_projects', ['project_id' => $project->id, 'role' => 'supporting', 'version_no' => 2, 'validation_status' => 'pending']); $this->assertSame(2, DB::table('audit_events')->where('entity_type', 'cluster_projects')->count()); $project->delete(); $this->post($url, ['project_id' => $project->id, 'role' => 'anchor'])->assertSessionHasErrors('project_id'); $this->get('/management/clusters/'.$cluster->id.'/edit')->assertOk(); } public function test_chain_edit_cannot_cross_cluster_and_codes_are_unique_per_cluster(): void { $this->manager(); $first = EconomicCluster::create(['code' => 'C1', 'name' => 'Primeiro', 'sector' => 'Energia', 'status' => 'planned']); $second = EconomicCluster::create(['code' => 'C2', 'name' => 'Segundo', 'sector' => 'Agro', 'status' => 'planned']); $data = ['code' => 'INPUT', 'name' => 'Insumos', 'stage' => 'input']; $this->post('/management/clusters/'.$first->id.'/chains', $data)->assertSessionHasNoErrors(); $chain = ValueChain::firstOrFail(); $this->post('/management/clusters/'.$first->id.'/chains', $data)->assertSessionHasErrors('code'); $this->post('/management/clusters/'.$second->id.'/chains', $data)->assertSessionHasNoErrors(); $this->put('/management/clusters/'.$second->id.'/chains/'.$chain->id, [...$data, 'name' => 'Alterado'])->assertNotFound(); $this->assertSame('Insumos', $chain->fresh()->name); $this->put('/management/clusters/'.$first->id.'/chains/'.$chain->id, [...$data, 'stage' => 'processing'])->assertSessionHasNoErrors(); $this->assertSame('processing', $chain->fresh()->stage); } public function test_structure_management_requires_authorised_verified_manager(): void { $this->get('/management/clusters')->assertRedirect('/login'); $this->actingAs(User::factory()->create()); $this->get('/management/projects')->assertForbidden(); $this->post('/management/clusters', [])->assertForbidden(); $this->get('/management/corridors')->assertForbidden(); $this->post('/management/corridors', [])->assertForbidden(); $this->assertDatabaseCount('economic_clusters', 0); $this->assertDatabaseCount('development_corridors', 0); } public function test_development_corridors_have_private_crud_with_supported_fields_and_statuses(): void { $this->manager(); $data = ['code' => 'COR-TEST', 'name' => 'Corredor de teste', 'status' => 'proposed', 'description' => 'Descrição sintética para validar o cadastro administrativo.']; $this->post('/management/corridors', $data)->assertSessionHasNoErrors()->assertRedirect(); $corridor = DevelopmentCorridor::firstOrFail(); $this->assertSame('internal', $corridor->data_classification); $this->assertDatabaseHas('development_corridors', ['id' => $corridor->id, 'status' => 'proposed']); $this->get('/management/corridors')->assertInertia(fn (AssertableInertia $page) => $page ->component('structures/index')->where('structure', 'corridors')->has('records.data', 1)); $this->get('/management/corridors/'.$corridor->id.'/edit')->assertInertia(fn (AssertableInertia $page) => $page ->component('structures/editor')->where('structure', 'corridors')->where('record.id', $corridor->id)); $this->put('/management/corridors/'.$corridor->id, [...$data, 'status' => 'active', 'description' => 'Descrição actualizada.']) ->assertSessionHasNoErrors(); $this->assertDatabaseHas('development_corridors', ['id' => $corridor->id, 'status' => 'active', 'description' => 'Descrição actualizada.']); } public function test_structure_relations_require_sources_and_independent_review(): void { $editor = User::factory()->create(); $reviewer = User::factory()->create(); config(['portal.managers' => [$editor->email, $reviewer->email]]); $cluster = EconomicCluster::create(['code' => 'REL-C1', 'name' => 'Cluster relacional', 'sector' => 'Energia', 'status' => 'planned']); $project = Project::factory()->create(); $url = '/management/structure-relations/cluster-project'; $payload = ['cluster_id' => $cluster->id, 'project_id' => $project->id, 'role' => 'anchor', 'association_type' => 'official', 'source_reference' => 'Diploma oficial, artigo 4, secao 2', 'evidence_reference' => 'fixture:test:evidence-01', 'valid_from' => today()->toDateString()]; $this->actingAs($editor)->post($url, [...$payload, 'source_reference' => 'curta'])->assertSessionHasErrors('source_reference'); $this->post($url, $payload)->assertSessionHasNoErrors(); $relation = DB::table('cluster_projects')->where('cluster_id', $cluster->id)->where('project_id', $project->id)->first(); $this->assertSame('pending', $relation->validation_status); $this->assertSame($editor->id, $relation->editor_id); $this->post('/management/structure-relations/cluster-project/'.$relation->id.'/validate', ['version_no' => 1])->assertForbidden(); $this->actingAs($reviewer)->post('/management/structure-relations/cluster-project/'.$relation->id.'/validate', ['version_no' => 1])->assertSessionHasNoErrors(); $this->assertDatabaseHas('cluster_projects', ['id' => $relation->id, 'validation_status' => 'validated', 'validator_id' => $reviewer->id, 'version_no' => 2]); $this->post('/management/structure-relations/cluster-project/'.$relation->id.'/revoke', ['version_no' => 1])->assertStatus(409); } }