170 lines
8.7 KiB
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();
|
|
}
|
|
}
|