Files
Investidor-V2/tests/Feature/InvestmentThematicMapTest.php
T

133 lines
7.7 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\Opportunity;
use App\Models\User;
use App\Services\InvestmentGeography;
use App\Support\IsolatedQaDatabase;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
class InvestmentThematicMapTest extends TestCase
{
use DatabaseTransactions;
protected function setUp(): void
{
parent::setUp();
$this->assertTrue(IsolatedQaDatabase::allowsSchemaChanges());
$this->withoutVite();
$this->withoutMiddleware(ThrottleRequests::class);
}
private function proof(string $category = 'resource', string $purpose = 'local'): array
{
return ['id' => (string) Str::uuid(), 'category' => $category, 'purpose' => $purpose,
'statement' => 'SYNTHETIC QA documented observation', 'source' => 'Synthetic public source',
'locator' => 'Table QA', 'period' => '2026', 'scope' => 'Synthetic district',
'coverage' => 'Partial observation; no availability confirmed', 'verified_on' => '2026-10-01'];
}
private function record(string $prefix, array $proofs, ?string $territory = null): Opportunity
{
$record = Opportunity::create(['sector_id' => 1, 'reference' => 'QA-'.Str::uuid(), 'slug' => 'qa-'.Str::uuid(),
'title' => $prefix.' synthetic thematic map', 'status' => 'published', 'published_at' => now()->subMinute(),
'primary_territory_id' => $territory]);
$record->forceFill(['workflow_context' => ['promotional_summary' => ['projection' => ['proactive' => [
'evidence' => $proofs, 'state' => 'hypothesis', 'limitations' => 'SYNTHETIC QA scope only']]],
'private_evidence' => 'PRIVATE-DRAFT-MUST-NOT-LEAK']])->save();
return $record;
}
private function url(string $prefix, array $filters = []): string
{
return '/api/v1/investment/map?'.http_build_query(['q' => $prefix, ...$filters]);
}
public function test_only_selected_public_local_thematic_evidence_is_exposed_with_its_sources_and_limits(): void
{
$prefix = 'THEME-'.Str::uuid();
$record = $this->record($prefix, [$this->proof(), $this->proof('infrastructure'), $this->proof('resource', 'methodology'), $this->proof('demand')]);
$before = $record->fresh()->getRawOriginal('workflow_context');
$response = $this->getJson($this->url($prefix))->assertOk()->assertJsonPath('matched_count', 1)
->assertJsonPath('thematic_records.0.id', $record->id)->assertJsonCount(3, 'thematic_records.0.evidence')
->assertJsonPath('thematic_records.0.evidence.0.source', 'Synthetic public source')
->assertJsonPath('thematic_records.0.evidence.0.period', '2026')
->assertJsonPath('thematic_records.0.evidence.0.scope', 'Synthetic district')
->assertJsonPath('thematic_records.0.evidence.2.category', 'economic_potential')
->assertJsonPath('thematic_records.0.evidence.2.source_category', 'demand')
->assertJsonPath('thematic_records.0.state', 'hypothesis')->assertJsonCount(0, 'features')->assertJsonCount(1, 'unlocated');
$this->assertStringNotContainsString('PRIVATE-DRAFT-MUST-NOT-LEAK', $response->getContent());
$this->assertSame($before, $record->fresh()->getRawOriginal('workflow_context'));
}
public function test_workforce_is_explicit_and_economic_context_preserves_each_source_without_inferred_totals(): void
{
$prefix = 'THEME-'.Str::uuid();
$this->record($prefix, [$this->proof('workforce'), $this->proof('activity'), $this->proof('supply'), $this->proof('demand'), $this->proof('workforce', 'methodology')]);
$r = $this->getJson($this->url($prefix))->assertOk()->assertJsonCount(4, 'thematic_records.0.evidence')
->assertJsonPath('thematic_records.0.evidence.0.category', 'workforce')
->assertJsonPath('thematic_records.0.evidence.0.source_category', 'workforce')
->assertJsonPath('thematic_records.0.state', 'hypothesis')->assertJsonCount(0, 'features')->json('thematic_records.0');
foreach (['activity', 'supply', 'demand'] as $i => $source) {
$this->assertSame('economic_potential', $r['evidence'][$i + 1]['category']);
$this->assertSame($source, $r['evidence'][$i + 1]['source_category']);
$this->assertSame('2026', $r['evidence'][$i + 1]['period']);
}
$this->assertArrayNotHasKey('available_workers', $r);
$this->assertArrayNotHasKey('potential_score', $r);
}
public function test_unpublished_future_restricted_and_unselected_private_evidence_are_not_disclosed(): void
{
$prefix = 'THEME-'.Str::uuid();
$draft = $this->record($prefix, [$this->proof()]);
$draft->update(['status' => 'draft']);
$future = $this->record($prefix, [$this->proof()]);
$future->update(['published_at' => now()->addDay()]);
$restricted = $this->record($prefix, [$this->proof()]);
$restricted->forceFill(['owner_user_id' => User::factory()->create()->id])->save();
$private = $this->record($prefix, []);
$private->forceFill(['workflow_context' => ['private_evidence' => [$this->proof()]]])->save();
$this->getJson($this->url($prefix))->assertOk()->assertJsonPath('matched_count', 1)->assertJsonCount(0, 'thematic_records');
}
public function test_thematic_records_follow_the_same_province_and_district_filters_without_inventing_coordinates(): void
{
$prefix = 'THEME-'.Str::uuid();
$geo = app(InvestmentGeography::class);
$districts = $geo->publicReferences()->where('location_type', 'district')->get(['id', 'parent_id']);
$district = $districts->first();
$other = $districts->first(fn ($row) => $row->parent_id !== $district->parent_id);
$this->assertNotNull($other);
$record = $this->record($prefix, [$this->proof()], $district->id);
$this->record($prefix, [$this->proof('infrastructure')], $other->id);
foreach ([['province_id' => $district->parent_id], ['province_id' => $district->parent_id, 'district_id' => $district->id]] as $filters) {
$this->getJson($this->url($prefix, $filters))->assertOk()->assertJsonPath('matched_count', 1)
->assertJsonCount(1, 'thematic_records')->assertJsonPath('thematic_records.0.id', $record->id)
->assertJsonCount(0, 'features')->assertJsonCount(1, 'unlocated');
}
$this->assertNull($record->fresh()->district);
}
public function test_thematic_evidence_reuses_existing_site_coordinates_without_relocating_the_resource(): void
{
$prefix = 'THEME-'.Str::uuid();
$record = $this->record($prefix, [$this->proof()]);
$site = (string) Str::uuid();
DB::table('geo_locations')->insert(['id' => $site, 'name' => 'Synthetic opportunity site', 'code' => $site,
'location_type' => 'site', 'data_classification' => 'public', 'source_system' => 'investment-site-editor']);
DB::insert("INSERT INTO geo_geometries(id,geo_location_id,geometry_type,geom,properties,data_classification) VALUES (?,?,'point',ST_SetSRID(ST_MakePoint(32.5,-25.9),4326),'{}'::jsonb,'public')", [(string) Str::uuid(), $site]);
$record->update(['primary_territory_id' => $site]);
$this->getJson($this->url($prefix))->assertOk()->assertJsonCount(1, 'features')
->assertJsonPath('features.0.geometry.coordinates', [32.5, -25.9])
->assertJsonPath('features.0.properties.id', $record->id)->assertJsonPath('thematic_records.0.id', $record->id);
$this->assertSame($site, $record->fresh()->primary_territory_id);
}
}