where('group', $group)->where('code', $code)->value('id'); } private function opportunity(): Opportunity { $sector = Sector::firstOrCreate(['slug' => 'test-sector'], ['name' => 'Sector sintético', 'is_active' => true]); return Opportunity::create(['sector_id' => $sector->id, 'reference' => (string) Str::uuid(), 'slug' => (string) Str::uuid(), 'title' => 'Oportunidade sintética', 'status' => 'published', 'published_at' => now()->subMinute(), 'province' => 'Gaza', 'estimated_investment_amount' => 1000, 'estimated_investment_currency' => 'USD']); } private function payload(int $version = 1): array { return ['version' => $version, 'payload' => ['full_name' => 'Pessoa de teste', 'email' => 'test@example.com', 'country' => 'Moçambique', 'profile_id' => $this->option('investor_profile', 'individual'), 'sector' => 'Educação', 'objective' => 'Criar uma oficina de formação exclusivamente sintética.', 'participation_id' => $this->option('participation', 'invest'), 'horizon' => '12 meses', 'contact_consent' => true], 'needs' => [['category_id' => $this->option('need_category', 'education'), 'option_id' => $this->option('education', 'laboratory'), 'quantity' => 2, 'unit_id' => $this->option('unit', 'units'), 'priority_id' => $this->option('priority', 'normal'), 'details' => ['description' => 'Laboratórios sintéticos para formação']]]]; } public function test_submission_is_versioned_encrypted_private_and_idempotently_integrated(): void { $this->seed(InvestmentReferenceSeeder::class); $user = User::factory()->create(); $opportunity = $this->opportunity(); $id = $this->actingAs($user)->postJson('/api/v1/investment/submissions', ['opportunity_id' => $opportunity->id])->assertCreated()->json('id'); $this->getJson('/api/v1/investment/submissions/'.$id)->assertJsonPath('submission.payload.sector', 'Sector sintético')->assertJsonPath('submission.payload.location', 'Gaza'); $this->putJson('/api/v1/investment/submissions/'.$id, $this->payload())->assertOk()->assertJsonPath('version', 2); $this->putJson('/api/v1/investment/submissions/'.$id, $this->payload())->assertConflict(); $this->assertStringNotContainsString('test@example.com', DB::table('investment_submissions')->where('id', $id)->value('payload')); $this->assertStringNotContainsString('Laboratórios', DB::table('investment_specific_needs')->value('details')); $this->actingAs(User::factory()->create())->getJson('/api/v1/investment/submissions/'.$id)->assertNotFound(); $this->postJson('/api/v1/investment/submissions/'.$id.'/submit', $this->payload(2))->assertNotFound(); $this->actingAs($user)->postJson('/api/v1/investment/submissions/'.$id.'/submit', $this->payload(2))->assertOk(); $this->postJson('/api/v1/investment/submissions/'.$id.'/submit', $this->payload(2))->assertOk(); $this->assertDatabaseCount('interest_requests', 1); $this->assertDatabaseCount('outbox_events', 1); $this->assertSame($id, InterestRequest::firstOrFail()->metadata['submission_id']); $this->getJson('/api/v1/investment/submissions/'.$id)->assertHeader('Cache-Control', 'no-store, private'); InterestRequest::firstOrFail()->update(['status' => 'in_review']); $this->postJson('/api/v1/investment/submissions/'.$id.'/submit', $this->payload(2))->assertJsonPath('status', 'in_review'); $this->get('/balcao-virtual')->assertInertia(fn (Assert $page) => $page->has('requests', 1)); $this->assertSame(3, DB::table('investment_submission_events')->where('submission_id', $id)->count()); $this->putJson('/api/v1/investment/submissions/'.$id, $this->payload(3))->assertConflict(); } public function test_invalid_reference_group_consent_and_unpublished_target_are_rejected(): void { $this->seed(InvestmentReferenceSeeder::class); $user = User::factory()->create(); $opportunity = $this->opportunity(); $id = $this->actingAs($user)->postJson('/api/v1/investment/submissions', ['opportunity_id' => $opportunity->id])->json('id'); $data = $this->payload(); $data['needs'][0]['option_id'] = $this->option('infrastructure', 'energy'); $this->putJson('/api/v1/investment/submissions/'.$id, $data)->assertUnprocessable(); $data = $this->payload(); $data['payload']['contact_consent'] = false; $this->postJson('/api/v1/investment/submissions/'.$id.'/submit', $data)->assertUnprocessable(); $data = $this->payload(); $data['payload']['participation_id'] = $this->option('participation', 'other'); $this->putJson('/api/v1/investment/submissions/'.$id, $data)->assertUnprocessable(); $opportunity->update(['status' => 'draft']); $this->postJson('/api/v1/investment/submissions/'.$id.'/submit', $this->payload())->assertNotFound(); $this->assertDatabaseCount('interest_requests', 0); } public function test_attachments_are_private_and_content_changes_are_blocked(): void { Storage::fake('local'); $user = User::factory()->create(); $id = $this->actingAs($user)->postJson('/api/v1/investment/submissions')->assertCreated()->json('id'); $this->post('/investidor/manifestacoes/'.$id.'/attachments', ['version' => '1', 'file' => UploadedFile::fake()->createWithContent('nota.txt', 'Anexo privado de teste')])->assertSessionHasNoErrors()->assertRedirect(); $attachment = DB::table('investment_submission_attachments')->first(); $document = DB::table('documents')->where('id', $attachment->document_id)->first(); $url = '/investidor/manifestacoes/'.$id.'/attachments/'.$attachment->id; $this->get($url)->assertDownload('nota.txt'); $this->assertSame('confidential', $document->data_classification); $this->actingAs(User::factory()->create())->get($url)->assertNotFound(); Storage::disk('local')->put($document->storage_key, 'Alteração'); $this->actingAs($user)->get($url)->assertNotFound(); $this->assertSame(2, InvestmentSubmission::findOrFail($id)->version); } public function test_context_editing_combined_facets_and_education_discovery(): void { $this->seed(InvestmentReferenceSeeder::class); $manager = User::factory()->create(); config(['portal.managers' => [$manager->email]]); $opportunity = $this->opportunity(); $facet = $this->option('education', 'laboratory'); $data = ['version' => 0, 'is_education' => true, 'facets' => [$facet], 'source_reference' => 'Fonte exclusivamente sintética']; $url = '/api/v1/investment/admin/opportunities/'.$opportunity->id.'/context'; $this->actingAs($manager)->putJson($url, $data)->assertOk()->assertJsonPath('version', 1); $this->assertSame('draft', $opportunity->fresh()->status); $this->putJson($url, $data)->assertConflict(); $this->getJson('/api/v1/investment/search?education=1')->assertJsonPath('total', 0); $opportunity->refresh()->update(['status' => 'published', 'published_at' => now()]); $this->getJson('/api/v1/investment/search?education=1&province=Gaza&facets[]='.$facet)->assertOk()->assertJsonPath('total', 1); $this->getJson('/api/v1/investment/search?education=1&province=Maputo')->assertJsonPath('total', 0); $this->get('/educacao')->assertInertia(fn (Assert $page) => $page->where('filters.education', true)->has('results.data', 1)); $this->actingAs(User::factory()->create())->putJson($url, [...$data, 'version' => 1])->assertForbidden(); } public function test_map_discloses_only_public_coordinates_of_published_opportunities(): void { $opportunity = $this->opportunity(); $id = (string) Str::uuid(); DB::table('geo_locations')->insert(['id' => $id, 'code' => $id, 'name' => 'Local de teste', 'location_type' => 'site', 'data_classification' => 'public']); DB::table('opportunity_locations')->insert(['opportunity_id' => $opportunity->id, 'geo_location_id' => $id]); DB::statement("INSERT INTO geo_geometries(geo_location_id,geometry_type,geom,data_classification) VALUES(?, 'point', ST_SetSRID(ST_MakePoint(32,-25),4326), 'public')", [$id]); $this->getJson('/api/v1/investment/map')->assertOk()->assertJsonCount(1, 'features'); DB::table('geo_geometries')->where('geo_location_id', $id)->update(['data_classification' => 'restricted']); $this->getJson('/api/v1/investment/map')->assertJsonCount(0, 'features'); DB::table('geo_geometries')->where('geo_location_id', $id)->update(['data_classification' => 'public']); $opportunity->update(['status' => 'draft']); $this->getJson('/api/v1/investment/map')->assertJsonCount(0, 'features'); } public function test_territorial_filters_intersect_and_hide_withdrawn_relations(): void { $this->artisan('portfolio:import', ['--approval' => 'Autorização sintética exclusivamente para teste.'])->assertSuccessful(); $clusters = InvestmentDossier::whereNotNull('cluster_id')->limit(2)->get(); $corridor = InvestmentDossier::whereNotNull('corridor_id')->firstOrFail(); $opportunity = $this->opportunity(); foreach ($clusters as $cluster) { DB::table('opportunity_clusters')->insert(['opportunity_id' => $opportunity->id, 'cluster_id' => $cluster->cluster_id]); } DB::table('opportunity_corridors')->insert(['opportunity_id' => $opportunity->id, 'corridor_id' => $corridor->corridor_id]); $url = '/api/v1/investment/search?cluster_id='.$clusters[0]->cluster_id.'&corridor_id='.$corridor->corridor_id; $this->getJson($url)->assertOk()->assertJsonPath('total', 1); $this->getJson($url.'&province=Maputo')->assertJsonPath('total', 0); $this->getJson('/api/v1/investment/search?cluster_id='.$clusters[1]->cluster_id)->assertJsonPath('total', 1); $clusters[0]->update(['publication_status' => 'draft']); $this->getJson($url)->assertJsonPath('total', 0); $this->getJson('/api/v1/investment/opportunities/'.$opportunity->slug)->assertJsonCount(1, 'context.clusters'); $this->getJson('/api/v1/investment/references')->assertJsonMissing(['id' => $clusters[0]->cluster_id]); } public function test_favourites_comparison_and_csv_respect_publication_and_ownership(): void { $user = User::factory()->create(); $opportunity = $this->opportunity(); $opportunity->update(['title' => '=FORMULA()']); $this->actingAs($user)->postJson('/api/v1/investment/favourites/'.$opportunity->id, ['saved' => true])->assertOk(); $this->postJson('/api/v1/investment/favourites/'.$opportunity->id, ['saved' => true])->assertOk(); $this->assertDatabaseCount('opportunity_favourites', 1); $this->getJson('/api/v1/investment/compare?ids[]='.$opportunity->id)->assertOk()->assertJsonCount(1, 'records'); $this->assertStringContainsString("'=FORMULA()", $this->get('/api/v1/investment/export')->assertOk()->streamedContent()); $this->actingAs(User::factory()->create())->getJson('/api/v1/investment/submissions')->assertJsonCount(0, 'favourites'); $opportunity->update(['status' => 'draft']); $this->getJson('/api/v1/investment/compare?ids[]='.$opportunity->id)->assertNotFound(); $this->postJson('/api/v1/investment/favourites/'.$opportunity->id, ['saved' => true])->assertNotFound(); } public function test_reference_updates_preserve_codes_and_other_option(): void { $this->seed(InvestmentReferenceSeeder::class); $user = User::factory()->create(); config(['portal.managers' => [$user->email]]); $id = $this->option('education', 'other'); $data = ['group' => 'education', 'code' => 'other', 'label' => 'Outro — detalhar', 'active' => true, 'requires_description' => true, 'sort_order' => 1]; $this->actingAs($user)->postJson('/api/v1/investment/admin/references/'.$id, $data)->assertOk(); $this->postJson('/api/v1/investment/admin/references/'.$id, [...$data, 'active' => false])->assertUnprocessable(); $this->postJson('/api/v1/investment/admin/references/'.$id, [...$data, 'code' => 'changed'])->assertUnprocessable(); $count = DB::table('investment_reference_options')->count(); $this->seed(InvestmentReferenceSeeder::class); $this->assertSame($count, DB::table('investment_reference_options')->count()); $this->assertSame('Outro — detalhar', DB::table('investment_reference_options')->where('id', $id)->value('label')); } }