seed(InvestmentPortfolioSeeder::class); $dossier = InvestmentDossier::firstOrFail(); $dossier->update(['version_no' => $dossier->version_no, 'title' => 'Revisão humana preservada']); $this->seed(InvestmentPortfolioSeeder::class); $this->assertDatabaseCount('projects', 15); $this->assertDatabaseCount('economic_clusters', 8); $this->assertDatabaseCount('development_corridors', 6); $this->assertDatabaseCount('investment_dossiers', 29); $this->assertSame('Revisão humana preservada', $dossier->fresh()->title); $this->assertSame(15, DB::table('corridor_projects')->distinct()->count('project_id')); $this->assertDatabaseHas('cluster_projects', ['role' => 'anchor']); $this->get('/carteira')->assertInertia(fn (Assert $page) => $page->component('portfolio/index')->has('records.data', 0)); $this->get('/carteira/'.$dossier->id)->assertNotFound(); } public function test_stale_editor_cannot_overwrite_or_publish_a_newer_version(): void { $this->seed(InvestmentPortfolioSeeder::class); $manager = User::factory()->create(); config(['portal.managers' => [$manager->email]]); $dossier = InvestmentDossier::firstOrFail(); $url = '/management/portfolio/'.$dossier->id; $payload = ['version_no' => $dossier->version_no, 'title' => 'Revisao mais recente', 'source' => $dossier->source, 'content' => $dossier->content]; $this->actingAs($manager)->put($url, $payload)->assertRedirect()->assertSessionHasNoErrors(); $this->putJson($url, [...$payload, 'title' => 'Alteracao obsoleta'])->assertStatus(409); $approval = ['version_no' => $dossier->version_no, 'approval_evidence' => 'Aprovacao sintetica da versao revista']; $this->postJson($url.'/publish', $approval)->assertStatus(409); $this->assertSame('Revisao mais recente', $dossier->fresh()->title); $this->assertSame('draft', $dossier->fresh()->publication_status); $this->assertSame(2, $dossier->fresh()->version_no); $this->post($url.'/publish', [...$approval, 'version_no' => 2])->assertRedirect()->assertSessionHasNoErrors(); $this->assertSame(3, $dossier->fresh()->version_no); $this->assertSame('published', $dossier->fresh()->publication_status); $this->postJson($url.'/publish', [...$approval, 'version_no' => 2])->assertStatus(409); $this->putJson($url, array_diff_key($payload, ['version_no' => true]))->assertUnprocessable()->assertJsonValidationErrors('version_no'); $this->get('/carteira/'.$dossier->id)->assertInertia(fn (Assert $page) => $page->missing('record.version_no')->missing('record.approval_evidence')); $audit = DB::table('audit_events')->where('entity_id', $dossier->id)->orderByDesc('created_at')->get(); $this->assertCount(2, $audit); $this->assertStringNotContainsString('Aprovacao sintetica', json_encode($audit)); } private function publish(): InvestmentDossier { $this->seed(InvestmentPortfolioSeeder::class); $editor = User::factory()->create(); $reviewer = User::factory()->create(); config(['portal.managers' => [$editor->email, $reviewer->email]]); $dossier = InvestmentDossier::whereNotNull('project_id')->firstOrFail(); $this->actingAs($editor)->put('/management/portfolio/'.$dossier->id, ['version_no' => $dossier->version_no, 'title' => $dossier->title, 'source' => $dossier->source, 'content' => $dossier->content])->assertSessionHasNoErrors(); $evidence = ['approval_evidence' => 'Autorização fictícia apenas para teste']; $this->post('/management/portfolio/'.$dossier->id.'/publish', [...$evidence, 'version_no' => $dossier->fresh()->version_no])->assertSessionHasNoErrors()->assertRedirect(); $this->assertSame($editor->id, $dossier->fresh()->reviewer_id); $this->actingAs($reviewer)->post('/management/portfolio/'.$dossier->id.'/publish', [...$evidence, 'version_no' => $dossier->fresh()->version_no])->assertSessionHasNoErrors()->assertRedirect(); return $dossier->fresh(); } public function test_authorized_manager_can_publish_and_internal_evidence_is_hidden(): void { $dossier = $this->publish(); $this->get('/carteira/'.$dossier->id)->assertInertia(fn (Assert $page) => $page->component('portfolio/show') ->where('record.title', $dossier->title)->missing('record.approval_evidence')->missing('record.editor_id')->has('related', 0)); $this->put('/management/portfolio/'.$dossier->id, ['version_no' => $dossier->version_no, 'title' => 'Conteúdo alterado', 'source' => $dossier->source, 'content' => $dossier->content])->assertSessionHasNoErrors(); $this->get('/carteira/'.$dossier->id)->assertNotFound(); $this->actingAs(User::factory()->create())->get('/management/portfolio')->assertForbidden(); $this->post('/management/portfolio/'.$dossier->id.'/publish', ['approval_evidence' => 'Tentativa sem privilégios de gestão'])->assertForbidden(); } public function test_owner_approval_publishes_document_once_and_preserves_later_edits(): void { $approval = 'Aprovação expressa do proprietário para executar a carteira v4.'; $this->artisan('portfolio:import', ['--approval' => $approval])->assertSuccessful(); $this->assertSame(29, InvestmentDossier::where('publication_status', 'published')->count()); $this->assertSame(29, DB::table('audit_events')->where('action', 'owner_approved')->count()); $dossier = InvestmentDossier::firstOrFail(); $this->get('/carteira/'.$dossier->id)->assertInertia(fn (Assert $page) => $page ->missing('record.approval_evidence')->missing('record.approval_basis')); $this->artisan('portfolio:import', ['--approval' => $approval])->assertSuccessful(); $this->assertSame(29, DB::table('audit_events')->where('action', 'owner_approved')->count()); $editor = User::factory()->create(); config(['portal.managers' => [$editor->email]]); $this->actingAs($editor)->put('/management/portfolio/'.$dossier->id, [ 'version_no' => $dossier->version_no, 'title' => 'Alteração posterior', 'source' => $dossier->source, 'content' => $dossier->content, ])->assertSessionHasNoErrors(); $this->artisan('portfolio:import', ['--approval' => $approval])->assertSuccessful(); $this->assertSame('draft', $dossier->fresh()->publication_status); $this->assertSame('Alteração posterior', $dossier->fresh()->title); $this->get('/carteira/'.$dossier->id)->assertNotFound(); } public function test_personal_journey_is_isolated_and_retries_do_not_duplicate_interest_or_event(): void { $dossier = $this->publish(); $owner = User::factory()->create(); $other = User::factory()->create(); $this->actingAs($owner)->post('/investidor/guardar/'.$dossier->id)->assertRedirect('/investidor'); $this->post('/investidor/guardar/'.$dossier->id)->assertRedirect(); $this->assertDatabaseCount('investor_journeys', 1); $journey = InvestorJourney::firstOrFail(); $payload = ['message' => 'Pretendo investir neste projecto de teste.', 'country' => 'Moçambique', 'privacy_consent' => true, 'role' => 'investir', 'horizon' => '12 meses']; $url = '/investidor/'.$journey->id.'/interesse'; $this->post($url, [...$payload, 'privacy_consent' => false])->assertSessionHasErrors('privacy_consent'); $this->actingAs($other)->post($url, $payload)->assertNotFound(); $this->get('/investidor')->assertInertia(fn (Assert $page) => $page->has('journeys', 0)); $this->actingAs($owner)->post($url, $payload)->assertSessionHasNoErrors()->assertRedirect(); $this->post($url, $payload)->assertSessionHasNoErrors(); $this->assertDatabaseCount('interest_requests', 1); $this->assertDatabaseCount('outbox_events', 1); $this->assertDatabaseHas('investor_journeys', ['id' => $journey->id, 'status' => 'active']); $metadata = json_decode(DB::table('interest_requests')->value('metadata'), true); $this->assertSame($journey->id, $metadata['journey_id']); $this->assertSame($dossier->project_id, $metadata['project_id']); $this->get('/investidor')->assertInertia(fn (Assert $page) => $page->has('journeys', 1)->where('journeys.0.request_status', 'received')); $conversion = ['owner_user_id' => $owner->id, 'title' => 'Interesse da jornada', 'next_action' => 'Contactar investidor', 'due_at' => now()->addDay()->toIso8601String()]; $interest = InterestRequest::firstOrFail(); try { app(ConvertInterestToLead::class)->convert($interest, $conversion, $owner->id); $this->fail('A conversão deve preservar o investidor da jornada.'); } catch (ValidationException $exception) { $this->assertArrayHasKey('investor_id', $exception->errors()); } $lead = app(ConvertInterestToLead::class)->convert($interest, [...$conversion, 'investor_id' => $journey->investor_id], $owner->id); $this->assertSame($journey->investor_id, $lead->investor_id); $this->assertDatabaseCount('investors', 1); } public function test_unpublished_and_deleted_entities_cannot_start_journeys(): void { $dossier = $this->publish(); DB::table('projects')->where('id', $dossier->project_id)->update(['deleted_at' => now()]); $this->get('/carteira/'.$dossier->id)->assertNotFound(); $this->post('/investidor/guardar/'.$dossier->id)->assertNotFound(); $this->assertDatabaseCount('investor_journeys', 0); } public function test_origin_context_is_validated_and_preserved_in_the_interest(): void { $project = $this->publish(); $corridorId = DB::table('corridor_projects')->where('project_id', $project->project_id)->value('corridor_id'); $origin = InvestmentDossier::where('corridor_id', $corridorId)->firstOrFail(); $origin->update(['editor_id' => $project->editor_id, 'reviewer_id' => $project->reviewer_id, 'publication_status' => 'published', 'published_at' => now(), 'approval_evidence' => 'Teste de contexto autorizado']); $other = InvestmentDossier::whereNotNull('project_id')->where('id', '!=', $project->id)->firstOrFail(); $other->update(['editor_id' => $project->editor_id, 'reviewer_id' => $project->reviewer_id, 'publication_status' => 'published', 'published_at' => now(), 'approval_evidence' => 'Teste de contexto autorizado']); $this->actingAs(User::factory()->create())->post('/investidor/guardar/'.$project->id, ['origin_id' => $other->id])->assertStatus(422); $this->post('/investidor/guardar/'.$project->id, ['origin_id' => $origin->id])->assertRedirect(); $journey = InvestorJourney::firstOrFail(); $this->post('/investidor/'.$journey->id.'/interesse', ['message' => 'Participar no corredor através deste projecto.', 'country' => 'Moçambique', 'privacy_consent' => true, 'role' => 'financiar', 'horizon' => '24 meses'])->assertSessionHasNoErrors(); $metadata = json_decode(DB::table('interest_requests')->value('metadata'), true); $this->assertSame($origin->corridor_id, $metadata['corridor_id']); $this->assertSame($project->project_id, $metadata['project_id']); } }