withoutVite(); config(['portal.profile_access_enabled' => true, 'portal.managers' => [], 'suitecrm.enabled' => false, 'investmoz_ai.enabled' => false]); $this->actor = F::operator(); $this->login($this->actor); $parent = null; foreach (['country', 'province', 'district'] as $level) { $id = (string) Str::uuid(); DB::table('geo_locations')->insert(['id' => $id, 'name' => 'SYNTHETIC review '.$level, 'code' => $id, 'location_type' => $level, 'parent_id' => $parent, 'data_classification' => 'public', 'source_system' => 'cartografia-workbook', 'external_id' => '964a187ad32ea553b9e112cd159caa90b900050c42b7329594a75bcd798a96ff:'.$id]); $parent = $id; } $this->territory = $parent; $this->source = hash('sha256', 'SYNTHETIC-SUPPLY-REVIEW-20261102'); DB::table('proactive_supply_sources')->insert(['sha256' => $this->source, 'name' => 'SYNTHETIC review source.xlsx', 'period' => 'Synthetic historical data', 'limitations' => 'Synthetic observations, not actual establishments', 'receipt' => json_encode(['private_contact' => 'SYNTHETIC PRIVATE CONTACT', 'nuit' => 'SYNTHETIC PRIVATE IDENTIFIER']), 'imported_at' => now()]); $base = ['source_hash' => $this->source, 'sheet' => 'SYNTHETIC QA', 'entity_key' => hash('sha256', 'SYNTHETIC ENTITY'), 'name' => 'SYNTHETIC 100% Algodão', 'province' => 'Zambézia', 'district' => 'Gurué', 'cae' => '01610', 'activity' => 'Cultivo de algodão', 'sector' => 'Synthetic agriculture', 'source_date' => 'Serial Excel original: 45000', 'duplicate_of' => null, 'identity_conflict' => false, 'territory_id' => $this->territory, 'territory_resolution' => 'exact_normalized_province_district']; foreach ([ 2 => [], 3 => ['name' => 'SYNTHETIC 1000', 'activity' => '', 'territory_id' => null, 'territory_resolution' => 'unresolved'], 4 => ['name' => 'Denominação não indicada', 'territory_id' => null, 'territory_resolution' => 'ambiguous'], 5 => ['name' => 'SYNTHETIC other denomination', 'identity_conflict' => true, 'territory_id' => '10000000-0000-4000-8000-000000000001'], 6 => ['duplicate_of' => $this->id(2)], 7 => ['name' => 'SYNTHETIC_UNDER_SCORE', 'territory_id' => null, 'territory_resolution' => 'unresolved'], ] as $number => $changes) { DB::table('proactive_supply_observations')->insert([...$base, 'id' => $this->id($number), 'row_number' => $number, 'fingerprint' => hash('sha256', 'SYNTHETIC-FINGERPRINT-'.($number === 6 ? 2 : $number)), ...$changes]); } } private function login(User $actor): void { $this->actingAs($actor)->withSession(['portal.mfa_actor' => $actor->id, 'portal.mfa_at' => time(), 'portal.access_revision' => (int) $actor->portal_access_revision]); } private function id(int $row): string { return hash('sha256', $this->source.'|SYNTHETIC QA|'.$row); } private function url(array $filters = []): string { return $this->base.'?'.http_build_query(['source_hash' => $this->source, ...$filters]); } public function test_unmapped_and_ambiguous_rows_are_recoverable_without_mutation_or_a_guessed_territory(): void { $before = DB::table('proactive_supply_observations')->where('source_hash', $this->source)->orderBy('id')->get()->toJson(); $response = $this->getJson($this->url())->assertOk()->assertHeader('Cache-Control', 'no-store, private') ->assertJsonPath('total', 3)->assertJsonPath('coverage.observations', 6)->assertJsonPath('coverage.distinct_observations', 5) ->assertJsonPath('coverage.repeated', 1)->assertJsonPath('coverage.unmapped', 3)->assertJsonPath('coverage.ambiguous', 1) ->assertJsonPath('coverage.territory_unavailable', 1)->assertJsonPath('coverage.identity_conflict', 1) ->assertJsonPath('coverage.unclassified', 1)->assertJsonPath('coverage.unnamed', 1); foreach ($response->json('rows') as $row) { $this->assertNull($row['territory_id']); $this->assertNull($row['canonical_territory']); } $this->getJson($this->url(['status' => 'ambiguous']))->assertOk()->assertJsonPath('total', 1)->assertJsonPath('rows.0.id', $this->id(4)); $this->getJson($this->url(['status' => 'all', 'observation_id' => $this->id(4)]))->assertOk() ->assertJsonPath('rows.0.sheet', 'SYNTHETIC QA')->assertJsonPath('rows.0.row_number', 4)->assertJsonPath('rows.0.source_hash', $this->source) ->assertJsonPath('rows.0.source_date', 'Serial Excel original: 45000'); $this->assertSame($before, DB::table('proactive_supply_observations')->where('source_hash', $this->source)->orderBy('id')->get()->toJson()); } public function test_private_receipts_identity_hashes_contacts_and_private_territories_are_not_projected(): void { $response = $this->getJson($this->url(['status' => 'all']))->assertOk()->assertDontSee('SYNTHETIC PRIVATE CONTACT')->assertDontSee('SYNTHETIC PRIVATE IDENTIFIER'); foreach ($response->json('rows') as $row) { foreach (['entity_key', 'fingerprint', 'receipt', 'contact', 'nuit', 'geom'] as $key) { $this->assertArrayNotHasKey($key, $row); } } foreach ($response->json('sources') as $source) { $this->assertArrayNotHasKey('receipt', $source); } $this->getJson($this->url(['status' => 'territory_unavailable']))->assertOk()->assertJsonCount(1, 'rows')->assertJsonPath('rows.0.canonical_territory', null); $this->getJson('/minhas-oportunidades/oferta-interna?territory_id='.$this->territory)->assertOk()->assertJsonMissingPath('rows.0.entity_key'); } public function test_repeated_sightings_identity_conflicts_and_activity_gaps_are_distinct_and_not_consolidated(): void { $this->getJson($this->url(['status' => 'repeated']))->assertOk()->assertJsonPath('total', 1)->assertJsonPath('rows.0.duplicate_of', $this->id(2)); $this->getJson($this->url(['status' => 'identity_conflict']))->assertOk()->assertJsonPath('rows.0.id', $this->id(5)); $this->getJson($this->url(['status' => 'unclassified']))->assertOk()->assertJsonPath('rows.0.id', $this->id(3)); $this->getJson($this->url(['status' => 'unnamed']))->assertOk()->assertJsonPath('rows.0.id', $this->id(4)); $this->assertSame(6, DB::table('proactive_supply_observations')->where('source_hash', $this->source)->count()); } public function test_text_search_is_accent_insensitive_and_wildcards_are_literal(): void { foreach (['algodao', 'ALGODÃO', '100%'] as $q) { $response = $this->getJson($this->url(['status' => 'all', 'q' => $q]))->assertOk(); $this->assertGreaterThan(0, $response->json('total')); $this->assertNotContains($this->id(3), array_column($response->json('rows'), 'id')); } $this->getJson($this->url(['status' => 'all', 'q' => 'UNDER_']))->assertOk()->assertJsonPath('total', 1)->assertJsonPath('rows.0.id', $this->id(7)); $this->getJson($this->url(['status' => 'all', 'q' => 'NO-SYNTHETIC-MATCH']))->assertOk()->assertJsonPath('total', 0)->assertJsonPath('status', 'Informação insuficiente nesta base e nestes filtros.'); } public function test_pagination_and_direct_reference_recover_the_same_source_line(): void { $base = (array) DB::table('proactive_supply_observations')->where('id', $this->id(2))->first(); for ($row = 100; $row < 126; $row++) { DB::table('proactive_supply_observations')->insert([...$base, 'id' => $this->id($row), 'row_number' => $row, 'name' => 'SYNTHETIC PAGE '.$row, 'fingerprint' => hash('sha256', 'SYNTHETIC-PAGE-'.$row)]); } $this->getJson($this->url(['status' => 'all', 'q' => 'SYNTHETIC PAGE']))->assertOk()->assertJsonPath('total', 26)->assertJsonCount(25, 'rows')->assertJsonPath('rows.0.row_number', 100); $this->getJson($this->url(['status' => 'all', 'q' => 'SYNTHETIC PAGE', 'page' => 2]))->assertOk()->assertJsonCount(1, 'rows')->assertJsonPath('rows.0.row_number', 125); $this->getJson($this->url(['status' => 'all', 'observation_id' => $this->id(125), 'page' => 999]))->assertOk()->assertJsonPath('filters.page', 1)->assertJsonPath('rows.0.row_number', 125); } public function test_unassigned_users_cannot_enumerate_or_validate_private_inventory_and_invalid_filters_are_explicit(): void { $this->getJson($this->url(['status' => 'not-a-state']))->assertUnprocessable(); $this->getJson($this->url(['source_hash' => str_repeat('f', 64)]))->assertUnprocessable()->assertJsonValidationErrors('source_hash'); $this->login(User::factory()->create()); $this->getJson($this->url())->assertForbidden(); $this->getJson($this->base.'?record_id=999999999&source_hash=invalid')->assertForbidden(); $this->getJson('/minhas-oportunidades/oferta-interna?record_id=999999999')->assertForbidden(); } public function test_private_page_is_wired_and_preserves_applied_filters(): void { $this->get($this->url(['status' => 'ambiguous']))->assertOk()->assertHeader('Cache-Control', 'no-store, private') ->assertInertia(fn ($page) => $page->component('management/proactive-supply-review')->where('filters.status', 'ambiguous')->where('rows.0.id', $this->id(4))); } public function test_existing_comparison_keeps_server_side_lineage_and_rejects_repetitions(): void { $service = app(ProactiveSupply::class); $evidence = $service->evidence($this->actor, $this->id(2), null); $this->assertSame('internal', $evidence['access']); $this->assertStringContainsString($this->source, $evidence['source']); $this->assertSame('SYNTHETIC QA!linha 2', $evidence['locator']); $comparison = ['observation_id' => $this->id(2), 'relationship' => 'uncertain', 'rationale' => 'SYNTHETIC comparison', 'limitations' => 'SYNTHETIC unknown current operation']; $this->assertSame([$comparison], $service->comparisons($this->actor, [$comparison], null)); $this->expectException(ValidationException::class); $service->comparisons($this->actor, [[...$comparison, 'observation_id' => $this->id(6)]], null); } public function test_database_policy_requires_actor_context_for_sources_and_observations(): void { $role = 'supply_review_qa_'.bin2hex(random_bytes(6)); DB::statement('CREATE ROLE '.$role.' NOLOGIN'); DB::statement('GRANT USAGE ON SCHEMA public TO '.$role); DB::statement('GRANT SELECT ON proactive_supply_sources,proactive_supply_observations TO '.$role); DB::select("select set_config('app.actor_user_id','',true),set_config('app.proactive_supply_read','',true)"); try { DB::statement('SET LOCAL ROLE '.$role); $this->assertSame(0, DB::table('proactive_supply_observations')->count()); $this->assertSame(0, DB::table('proactive_supply_sources')->count()); DB::select("select set_config('app.actor_user_id',?,true),set_config('app.proactive_supply_read','internal',true)", [(string) $this->actor->id]); $this->assertSame(6, DB::table('proactive_supply_observations')->where('source_hash', $this->source)->count()); $this->assertSame(1, DB::table('proactive_supply_sources')->where('sha256', $this->source)->count()); } finally { DB::statement('RESET ROLE'); } } }