withoutVite(); config(['existing-business.enabled' => true]); } private function business(?string $geometry = 'POINT(32 -26)', array $location = [], array $attributes = []): string { $id = (string) Str::uuid(); DB::table('existing_businesses')->insert([...[ 'id' => $id, 'code' => 'QA-'.Str::uuid(), 'owner_user_id' => User::factory()->create()->id, 'modality' => 'company_sale', 'status' => 'published', 'classification' => 'confidential', 'version_no' => 1, 'payload' => Crypt::encryptString('{"title":"PRIVATE SYNTHETIC"}'), 'public_title' => 'Synthetic public '.$id, 'public_slug' => 'qa-'.$id, 'published_at' => now()->subDay(), 'created_at' => now(), 'updated_at' => now(), ], ...$attributes]); if ($geometry !== null || $location !== []) { DB::table('existing_business_locations')->insert([...[ 'id' => (string) Str::uuid(), 'existing_business_id' => $id, 'precision_level' => 'exact', 'is_primary' => true, 'is_public' => true, 'geom' => $geometry === null ? null : DB::raw("ST_GeomFromText('".$geometry."',4326)"), 'created_at' => '2026-10-01 10:00:00+00', 'updated_at' => '2026-10-02 10:00:00+00', ], ...$location]); } return $id; } private function url(string $origin, array $filters = []): string { return '/api/v1/business-opportunities?'.http_build_query(['near_business' => $origin, 'radius_km' => 20, ...$filters]); } public function test_radius_orders_by_distance_excludes_unusable_and_private_points_and_retains_context(): void { $origin = $this->business(); $near = $this->business('POINT(32.01 -26)'); $far = $this->business('POINT(34 -26)'); $this->business('POINT(32.001 -26)', ['is_public' => false]); $this->business('POINT(32.001 -26)', ['is_primary' => false]); $this->business('POLYGON((32 -26,32.01 -26,32.01 -26.01,32 -26))'); $this->business('POINT(200 -26)'); $this->business(null); $this->business('POINT(32.001 -26)', [], ['status' => 'draft']); $this->business('POINT(32.001 -26)', [], ['published_at' => now()->addDay()]); $this->business('POINT(32.001 -26)', [], ['expires_at' => now()->subDay()]); $deleted = $this->business(); DB::table('existing_businesses')->where('id', $deleted)->update(['deleted_at' => now()]); $response = $this->getJson($this->url($origin))->assertOk()->assertJsonCount(2, 'data') ->assertJsonPath('data.0.id', $origin)->assertJsonPath('data.0.distance_km', 0) ->assertJsonPath('data.1.id', $near)->assertJsonPath('geographicCoverage.with_public_point', 3) ->assertJsonPath('geographicCoverage.without_public_point', 5) ->assertJsonPath('geographicCoverage.origin_id', $origin) ->assertJsonPath('data.1.location_recorded_at', '2026-10-02T10:00:00+00:00'); $this->assertGreaterThan(0, $response->json('data.1.distance_km')); $this->assertLessThan(2, $response->json('data.1.distance_km')); foreach (['geom', 'point', 'ewkt', 'owner_user_id', 'payload'] as $field) { $response->assertJsonMissingPath('data.0.'.$field)->assertJsonMissingPath('geographicCoverage.'.$field); } $this->assertStringNotContainsString('PRIVATE SYNTHETIC', $response->getContent()); $first = $this->getJson($this->url($origin, ['per_page' => 1]))->assertOk(); $next = $first->json('links.next'); $this->assertStringContainsString('near_business='.$origin, $next); $this->getJson($next)->assertOk()->assertJsonPath('data.0.id', $near); $this->getJson($this->url($origin, ['radius_km' => 300]))->assertOk()->assertJsonCount(3, 'data')->assertJsonPath('data.2.id', $far); $this->get('/negocios?'.http_build_query(['near_business' => $origin, 'radius_km' => 20])) ->assertInertia(fn (Assert $page) => $page->where('geographicCoverage.origin_id', $origin)->has('results.data', 2)); $query = '?'.http_build_query(['near_business' => $origin, 'radius_km' => 20]); $this->get('/negocios/qa-'.$near.$query)->assertInertia(fn (Assert $page) => $page ->where('similarRadiusAvailable', true)->where('returnUrl', '/negocios'.$query) ->where('interestUrl', '/negocios/qa-'.$near.'/manifestar-interesse'.$query)); } public function test_radius_combines_sector_modality_and_disclosed_classification_without_widening(): void { $sector = Sector::create(['name' => 'Synthetic QA', 'slug' => 'synthetic-proximity', 'is_active' => true, 'published_at' => now()]); $origin = $this->business(); $match = $this->business('POINT(32.01 -26)', [], ['sector_id' => $sector->id]); $this->business('POINT(32.01 -26)', [], ['sector_id' => $sector->id]); $other = $this->business('POINT(32.01 -26)', [], ['sector_id' => $sector->id, 'modality' => 'real_estate']); $wrongSector = $this->business('POINT(32.01 -26)'); foreach ([$match, $other, $wrongSector] as $id) { $row = ['related_id' => (string) $sector->id, 'classification' => ['target_type' => 'sector', 'purpose' => 'coverage', 'disclosure' => 'public', 'requested_state' => 'preliminary', 'external_status' => 'not_declared', 'source_reference' => 'Synthetic QA source']]; $state = ['schema_version' => ExistingBusinessClassification::VERSION, 'relations' => [$row], 'authors' => [], 'decisions' => [], 'publication' => [$row]]; DB::table('existing_business_revisions')->insert(['id' => (string) Str::uuid(), 'business_id' => $id, 'version_no' => 1, 'action' => 'classification_disclosed', 'snapshot' => Crypt::encryptString(json_encode(['context' => ['multidimensional' => $state]], JSON_THROW_ON_ERROR)), 'sha256' => hash('sha256', $id), 'actor_id' => DB::table('existing_businesses')->where('id', $id)->value('owner_user_id'), 'reason' => 'Synthetic disclosed classification', 'created_at' => now()]); } $this->getJson($this->url($origin, ['sector' => $sector->id, 'modality' => 'company_sale', 'association_type' => 'sector', 'association_target' => (string) $sector->id])) ->assertOk()->assertJsonCount(1, 'data')->assertJsonPath('data.0.id', $match) ->assertJsonPath('geographicCoverage.with_public_point', 1)->assertJsonPath('geographicCoverage.without_public_point', 0); } public function test_invalid_or_unpublished_reference_never_falls_back_to_an_area_or_centroid(): void { $private = $this->business('POINT(32 -26)', ['is_public' => false]); $polygon = $this->business('POLYGON((32 -26,32.01 -26,32.01 -26.01,32 -26))'); $draft = $this->business('POINT(32 -26)', [], ['status' => 'draft']); foreach ([$private, $polygon, $draft, (string) Str::uuid()] as $origin) { $this->getJson($this->url($origin))->assertUnprocessable()->assertJsonValidationErrors('near_business'); } $origin = $this->business(); foreach ([0, -1, 2001, 'bad'] as $radius) { $this->getJson($this->url($origin, ['radius_km' => $radius]))->assertUnprocessable()->assertJsonValidationErrors('radius_km'); } $this->getJson('/api/v1/business-opportunities?near_business='.$origin)->assertUnprocessable()->assertJsonValidationErrors('radius_km'); $this->getJson('/api/v1/business-opportunities?radius_km=20')->assertUnprocessable()->assertJsonValidationErrors('near_business'); } public function test_coarse_precision_does_not_disclose_a_finer_territory_through_filters_or_radius(): void { $ids = ['country' => (string) Str::uuid(), 'province' => (string) Str::uuid(), 'district' => (string) Str::uuid()]; foreach ($ids as $kind => $id) { DB::table('geo_locations')->insert(['id' => $id, 'code' => $id, 'name' => 'Synthetic '.$kind, 'location_type' => $kind, 'parent_id' => $kind === 'country' ? null : $ids[$kind === 'province' ? 'country' : 'province'], 'data_classification' => 'public', 'source_system' => 'cartografia-workbook', 'external_id' => '964a187ad32ea553b9e112cd159caa90b900050c42b7329594a75bcd798a96ff:'.$id]); } $coarse = $this->business('POINT(32 -26)', ['precision_level' => 'province', 'province_id' => $ids['province'], 'district_id' => $ids['district']]); $fine = $this->business('POINT(32 -26)', ['precision_level' => 'district', 'province_id' => $ids['province'], 'district_id' => $ids['district']]); $this->getJson('/api/v1/business-opportunities?province_id='.$ids['province'].'&district_id='.$ids['district']) ->assertOk()->assertJsonCount(1, 'data')->assertJsonPath('data.0.id', $fine); $this->getJson('/api/v1/business-opportunities/qa-'.$coarse)->assertOk()->assertJsonPath('data.district', null); foreach ([$coarse, $fine] as $origin) { $this->getJson($this->url($origin))->assertUnprocessable(); } $exact = $this->business('POINT(32 -26)', ['province_id' => $ids['province'], 'district_id' => $ids['district']]); $origin = $this->business(); $this->getJson($this->url($origin, ['province_id' => $ids['province'], 'district_id' => $ids['district']])) ->assertOk()->assertJsonCount(1, 'data')->assertJsonPath('data.0.id', $exact) ->assertJsonPath('geographicCoverage.with_public_point', 1)->assertJsonPath('geographicCoverage.without_public_point', 1); DB::table('geo_locations')->where('id', $ids['district'])->update(['data_classification' => 'restricted']); $this->getJson($this->url($exact))->assertUnprocessable(); $this->getJson('/api/v1/business-opportunities/qa-'.$coarse)->assertOk()->assertJsonPath('data.province', 'Synthetic province'); } }