149 lines
8.2 KiB
PHP
149 lines
8.2 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Opportunity;
|
|
use App\Services\CorridorAgribusinessOpportunities;
|
|
use App\Services\InvestmentGeography;
|
|
use App\Support\IsolatedQaDatabase;
|
|
use Illuminate\Foundation\Application;
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|
use Illuminate\Support\Str;
|
|
use Inertia\Testing\AssertableInertia;
|
|
use Tests\TestCase;
|
|
|
|
class ProvinceSearchCompatibilityTest extends TestCase
|
|
{
|
|
use DatabaseTransactions;
|
|
|
|
public function createApplication(): Application
|
|
{
|
|
$app = parent::createApplication();
|
|
if (! IsolatedQaDatabase::allowsSchemaChanges()) {
|
|
throw new \LogicException('Isolated QA required');
|
|
}
|
|
|
|
return $app;
|
|
}
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->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);
|
|
}
|
|
}
|
|
}
|
|
}
|