'id bigint, status text, published_at timestamptz, deleted_at timestamptz, owner_user_id bigint, workflow_context jsonb, primary_territory_id uuid, province text, geography_scope text', 'opportunity_locations' => 'opportunity_id bigint, geo_location_id uuid', 'geo_locations' => 'id uuid, parent_id uuid, name text, location_type text, deleted_at timestamptz, data_classification text, source_system text, external_id text', 'geo_territory_profiles' => 'geo_location_id uuid, source_id uuid, status text, valid_from date, valid_to date', 'geo_sources' => 'id uuid, status text', 'audit_events' => 'id bigint, source_system text, after_data jsonb, occurred_at timestamptz', ] as $table => $columns) { DB::statement('CREATE TEMP TABLE '.$table.' ('.$columns.') ON COMMIT DROP'); } } protected function tearDown(): void { if (DB::transactionLevel() > 0) { DB::rollBack(); } parent::tearDown(); } private function territory(string $name, string $type, ?string $parent = null, array $attributes = []): string { $id = (string) Str::uuid(); DB::table('geo_locations')->insert(array_merge([ 'id' => $id, 'parent_id' => $parent, 'name' => $name, 'location_type' => $type, 'deleted_at' => null, 'data_classification' => 'public', 'source_system' => 'cartografia-workbook', 'external_id' => '964a187ad32ea553b9e112cd159caa90b900050c42b7329594a75bcd798a96ff:'.$id, ], $attributes)); return $id; } private function opportunity(int $id, array $attributes = []): void { DB::table('opportunities')->insert(array_merge([ 'id' => $id, 'status' => 'published', 'published_at' => now()->subMinute(), 'deleted_at' => null, 'owner_user_id' => null, 'workflow_context' => null, 'primary_territory_id' => null, 'province' => null, 'geography_scope' => 'unspecified', ], $attributes)); } private function statistics(): array { return app(InvestmentGeography::class)->opportunityStatistics(); } public function test_multiple_territories_and_repeated_links_do_not_multiply_the_opportunity_total(): void { $country = $this->territory('QA country', 'country'); $first = $this->territory('QA province A', 'province', $country); $other = $this->territory('QA province B', 'province', $country); $district = $this->territory('QA district', 'district', $first); $this->opportunity(1, ['primary_territory_id' => $district, 'province' => 'Stale declaration']); foreach ([$district, $district, $first, $other, $other] as $id) { DB::table('opportunity_locations')->insert(['opportunity_id' => 1, 'geo_location_id' => $id]); } $this->assertSame(['opportunities' => 1, 'provinces' => 2], $this->statistics()); $this->opportunity(2, ['primary_territory_id' => $first]); $this->opportunity(3); $this->assertSame(['opportunities' => 3, 'provinces' => 2], $this->statistics()); } public function test_publication_guards_apply_to_both_counts(): void { $country = $this->territory('QA country', 'country'); $public = $this->territory('QA public province', 'province', $country); $hidden = $this->territory('QA unpublished coverage', 'province', $country); $this->opportunity(1, ['primary_territory_id' => $public]); foreach ([ ['status' => 'draft'], ['published_at' => now()->addDay()], ['published_at' => null], ['deleted_at' => now()], ['owner_user_id' => 9, 'workflow_context' => '{"publication":{"visibility":"restricted"}}'], ] as $index => $attributes) { $this->opportunity($index + 2, ['primary_territory_id' => $hidden, ...$attributes]); } $this->opportunity(20, ['primary_territory_id' => $hidden]); DB::table('audit_events')->insert(['id' => 1, 'source_system' => 'opportunity-origin-review', 'occurred_at' => now(), 'after_data' => '{"opportunity_id":20,"classification":"example"}']); $this->assertSame(['opportunities' => 1, 'provinces' => 1], $this->statistics()); } public function test_no_territory_and_national_scope_do_not_invent_provincial_coverage(): void { $country = $this->territory('QA country', 'country'); $this->territory('QA known province', 'province', $country); $this->opportunity(1); $this->opportunity(2, ['province' => 'QA known province', 'geography_scope' => 'national']); $this->opportunity(3, ['workflow_context' => '{"corridor_agribusiness":{"corridor":"Corredor de Maputo"}}']); $this->assertSame(['opportunities' => 3, 'provinces' => 0], $this->statistics()); } public function test_legacy_names_require_a_public_province_and_cannot_override_canonical_links(): void { $country = $this->territory('QA country', 'country'); $first = $this->territory('QA province A', 'province', $country); $this->territory('QA province B', 'province', $country); $this->opportunity(1, ['province' => ' qa PROVINCE A ']); $this->opportunity(2, ['province' => 'Unknown province']); $this->opportunity(3, ['province' => ' ']); $this->opportunity(4, ['province' => 'QA province B', 'primary_territory_id' => $first]); $this->opportunity(5, ['province' => 'QA province B']); DB::table('opportunity_locations')->insert(['opportunity_id' => 5, 'geo_location_id' => $first]); $this->assertSame(['opportunities' => 5, 'provinces' => 1], $this->statistics()); } public function test_private_removed_broken_and_cyclic_ancestry_are_not_counted(): void { $country = $this->territory('QA country', 'country'); $private = $this->territory('QA private', 'province', $country, ['data_classification' => 'restricted']); $child = $this->territory('QA private child', 'district', $private); $removed = $this->territory('QA removed', 'province', $country, ['deleted_at' => now()]); $broken = $this->territory('QA broken', 'province', (string) Str::uuid()); $cycle = $this->territory('QA cycle', 'province'); DB::table('geo_locations')->where('id', $cycle)->update(['parent_id' => $cycle]); foreach ([$private, $child, $removed, $broken, $cycle] as $index => $id) { $this->opportunity($index + 1, ['primary_territory_id' => $id]); } $this->assertSame(['opportunities' => 5, 'provinces' => 0], $this->statistics()); } public function test_city_and_province_are_distinct_but_duplicate_province_names_are_not_added(): void { $country = $this->territory('QA country', 'country'); foreach (['Maputo Cidade', 'Maputo ProvĂ­ncia', ' maputo cidade '] as $index => $name) { $this->opportunity($index + 1, ['primary_territory_id' => $this->territory($name, 'province', $country)]); } $this->assertSame(['opportunities' => 3, 'provinces' => 2], $this->statistics()); } public function test_empty_catalogue_returns_zeroes_and_query_count_does_not_grow_with_links(): void { DB::enableQueryLog(); DB::flushQueryLog(); $this->assertSame(['opportunities' => 0, 'provinces' => 0], $this->statistics()); $emptyCount = count(DB::getQueryLog()); DB::disableQueryLog(); $country = $this->territory('QA country', 'country'); $province = $this->territory('QA province', 'province', $country); for ($id = 1; $id <= 120; $id++) { $this->opportunity($id, ['primary_territory_id' => $province]); DB::table('opportunity_locations')->insert(['opportunity_id' => $id, 'geo_location_id' => $province]); } DB::enableQueryLog(); DB::flushQueryLog(); $this->assertSame(['opportunities' => 120, 'provinces' => 1], $this->statistics()); $this->assertSame($emptyCount, count(DB::getQueryLog())); $this->assertLessThanOrEqual(2, $emptyCount); DB::disableQueryLog(); } }