true]); $u = User::findOrFail($f['actors']['read']); auth()->setUser($u); app(CompanyContext::class)->selectFor($u->id, $scope['tenant_id'], $scope['company_id'], $scope['unit_id']); $service = app(SupplierPayables::class); $fingerprint = function (): array { $result = []; foreach (['pa_imported_sources', 'pa_source_histories', 'pa_sources', 'pa_proposals', 'pa_obligations', 'pa_history_imports', 'pa_imported_payments', 'pa_events', 'pa_access'] as $table) { $result[$table] = DB::selectOne('SELECT count(*) AS n, md5(COALESCE(string_agg(row_to_json(t)::text, chr(10) ORDER BY t.id), \'\')) AS hash FROM '.$table.' t'); } return json_decode(json_encode($result, JSON_THROW_ON_ERROR), true, 64, JSON_THROW_ON_ERROR); }; $before = $fingerprint(); $cases = []; $checks = 0; $assert = function (bool $value, string $message) use (&$checks): void { $checks++; if (! $value) throw new RuntimeException($message); }; $refused = function (callable $action, int $status) use ($assert): void { try { $action(); } catch (HttpExceptionInterface $e) { $assert($e->getStatusCode() === $status, 'Unexpected refusal status'); return; } throw new RuntimeException('Expected refusal absent'); }; $case = function (string $name, callable $test) use (&$cases, &$checks): void { $start = $checks; DB::beginTransaction(); try { $test(); $cases[] = ['name' => $name, 'assertions' => $checks - $start, 'passed' => true]; } finally { while (DB::transactionLevel() > 0) DB::rollBack(); } echo 'Passed focused evidence case: '.$name."\n"; }; $case('original-source-outside-first-page-has-own-evidence', function () use ($service, $u, $f, $scope, $assert): void { $first = $service->listing($u); $assert(! in_array($f['source_id'], array_column($first['documents'], 'local_id'), true), 'Original must be outside first page'); $data = $service->listing($u, ['source' => $f['source_id']]); $assert(count($data['documents']) === 1 && $data['documents'][0]['local_id'] === $f['source_id'], 'Exactly requested local source'); $assert($data['pagination']['documents']['total'] === 1 && $data['listing_query']['source'] === $f['source_id'], 'Focused count and query'); $assert(count($data['obligations']) === 1 && $data['obligations'][0]['balance_minor'] === 3000, 'Original balance retained'); $source = DB::table('pa_sources')->where($scope)->where('local_source_id', $f['source_id'])->value('id'); $assert($data['proposals']->every(fn ($p) => $p->source_id === $source) && $data['events']->every(fn ($e) => $e->source_id === $source), 'Only selected source evidence'); $assert($data['pagination']['events']['total'] === DB::table('pa_events')->where($scope)->where('source_id', $source)->count(), 'All own events counted'); }); $case('focus-survives-independent-evidence-pagination', function () use ($service, $u, $f, $scope, $assert): void { $source = DB::table('pa_sources')->where($scope)->where('local_source_id', $f['source_id'])->value('id'); $proposal = (array) DB::table('pa_proposals')->where($scope)->where('source_id', $source)->orderByDesc('id')->first(); unset($proposal['id']); for ($n = 1; $n <= 55; $n++) { DB::table('pa_proposals')->insert(array_replace($proposal, ['capture_version' => $proposal['capture_version'] + $n])); DB::table('pa_events')->insert($scope + ['source_id' => $source, 'actor_id' => $u->id, 'kind' => 'qa.evidence_only', 'payload' => '{}', 'created_at' => now()]); } $a = $service->listing($u, ['source' => $f['source_id']]); $b = $service->listing($u, ['source' => $f['source_id'], 'proposal_page' => 2, 'event_page' => 2]); $assert($a['documents'] === $b['documents'] && $a['obligations'] === $b['obligations'], 'Evidence pagination leaves document and balance fixed'); $assert(! array_intersect($a['proposals']->pluck('id')->all(), $b['proposals']->pluck('id')->all()), 'Disjoint proposal pages'); $assert(! array_intersect($a['events']->pluck('id')->all(), $b['events']->pluck('id')->all()), 'Disjoint event pages'); $html = Workspace::render($a, ['host' => 'QA', 'actor' => 'QA', 'organisation' => 'QA', 'api' => '/api/supplier-payables', 'csrf' => '', 'document_filter' => 'source']); $assert(str_contains($html, 'name="source" value="'.$f['source_id'].'"'), 'Filter form retains exact source'); $links = []; preg_match_all('/href="([^"]+)"/', $html, $links); $proposalFocus = false; $eventFocus = false; foreach ($links[1] as $link) { parse_str(parse_url(html_entity_decode($link), PHP_URL_QUERY) ?? '', $query); if (($query['source'] ?? null) !== (string) $f['source_id']) continue; $proposalFocus = $proposalFocus || ($query['proposal_page'] ?? null) === '2'; $eventFocus = $eventFocus || ($query['event_page'] ?? null) === '2'; } $assert($proposalFocus && $eventFocus, 'Both evidence pagers retain exact source'); $assert(str_contains($html, 'Conferir evidências desta origem') && str_contains($html, 'Consultar todas as origens'), 'Entry and exit links'); $native = $a; unset($native['listing_query']['source']); $native['listing_query']['invoice'] = 77; $html = Workspace::render($native, ['host' => 'QA Native', 'actor' => 'QA', 'organisation' => 'QA', 'api' => '/api/supplier-payables', 'csrf' => '', 'page_url' => '/obrigacoes-fornecedores']); $assert(str_contains($html, 'name="invoice" value="77"') && ! str_contains($html, 'Conferir evidências desta origem'), 'Native invoice focus preserved without SGF source links'); }); $case('combined-filters-remain-intersections', function () use ($service, $u, $f, $assert): void { foreach ([['q' => 'NO-SUCH-FOCUSED-NUMBER'], ['recognition' => 'unrecognised'], ['page' => 2]] as $filter) { $data = $service->listing($u, ['source' => $f['source_id']] + $filter); $assert(! $data['documents'] && ! $data['obligations'] && $data['events']->isEmpty() && $data['proposals']->isEmpty(), 'Filtered-out focus must not expose other evidence'); $assert($data['listing_query']['source'] === $f['source_id'], 'Empty focus retained'); } }); $case('foreign-source-identities-and-invalid-input-refused', function () use ($service, $u, $f, $scope, $refused, $assert): void { $original = (array) DB::table('pa_imported_sources')->where('id', $f['source_id'])->first(); unset($original['id']); foreach (['tenant_id', 'company_id', 'unit_id'] as $field) { $foreign = DB::table('pa_imported_sources')->insertGetId(array_replace($original, [$field => $scope[$field] + 100000])); $refused(fn () => $service->listing($u, ['source' => $foreign]), 403); } $refused(fn () => $service->listing($u, ['source' => PHP_INT_MAX]), 403); foreach ([0, -1, [], 'not-an-id', true, null] as $bad) $refused(fn () => $service->listing($u, ['source' => $bad]), 422); DB::table('pa_access')->where($scope)->where(['user_id' => $u->id, 'action' => 'view'])->delete(); $refused(fn () => $service->listing($u, ['source' => $f['source_id']]), 403); }); $case('current-source-integrity-and-command-revalidation', function () use ($service, $u, $scope, $assert, $refused): void { $id = $service->listing($u)['documents'][0]['local_id']; $data = $service->listing($u, ['source' => $id]); $row = DB::table('pa_imported_sources')->where($scope)->where('id', $id)->first(); $doc = json_decode($row->payload, true, 64, JSON_THROW_ON_ERROR); $doc['number'] = 'EVIDENCE-CHANGED-AFTER-READ'; DB::table('pa_imported_sources')->where('id', $id)->update(['payload' => Payables::canonical($doc)]); $refused(fn () => $service->listing($u, ['source' => $id]), 409); DB::table('pa_imported_sources')->where('id', $id)->update(['receipt_hash' => hash('sha256', Payables::canonical($doc))]); $current = $service->listing($u, ['source' => $id]); $assert($current['obligations'][0]['balance_minor'] === null && $current['obligations'][0]['source_conflict'], 'Changed historical anchor leaves balance indeterminate'); DB::table('pa_access')->insert($scope + ['user_id' => $u->id, 'action' => 'prepare', 'person' => 'sgf-history-person-'.$u->id]); try { $service->engine($u)->command('prepare_history', $id, 'Focused source changed after read.', (string) Str::uuid()); } catch (DomainException $e) { $assert(str_contains($e->getMessage(), 'Recibo histórico antigo'), 'Command revalidates current history'); return; } throw new RuntimeException('Command incorrectly reused focused snapshot'); }); $after = $fingerprint(); $assert($before === $after, 'Financial rows changed after rollback-only tests'); $report = ['increment' => 'SGF-PAYABLE-EVIDENCE-FOCUS-SIM-v1', 'passed' => true, 'cases' => $cases, 'assertions' => $checks, 'financial_rows_preserved' => true, 'rollback_only_test_rows' => true, 'before_after_fingerprint' => hash('sha256', Payables::canonical($before)), 'visual_review' => false]; file_put_contents($s['root'].'/evidence-cases.json', json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); echo json_encode(['cases' => count($cases), 'assertions' => $checks, 'passed' => true])."\n";