withoutVite(); config(['existing-business.enabled' => true, 'portal.profile_access_enabled' => false, 'portal.profile_administration_enabled' => false]); $this->owner = User::factory()->create(); $country = $this->territory('country'); $this->province = $this->territory('province', $country); $this->district = $this->territory('district', $this->province); $this->sector = DB::table('sectors')->insertGetId(['name' => 'Synthetic point sector', 'slug' => 'synthetic-point-sector', 'is_active' => true]); } private function territory(string $kind, ?string $parent = null): string { $id = (string) Str::uuid(); DB::table('geo_locations')->insert(['id' => $id, 'name' => 'Synthetic '.$kind, 'code' => $id, 'location_type' => $kind, 'parent_id' => $parent, 'version_no' => 1, 'data_classification' => 'public', 'source_system' => 'cartografia-workbook', 'external_id' => '964a187ad32ea553b9e112cd159caa90b900050c42b7329594a75bcd798a96ff:'.$id]); return $id; } private function draft(array $payload = []): array { return ['modality' => 'participation_sale', 'classification' => 'restricted', 'reason' => 'Synthetic point preparation', 'public_title' => 'Synthetic public point listing', 'sector_id' => $this->sector, 'payload' => [...[ 'title' => 'Synthetic private point listing', 'description' => 'Synthetic description for a staged independent publication.', 'source_reference' => 'SYNTHETIC source already registered', 'source_date' => '2026-10-02', 'geography_scope' => 'specific', 'province_id' => $this->province, 'district_id' => $this->district, 'location_unknown' => 'no', 'disclosure_precision' => 'exact', 'location_point_enabled' => 'yes', 'location_latitude' => '-26.0001', 'location_longitude' => '32.0001', 'location_point_source_mode' => 'listing', 'location_point_share_source' => 'yes', ], ...$payload]]; } private function create(array $payload = []): array { return $this->actingAs($this->owner)->postJson($this->base, $this->draft($payload))->assertCreated()->json(); } private function designated(string $role): User { $user = User::factory()->create(); $user->forceFill(['two_factor_secret' => Crypt::encryptString('SYNTHETIC'), 'two_factor_confirmed_at' => now()])->save(); config(['portal.managers' => [...config('portal.managers', []), $user->email], 'existing-business.'.$role => [...config('existing-business.'.$role, []), $user->email]]); return $user; } private function approve(string $id, int $version): User { $locationsBefore = DB::table('existing_business_locations')->orderBy('id')->get()->toJson(); $roles = ['submitted' => null, 'triage' => 'triagers', 'in_review' => 'reviewers', 'approval_l1' => 'reviewers', 'approval_l2' => 'approvers_l1', 'approved' => 'approvers_l2']; $users = []; foreach ($roles as $action => $role) { if ($role === null) { $this->actingAs($this->owner); } else { $this->actingAsWithMfa($users[$role] ??= $this->designated($role)); } $this->postJson($this->base.'/'.$id.'/transitions', ['version_no' => $version++, 'action' => $action, 'note' => 'Synthetic normal workflow decision', 'review_consent' => $action === 'submitted'])->assertOk(); $this->assertSame($locationsBefore, DB::table('existing_business_locations')->orderBy('id')->get()->toJson()); } return $this->designated('publishers'); } public function test_point_stays_private_until_normal_independent_publication_then_feeds_radius_and_source_dates(): void { $record = $this->create(); $id = $record['id']; $this->assertDatabaseCount('existing_business_locations', 0); $raw = DB::table('existing_businesses')->where('id', $id)->value('payload'); $this->assertStringNotContainsString('-26.0001', $raw); $this->getJson($this->base.'/'.$id)->assertOk()->assertJsonPath('record.payload.location_point_source_mode', 'listing') ->assertHeader('Cache-Control', 'no-store, private'); $this->actingAs(User::factory()->create())->getJson($this->base.'/'.$id)->assertForbidden(); $this->getJson('/api/v1/business-opportunities')->assertOk()->assertJsonCount(0, 'data'); $publisher = $this->approve($id, 1); $this->actingAs($this->owner)->postJson($this->base.'/'.$id.'/transitions', ['version_no' => 7, 'action' => 'published', 'note' => 'Synthetic owner cannot self publish'])->assertForbidden(); $this->actingAsWithMfa($publisher)->postJson($this->base.'/'.$id.'/transitions', ['version_no' => 6, 'action' => 'published', 'note' => 'Synthetic stale publication'])->assertConflict(); $this->assertDatabaseCount('existing_business_locations', 0); $saved = $this->postJson($this->base.'/'.$id.'/transitions', ['version_no' => 7, 'action' => 'published', 'note' => 'Synthetic authorized publication'])->assertOk()->assertJsonPath('version_no', 8); $point = DB::table('existing_business_locations')->where('existing_business_id', $id)->first(); $this->assertTrue($point->is_primary); $this->assertTrue($point->is_public); $this->assertSame(8, $point->source_payload_version); $this->assertSame($this->district, $point->district_id); $response = $this->getJson('/api/v1/business-opportunities?'.http_build_query(['near_business' => $id, 'radius_km' => 10]))->assertOk() ->assertJsonCount(1, 'data')->assertJsonPath('data.0.id', $id) ->assertJsonPath('data.0.location_source_reference', 'SYNTHETIC source already registered') ->assertJsonPath('data.0.location_source_date', '2026-10-02')->assertJsonPath('geographicCoverage.origin_source_date', '2026-10-02') ->assertJsonMissingPath('data.0.location_latitude')->assertJsonMissingPath('data.0.source_payload_version'); $this->assertStringNotContainsString('-26.0001', $response->getContent()); $this->get('/negocios/'.$saved->json('public_slug'))->assertOk()->assertInertia(fn ($page) => $page->where('similarRadiusAvailable', true)); $snapshot = json_decode(Crypt::decryptString(DB::table('existing_business_revisions')->where('business_id', $id)->where('action', 'published')->value('snapshot')), true); $this->assertSame('Synthetic district', $snapshot['context']['point_publication']['used']['reference_snapshot'][1]['name']); $this->assertSame('SYNTHETIC source already registered', $snapshot['context']['point_publication']['used']['source_reference']); $this->assertCount(1, $snapshot['locations']); $before = DB::table('existing_business_revisions')->where('business_id', $id)->pluck('sha256')->all(); DB::table('geo_locations')->where('id', $this->district)->update(['name' => 'Updated synthetic district', 'version_no' => 2]); $this->assertSame($before, DB::table('existing_business_revisions')->where('business_id', $id)->pluck('sha256')->all()); $this->putJson($this->base.'/'.$id, [...$this->draft(), 'version_no' => 8])->assertForbidden(); } public function test_point_chooses_one_declared_path_and_does_not_assume_the_first_of_multiple_locations(): void { $other = $this->territory('district', $this->province); $paths = [['province_id' => $this->province, 'district_id' => $this->district], ['province_id' => $this->province, 'district_id' => $other]]; $record = $this->create(['geography_scope' => 'multiple', 'locations_json' => json_encode($paths)]); $url = $this->base.'/'.$record['id']; $this->postJson($url.'/transitions', ['version_no' => 1, 'action' => 'submitted', 'review_consent' => true, 'note' => 'Synthetic point needs a selected site']) ->assertUnprocessable()->assertJsonValidationErrors('location_point_territory'); $key = app(BusinessPointPublication::class)->pathKey($paths[1]); $point = app(BusinessPointPublication::class)->candidate([...$this->draft()['payload'], 'geography_scope' => 'multiple', 'locations_json' => json_encode($paths), 'location_point_territory' => $key]); $this->assertSame($other, $point['path']['district_id']); $this->assertSame('-26.0001', $record['payload']['location_latitude']); $this->expectException(ValidationException::class); app(BusinessPointPublication::class)->candidate([...$this->draft()['payload'], 'geography_scope' => 'multiple', 'locations_json' => json_encode([$paths[0]]), 'location_point_territory' => $key]); } public function test_inapplicable_point_fields_remain_private_and_do_not_block_or_activate_a_point(): void { foreach ([['geography_scope' => 'national'], ['geography_scope' => 'digital'], ['disclosure_precision' => 'administrative'], ['location_unknown' => 'yes'], ['location_point_enabled' => 'no']] as $changes) { $payload = [...$this->draft()['payload'], ...$changes, 'location_latitude' => 'unfinished', 'location_point_source_mode' => 'custom', 'location_point_source_date' => 'unfinished']; $record = $this->actingAs($this->owner)->postJson($this->base, $this->draft($payload))->assertCreated()->json(); $this->assertSame('unfinished', $record['payload']['location_latitude']); $this->assertNull(app(BusinessPointPublication::class)->candidate($payload)); } $this->assertDatabaseCount('existing_business_locations', 0); } public function test_custom_source_unknown_date_and_reserved_reference_are_preserved_without_false_public_citation(): void { $record = $this->create(['location_point_source_mode' => 'custom', 'location_point_source_reference' => 'SYNTHETIC PRIVATE point source', 'location_point_source_date' => '', 'location_point_share_source' => 'no']); $publisher = $this->approve($record['id'], 1); $this->actingAsWithMfa($publisher)->postJson($this->base.'/'.$record['id'].'/transitions', ['version_no' => 7, 'action' => 'published', 'note' => 'Synthetic authorized source-aware publication'])->assertOk(); $this->getJson('/api/v1/business-opportunities')->assertOk()->assertJsonPath('data.0.location_source_reference', null)->assertJsonPath('data.0.location_source_date', null) ->assertDontSee('SYNTHETIC PRIVATE point source'); $snapshot = Crypt::decryptString(DB::table('existing_business_revisions')->where('business_id', $record['id'])->where('action', 'published')->value('snapshot')); $this->assertStringContainsString('SYNTHETIC PRIVATE point source', $snapshot); $this->assertStringNotContainsString('SYNTHETIC PRIVATE point source', DB::table('existing_business_revisions')->where('business_id', $record['id'])->where('action', 'published')->value('snapshot')); } public function test_invalid_coordinates_and_retired_territory_do_not_publish_or_change_the_workflow(): void { foreach ([['location_latitude' => '91'], ['location_longitude' => '-181'], ['location_latitude' => 'NaN'], ['location_latitude' => 'Infinity']] as $change) { $this->actingAs($this->owner)->postJson($this->base, $this->draft($change))->assertUnprocessable(); } $record = $this->create(['location_longitude' => '']); $this->postJson($this->base.'/'.$record['id'].'/transitions', ['version_no' => 1, 'action' => 'submitted', 'review_consent' => true, 'note' => 'Synthetic incomplete point submission'])->assertUnprocessable(); $this->assertDatabaseCount('business_workflow_transitions', 0); $record = $this->create(); $publisher = $this->approve($record['id'], 1); DB::table('geo_locations')->where('id', $this->district)->update(['data_classification' => 'restricted']); $this->actingAsWithMfa($publisher)->postJson($this->base.'/'.$record['id'].'/transitions', ['version_no' => 7, 'action' => 'published', 'note' => 'Synthetic retired point cannot publish'])->assertUnprocessable(); $this->assertDatabaseHas('existing_businesses', ['id' => $record['id'], 'status' => 'approved', 'version_no' => 7]); $this->assertDatabaseCount('existing_business_locations', 0); } public function test_migration_preserves_legacy_rows_and_public_queries_work_without_optional_source_columns(): void { $migration = require database_path('migrations/2026_11_03_000000_existing_business_location_sources.php'); $migration->down(); $this->assertFalse(Schema::hasColumn('existing_business_locations', 'public_source_reference')); $this->getJson('/api/v1/business-opportunities')->assertOk(); $migration->up(); $this->create(); DB::table('existing_business_locations')->insert(['id' => (string) Str::uuid(), 'existing_business_id' => DB::table('existing_businesses')->value('id'), 'province_id' => $this->province, 'precision_level' => 'province', 'is_primary' => true, 'is_public' => false, 'source_system' => 'synthetic-private-source']); $this->expectException(\RuntimeException::class); $migration->down(); } public function test_explicit_point_replacement_preserves_primary_identity_secondary_rows_and_prior_geometry_in_history(): void { $record = $this->create(); $primary = (string) Str::uuid(); $secondary = (string) Str::uuid(); foreach ([$primary, $secondary] as $location) { DB::table('existing_business_locations')->insert(['id' => $location, 'existing_business_id' => $record['id'], 'province_id' => $this->province, 'district_id' => $this->district, 'precision_level' => 'exact', 'is_primary' => $location === $primary, 'is_public' => false, 'geom' => DB::raw("ST_GeomFromText('POINT(33 -27)',4326)"), 'source_system' => 'SYNTHETIC previous source']); } $secondaryBefore = DB::table('existing_business_locations')->where('id', $secondary)->first(); $this->getJson($this->base.'/'.$record['id'])->assertOk()->assertJsonPath('registeredLocation.longitude', 33); $publisher = $this->approve($record['id'], 1); $this->actingAsWithMfa($publisher)->postJson($this->base.'/'.$record['id'].'/transitions', ['version_no' => 7, 'action' => 'published', 'note' => 'Synthetic explicit reviewed replacement'])->assertOk(); $this->assertDatabaseCount('existing_business_locations', 2); $this->assertDatabaseHas('existing_business_locations', ['id' => $primary, 'is_primary' => true, 'is_public' => true, 'source_system' => 'portal-business-form']); $this->assertEquals($secondaryBefore, DB::table('existing_business_locations')->where('id', $secondary)->first()); $snapshot = json_decode(Crypt::decryptString(DB::table('existing_business_revisions')->where('business_id', $record['id'])->where('action', 'published')->value('snapshot')), true); $this->assertSame('SRID=4326;POINT(33 -27)', $snapshot['context']['point_publication']['previous']['geometry_snapshot']); $this->assertSame('SYNTHETIC previous source', $snapshot['context']['point_publication']['previous']['source_system']); } public function test_no_point_opt_in_preserves_existing_location_and_source_through_publication(): void { $record = $this->create(['location_point_enabled' => 'no']); DB::table('existing_business_locations')->insert(['id' => (string) Str::uuid(), 'existing_business_id' => $record['id'], 'province_id' => $this->province, 'precision_level' => 'province', 'is_primary' => true, 'is_public' => true, 'source_system' => 'SYNTHETIC preserved source', 'public_source_reference' => 'Synthetic previous public citation']); $before = DB::table('existing_business_locations')->first(); $publisher = $this->approve($record['id'], 1); $this->actingAsWithMfa($publisher)->postJson($this->base.'/'.$record['id'].'/transitions', ['version_no' => 7, 'action' => 'published', 'note' => 'Synthetic preserve source publication'])->assertOk(); $this->assertEquals($before, DB::table('existing_business_locations')->first()); $this->getJson('/api/v1/business-opportunities')->assertOk()->assertJsonPath('data.0.location_source_reference', 'Synthetic previous public citation')->assertJsonPath('data.0.distance_km', null); } }