create(); config(['portal.managers' => [$manager->email]]); $sector = Sector::create(['name' => 'Synthetic map', 'slug' => 'synthetic-map', 'is_active' => true]); $record = Opportunity::create(['sector_id' => $sector->id, 'reference' => 'MAP-COORD', 'slug' => 'map-coord', 'title' => 'Synthetic marker', 'status' => 'published', 'published_at' => now()->subMinute()]); $this->getJson('/api/v1/investment/map')->assertOk()->assertJsonPath('matched_count', 1)->assertJsonPath('complete', false)->assertJsonCount(1, 'unlocated'); $this->actingAs($manager); $url = '/management/opportunities/'.$record->id.'/coordinates'; $payload = ['version' => 0, 'latitude' => -25.9, 'longitude' => 32.5, 'source_reference' => 'Synthetic coordinates for automated tests only', 'confirmed_site' => true]; $this->postJson($url, [...$payload, 'latitude' => 91])->assertUnprocessable(); $this->postJson($url, [...$payload, 'confirmed_site' => false])->assertUnprocessable(); $this->postJson($url, $payload)->assertOk()->assertJsonPath('version', 1); $this->postJson($url, $payload)->assertConflict(); $this->getJson('/api/v1/investment/map')->assertOk()->assertJsonPath('complete', true)->assertJsonPath('features.0.properties.id', $record->id)->assertJsonPath('features.0.geometry.coordinates.0', 32.5); $record->update(['status' => 'draft']); $this->getJson('/api/v1/investment/map')->assertOk()->assertJsonCount(0, 'features')->assertJsonCount(0, 'unlocated'); } public function test_public_cascade_filters_saved_locations_and_hides_unapproved_references(): void { $country = $this->location('country'); $province = $this->location('province', $country); $district = $this->location('district', $province); $other = $this->location('province', $country); $private = $this->location('province', $country); foreach ([$country, $province, $district, $other] as $id) { DB::table('geo_locations')->where('id', $id)->update(['source_system' => 'cartografia-workbook', 'external_id' => '964a187ad32ea553b9e112cd159caa90b900050c42b7329594a75bcd798a96ff:'.$id, 'data_classification' => 'public']); } $sector = Sector::create(['name' => 'Synthetic', 'slug' => 'synthetic', 'is_active' => true]); $opportunity = Opportunity::create(['sector_id' => $sector->id, 'reference' => 'SYNTHETIC-PUBLIC', 'slug' => 'synthetic-public', 'title' => 'Synthetic', 'status' => 'published', 'published_at' => now()->subMinute(), 'primary_territory_id' => $district]); $this->getJson('/api/v1/investment/territory-options?type=province')->assertOk()->assertJsonCount(2, 'data'); $this->getJson('/api/v1/investment/territory-options?type=district&parent_id='.$province)->assertOk()->assertJsonPath('data.0.id', $district); $service = app(InvestmentCatalogue::class); $this->assertSame([$opportunity->id], $service->query(['province_id' => $province])->pluck('id')->all()); $this->assertSame([], $service->query(['province_id' => $other])->pluck('id')->all()); $this->getJson('/oportunidades?province_id='.$other.'&district_id='.$district)->assertUnprocessable(); $this->getJson('/oportunidades?province_id='.$private)->assertUnprocessable(); $this->get('/oportunidades?province_id='.$province)->assertOk(); $this->assertCount(2, $service->context($opportunity->id)['primary_territory_path']); $manager = User::factory()->create(); config(['portal.managers' => [$manager->email]]); $this->actingAs($manager)->post('/management/opportunities', ['sector_id' => $sector->id, 'reference' => 'SYNTHETIC-CREATE', 'slug' => 'synthetic-create', 'title' => 'Synthetic create', 'status' => 'draft', 'is_featured' => false, 'estimated_investment_currency' => 'USD', 'province_id' => $province, 'district_id' => $district])->assertRedirect(); $this->assertDatabaseHas('opportunities', ['reference' => 'SYNTHETIC-CREATE', 'primary_territory_id' => $district, 'territory_version' => 1]); $this->post('/management/clusters', ['code' => 'GEO-CREATE', 'name' => 'Synthetic cluster', 'sector' => 'Synthetic', 'status' => 'planned', 'province_id' => $province, 'district_id' => $district])->assertRedirect(); $this->assertDatabaseHas('economic_clusters', ['code' => 'GEO-CREATE', 'primary_territory_id' => $district]); $manager->forceFill(['two_factor_secret' => Crypt::encryptString('SYNTHETIC'), 'two_factor_confirmed_at' => now()])->save(); $zone = $this->postJson('/api/v2/investment/admin/zones', ['code' => 'GEO-ZEE', 'name' => 'Synthetic ZEE', 'category' => 'zee', 'province_id' => $province, 'district_id' => $district])->assertCreated()->json('id'); $this->assertDatabaseHas('investment_zones', ['id' => $zone, 'primary_territory_id' => $district]); $this->getJson('/api/v2/investment/admin/zones?province_id='.$province)->assertOk()->assertJsonPath('records.total', 1); $this->getJson('/api/v2/investment/admin/zones?province_id='.$other)->assertOk()->assertJsonPath('records.total', 0); $this->postJson('/management/territories/zones/'.$zone, ['version' => 1, 'path' => [$province]])->assertOk(); $this->assertDatabaseHas('investment_zones', ['id' => $zone, 'primary_territory_id' => $province, 'version_no' => 2, 'publication_status' => 'draft']); $this->assertSame(2, DB::table('investment_zone_revisions')->where('zone_id', $zone)->count()); DB::table('geo_locations')->where('id', $province)->update(['data_classification' => 'internal']); $this->getJson('/api/v1/investment/territory-options?type=district&parent_id='.$province)->assertOk()->assertJsonCount(0, 'data'); } public function test_cascade_assignment_persists_and_rejects_mismatched_parent_and_stale_write(): void { $user = User::factory()->create(); config(['portal.managers' => [$user->email]]); $this->actingAs($user); $province = $this->location('province'); $other = $this->location('province'); $district = $this->location('district', $province); $cluster = (string) Str::uuid(); DB::table('economic_clusters')->insert(['id' => $cluster, 'code' => 'SYNTHETIC', 'name' => 'Synthetic cluster', 'sector' => 'Synthetic', 'status' => 'planned']); $this->getJson('/management/territory-subjects?kind=clusters&q=Synthetic')->assertOk()->assertJsonPath('data.0.id', $cluster); $this->get('/management/territory-associations')->assertOk(); $this->getJson('/management/territory-options?type=district&parent_id='.$province)->assertOk()->assertJsonCount(1, 'data')->assertJsonPath('data.0.id', $district); $this->getJson('/management/territory-options?type=district&parent_id='.$other)->assertOk()->assertJsonCount(0, 'data'); $url = '/management/territories/clusters/'.$cluster; $this->postJson($url, ['version' => 0, 'path' => [$other, $district]])->assertUnprocessable(); $this->postJson($url, ['version' => 0, 'path' => [$province, $district]])->assertOk()->assertJsonPath('version', 1); $this->getJson($url)->assertOk()->assertJsonPath('path.0.id', $district); $this->assertDatabaseHas('economic_clusters', ['id' => $cluster, 'primary_territory_id' => $district]); $this->postJson($url, ['version' => 0, 'path' => []])->assertConflict(); $this->postJson($url, ['version' => 1, 'path' => []])->assertOk(); $this->assertDatabaseHas('economic_clusters', ['id' => $cluster, 'primary_territory_id' => null]); } public function test_non_managers_cannot_read_internal_options(): void { config(['portal.managers' => []]); $this->actingAs(User::factory()->create())->getJson('/management/territory-options?type=province')->assertForbidden(); } public function test_opportunity_edit_saves_the_geographic_fields_with_the_record(): void { $manager = User::factory()->create(); config(['portal.managers' => [$manager->email]]); $country = $this->location('country'); $province = $this->location('province', $country); $sector = Sector::create(['name' => 'Synthetic edit', 'slug' => 'synthetic-edit', 'is_active' => true]); $record = Opportunity::create(['sector_id' => $sector->id, 'reference' => 'EDIT-GEO', 'slug' => 'edit-geo', 'title' => 'Synthetic edit', 'status' => 'draft']); $this->actingAs($manager)->put('/management/opportunities/'.$record->id, [ 'sector_id' => $sector->id, 'reference' => 'EDIT-GEO', 'slug' => 'edit-geo', 'title' => 'Synthetic edit', 'status' => 'draft', 'is_featured' => false, 'estimated_investment_currency' => 'USD', 'province_id' => $province, 'territory_version' => 0, ])->assertRedirect(); $this->assertDatabaseHas('opportunities', ['id' => $record->id, 'primary_territory_id' => $province]); $this->assertSame($province, $record->fresh()->primaryTerritory->id); $this->get('/management/opportunities/'.$record->id.'/edit')->assertOk(); $this->getJson('/management/territory-options?type=locality&parent_id='.$province)->assertUnprocessable(); $this->getJson('/management/territory-options?type=district&parent_id=00000000-0000-4000-8000-000000000001')->assertUnprocessable(); $this->getJson('/management/territory-options?type=province&q='.$province)->assertOk()->assertJsonPath('data.0.id', $province); $other = $this->location('province', $country); $district = $this->location('district', $other); $payload = ['sector_id' => $sector->id, 'reference' => 'EDIT-GEO', 'slug' => 'edit-geo', 'title' => 'Must not persist', 'status' => 'draft', 'is_featured' => false, 'estimated_investment_currency' => 'USD', 'province_id' => $province, 'district_id' => $district, 'territory_version' => 1]; $this->putJson('/management/opportunities/'.$record->id, $payload)->assertUnprocessable()->assertJsonValidationErrors('district_id'); $this->assertDatabaseHas('opportunities', ['id' => $record->id, 'title' => 'Synthetic edit', 'primary_territory_id' => $province]); $this->putJson('/management/opportunities/'.$record->id, [...$payload, 'district_id' => null, 'territory_version' => 0])->assertConflict(); } public function test_database_rejects_a_cycle_wrong_parent_and_duplicate_source_code(): void { $country = $this->location('country'); $province = $this->location('province', $country); $district = $this->location('district', $province); foreach ([ fn () => DB::table('geo_locations')->where('id', $province)->update(['parent_id' => $district]), fn () => DB::table('geo_locations')->where('id', $district)->update(['parent_id' => $country]), fn () => DB::table('geo_locations')->where('id', $country)->update(['parent_id' => $country]), ] as $invalid) { try { DB::transaction($invalid); $this->fail('Invalid hierarchy accepted'); } catch (QueryException $error) { $this->assertStringContainsString('ERROR', $error->getMessage()); } } DB::table('geo_locations')->where('id', $province)->update(['source_code' => '01']); $other = $this->location('province', $country); try { DB::transaction(fn () => DB::table('geo_locations')->where('id', $other)->update(['source_code' => '01'])); $this->fail('Duplicate source code accepted'); } catch (QueryException $error) { $this->assertStringContainsString('geo_source_parent_code_uq', $error->getMessage()); } } public function test_integrity_migration_reverses_without_data_and_preserves_imported_codes(): void { $migration = require database_path('migrations/2026_09_29_100000_territory_integrity.php'); $migration->down(); $migration->up(); $country = $this->location('country'); DB::table('geo_locations')->where('id', $country)->update(['source_code' => '508']); $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Preserve imported source codes'); $migration->down(); } public function test_background_options_do_not_consume_form_validation_feedback(): void { $manager = User::factory()->create(); config(['portal.managers' => [$manager->email]]); $this->actingAs($manager)->from('/management/opportunities/create') ->post('/management/opportunities', [])->assertSessionHasErrors('reference'); $this->withCookie(config('session.cookie'), session()->getId())->withCredentials(); $this->getJson('/management/territory-options?type=province')->assertOk()->assertSessionHasErrors('reference'); $this->get('/management/opportunities/create')->assertInertia(fn (AssertableInertia $page) => $page->has('errors.reference')); } public function test_investment_areas_are_idempotent_and_preserve_legacy_sectors(): void { $old = Sector::create(['name' => 'Energia e Mineração', 'slug' => 'energia', 'is_active' => true]); $this->seed(InvestmentAreaSeeder::class); $count = Sector::count(); $this->seed(InvestmentAreaSeeder::class); $this->assertSame($count, Sector::count()); $this->assertSame('Energia e Mineração', $old->fresh()->name); $this->assertDatabaseHas('sectors', ['name' => 'Energia', 'slug' => 'energia-especifica']); $this->assertDatabaseHas('sectors', ['name' => 'Mineração']); } private function location(string $type, ?string $parent = null): string { $id = (string) Str::uuid(); DB::table('geo_locations')->insert(['id' => $id, 'code' => $id, 'name' => 'Synthetic '.$id, 'location_type' => $type, 'parent_id' => $parent]); return $id; } }