withoutVite(); } private function createOpportunity(?string $territory, string $prefix, string $status = 'published', ?string $province = null): Opportunity { return Opportunity::create(['sector_id' => 1, 'reference' => 'SYNTHETIC-QA-'.Str::uuid(), 'slug' => 'synthetic-'.Str::uuid(), 'title' => $prefix.' synthetic provincial search', 'status' => $status, 'published_at' => now()->subMinute(), 'primary_territory_id' => $territory, 'province' => $province, 'district' => null, 'geography_scope' => 'specific']); } private function requests(array $filters): array { $query = http_build_query([...$filters, 'per_page' => 50]); return ['/api/v1/investment/search?'.$query, '/api/v1/opportunities/search?'.$query]; } public function test_a_single_province_includes_all_lower_levels_and_linked_locations_in_both_apis(): void { $geo = app(InvestmentGeography::class); $locality = $geo->publicReferences()->where('location_type', 'locality')->orderBy('id')->first(); $this->assertNotNull($locality, 'Public locality fixture required'); $path = $geo->path($locality->id); $this->assertCount(4, $path); $prefix = 'SYNTHETIC-'.Str::uuid(); $records = array_map(fn ($node) => $this->createOpportunity($node->id, $prefix, 'published', 'Legacy label differs from canonical hierarchy'), $path); $linked = $this->createOpportunity(null, $prefix); $linked->additionalTerritories()->attach($locality->id); $records[] = $linked; $private = $this->createOpportunity($locality->id, $prefix, 'draft'); $deleted = $this->createOpportunity($locality->id, $prefix); $deleted->delete(); $otherProvince = $geo->publicReferences()->where('location_type', 'province')->where('id', '<>', $path[0]->id)->first(); $outside = $this->createOpportunity($otherProvince->id, $prefix); foreach ([['province_id' => $path[0]->id], ['province' => $path[0]->name], ['geo_location_id' => $path[0]->id]] as $entry) { foreach ($this->requests([...$entry, 'q' => $prefix]) as $url) { $response = $this->getJson($url)->assertOk()->assertJsonPath('total', 5); $this->assertEqualsCanonicalizing(array_map(fn ($row) => $row->id, $records), array_column($response->json('data'), 'id'), $url); foreach ($response->json('data') as $row) { $this->assertNull($row['district']); } foreach ([$private, $deleted, $outside] as $row) { $this->assertNotContains($row->id, array_column($response->json('data'), 'id')); } } } foreach ($records as $record) { $this->assertNull($record->fresh()->district); $this->assertSame($record->primary_territory_id, $record->fresh()->primary_territory_id); } $this->assertSame([$locality->id], $linked->additionalTerritories()->pluck('geo_locations.id')->all()); foreach ($this->requests(['province_id' => $path[0]->id, 'district_id' => $path[1]->id, 'q' => $prefix]) as $url) { // Province-only records are not silently assigned to a district. $this->getJson($url)->assertOk()->assertJsonPath('total', 4); } foreach ($this->requests(['district_id' => $path[1]->id]) as $url) { $this->getJson($url)->assertUnprocessable(); } } public function test_published_province_declaration_is_found_without_inventing_a_district_or_overriding_an_explicit_other_territory(): void { $geo = app(InvestmentGeography::class); $province = $geo->publicReferences()->where('location_type', 'province')->first(); $other = $geo->publicReferences()->where('location_type', 'province')->where('id', '<>', $province->id)->first(); $district = $geo->publicReferences()->where('parent_id', $province->id)->where('location_type', 'district')->first(); $prefix = 'SYNTHETIC-'.Str::uuid(); $legacy = $this->createOpportunity(null, $prefix, 'published', $province->name); $contradictory = $this->createOpportunity($other->id, $prefix, 'published', $province->name); $linkedOutside = $this->createOpportunity(null, $prefix, 'published', $province->name); $linkedOutside->additionalTerritories()->attach($other->id); foreach ([['province' => $province->name], ['province_id' => $province->id], ['geo_location_id' => $province->id]] as $entry) { foreach ($this->requests([...$entry, 'q' => $prefix]) as $url) { $this->getJson($url)->assertOk()->assertJsonPath('total', 1)->assertJsonPath('data.0.id', $legacy->id)->assertJsonPath('data.0.district', null); } } foreach ($this->requests(['province_id' => $province->id, 'district_id' => $district->id, 'q' => $prefix]) as $url) { $this->getJson($url)->assertOk()->assertJsonPath('total', 0); } $this->assertNull($legacy->fresh()->primary_territory_id); $this->assertNull($legacy->fresh()->district); $this->assertSame($other->id, $contradictory->fresh()->primary_territory_id); } public function test_maputo_names_ids_page_map_and_export_share_the_existing_approved_corridor_coverage(): void { $provinceId = 'bfde2651-2b46-5d9c-a500-733fa55a8c5c'; $expected = Opportunity::published()->where('workflow_context->corridor_agribusiness->source_sha256', CorridorAgribusinessOpportunities::SOURCE_HASH) ->where('workflow_context->corridor_agribusiness->corridor', 'Corredor de Maputo')->pluck('id')->all(); $this->assertCount(5, $expected); foreach ([['province' => 'Maputo Província'], ['province' => ' mApUtO Província '], ['province_id' => $provinceId]] as $entry) { foreach ($this->requests([...$entry, 'sector' => 'agronegocio']) as $url) { $response = $this->getJson($url)->assertOk()->assertJsonPath('total', 5); $this->assertEqualsCanonicalizing($expected, array_column($response->json('data'), 'id')); foreach ($response->json('data') as $row) { $this->assertNull($row['district']); $this->assertStringContainsString('contexto provincial do corredor', $row['territorial_summary']); } } } $query = http_build_query(['sector' => 'agronegocio', 'province' => 'Maputo Província']); $this->get('/oportunidades?'.$query)->assertInertia(fn (AssertableInertia $page) => $page ->component('investment/catalogue')->where('filters.province_id', $provinceId)->missing('filters.province') ->has('territorySelection', 1)->where('results.total', 5)); $this->getJson('/api/v1/investment/map?'.$query)->assertOk()->assertJsonCount(0, 'features')->assertJsonCount(5, 'unlocated'); $csv = $this->get('/api/v1/investment/export?'.$query)->assertOk()->streamedContent(); foreach (Opportunity::whereIn('id', $expected)->get() as $row) { $this->assertStringContainsString($row->title, $csv); } foreach ($this->requests(['sector' => 'turismo', 'province_id' => $provinceId]) as $url) { $response = $this->getJson($url)->assertOk(); foreach ($response->json('data') as $row) { $this->assertNotContains($row['id'], $expected); } } } }