Files
Investidor-V2/tests/Feature/HomeTerritorialStatisticsTest.php

170 lines
8.7 KiB
PHP

<?php
namespace Tests\Feature;
use App\Services\InvestmentGeography;
use App\Support\IsolatedQaDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
/** Temporary tables shadow QA tables on this connection only; no migrations or permanent fixture writes. */
class HomeTerritorialStatisticsTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
if (! IsolatedQaDatabase::allowsSchemaChanges()) {
throw new \LogicException('Explicit isolated QA identity required for temporary fixtures.');
}
DB::beginTransaction();
foreach ([
'opportunities' => '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();
}
}