true]); $reader = User::findOrFail($f['actors']['read']); $service = app(SupplierPayables::class); $enter = function (User $user) use ($scope): void { auth()->setUser($user); app(CompanyContext::class)->selectFor($user->id, $scope['tenant_id'], $scope['company_id'], $scope['unit_id']); }; $checks = 0; $assert = function (bool $ok, string $message) use (&$checks): void { $checks++; if (! $ok) throw new RuntimeException($message); }; $request = function (array $query) use ($reader): Request { $r = Request::create('/api/supplier-payments?'.http_build_query($query), 'GET'); $r->headers->set('Accept', 'application/json'); $r->setUserResolver(fn () => $reader); $r->setLaravelSession(app('session')->driver()); return $r; }; if (DB::getSchemaBuilder()->hasTable('ps_payments')) throw new LogicException('Retained fixture must have no settlement schema'); DB::beginTransaction(); try { SettlementSchema::install(); foreach (['payments_view', 'payment_prepare'] as $action) DB::table('pa_access')->insert($scope + ['user_id' => $reader->id, 'action' => $action, 'person' => 'sgf-history-person-'.$reader->id]); $enter($reader); $base = $service->document($reader, $f['source_id']); unset($base['local_id']); $ids = []; foreach (['A', 'B'] as $n => $label) { $doc = array_replace($base, ['document' => 900001 + $n, 'number' => 'SIM-FOCUSED-ELIGIBLE-'.$label, 'payment_count' => 0]); $payload = Payables::canonical($doc); $id = DB::table('pa_imported_sources')->insertGetId($scope + ['payload' => $payload, 'receipt_hash' => hash('sha256', $payload)]); $enter(User::findOrFail($f['actors']['prepare'])); $proposal = $service->engine(auth()->user())->command('prepare', $id, 'Explicit nonhistorical synthetic source.', (string) Str::uuid()); $enter(User::findOrFail($f['actors']['validate'])); $result = $service->engine(auth()->user())->command('validate', $proposal['proposal_id'], 'Independent synthetic recognition.', (string) Str::uuid()); $assert($result['state'] === 'recognised', 'Normal synthetic obligation recognised through common engine'); $ids[] = $id; } $enter($reader); $controller = app(SupplierPaymentsController::class); $a = $controller->index($request([]))->getData(true); $b = $controller->index($request(['page' => 2]))->getData(true); $assert(count($a['obligations']) === 25 && count($b['obligations']) === 25, 'Payment obligation windows bounded'); $assert(! array_intersect(array_column($a['obligations'], 'id'), array_column($b['obligations'], 'id')), 'Second obligation page accessible and disjoint'); $assert($a['obligation_pagination']['total'] === 10002, 'Count reflects server document filters'); $focused = $controller->index($request(['source' => $ids[0]]))->getData(true); $assert(count($focused['obligations']) === 1 && $focused['obligations'][0]['balance_minor'] === 6000 && ! $focused['obligations'][0]['history_imported'], 'Focused eligible debt retains common balance'); $assert($focused['obligations'][0]['evidence_url'] === '/supplier-payables?source='.$ids[0], 'Exact source evidence link'); $origin = $focused['obligations'][0]['origin_snapshot']; $assert($origin['number'] === 'SIM-FOCUSED-ELIGIBLE-A', 'Exact recognition proposal snapshot'); $source = $focused['obligations'][0]['source_id']; $proposal = (array) DB::table('pa_proposals')->where($scope)->where('source_id', $source)->first(); unset($proposal['id']); for ($n = 1; $n <= 40; $n++) { $new = json_decode($proposal['snapshot'], true, 64, JSON_THROW_ON_ERROR); $new['supplier_name'] = 'LATER PROPOSAL MUST NOT REPLACE RECOGNITION'; DB::table('pa_proposals')->insert(array_replace($proposal, ['capture_version' => $proposal['capture_version'] + $n, 'snapshot' => Payables::canonical($new)])); } $latest = $controller->index($request(['source' => $ids[0], 'proposal_page' => 2]))->getData(true); $assert($latest['obligations'][0]['origin_snapshot'] === $origin, 'Recognition evidence does not depend on paginated proposal window'); $html = $controller->page($request(['source' => $ids[0]]))->getContent(); $assert(str_contains($html, 'name="source" value="'.$ids[0].'"') && str_contains($html, 'href="/supplier-payables?source='.$ids[0].'"'), 'Rendered selection and return retain focus'); $assert(str_contains($html, 'Conferir origem, propostas e histórico') && str_contains($html, 'SIM-FOCUSED-ELIGIBLE-A'), 'Evidence link and selected source visible'); $page = $controller->page($request([]))->getContent(); $assert(str_contains($page, 'Paginação de origens para pagamentos') && str_contains($page, 'page=2'), 'Rendered next-page navigation'); $payable = $service->listing($reader, ['source' => $ids[0]]); $html = Workspace::render($payable, ['host' => 'QA', 'actor' => 'QA', 'organisation' => 'QA', 'api' => '/api/supplier-payables', 'document_filter' => 'source', 'csrf' => '']); $assert(str_contains($html, 'href="/supplier-payments?source='.$ids[0].'"'), 'Obligation workspace carries focus into payment workspace'); $invalid = false; try { $controller->index($request(['company_id' => 999])); } catch (Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e) { $invalid = $e->getStatusCode() === 422; } $assert($invalid, 'Payment workspace refuses injected scope'); $foreign = false; try { $controller->index($request(['source' => PHP_INT_MAX])); } catch (Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e) { $foreign = $e->getStatusCode() === 403; } $assert($foreign, 'Payment workspace cannot bypass source isolation'); $assert(DB::table('ps_payments')->count() === 0 && DB::table('ps_allocations')->count() === 0, 'Navigation creates no payment or allocation'); DB::table('pa_access')->where($scope)->where(['user_id' => $reader->id, 'action' => 'payments_view'])->delete(); $revoked = false; try { $controller->index($request(['source' => $ids[0]])); } catch (Symfony\Component\HttpKernel\Exception\HttpExceptionInterface $e) { $revoked = $e->getStatusCode() === 403; } $assert($revoked, 'Payment read right independently enforced'); } finally { while (DB::transactionLevel() > 0) DB::rollBack(); } $assert(! DB::getSchemaBuilder()->hasTable('ps_payments'), 'Temporary settlement schema rolled back'); $report = ['passed' => true, 'assertions' => $checks, 'bounded_obligation_selection' => true, 'exact_recognition_proposal' => true, 'source_focus_bidirectional' => true, 'no_payment_or_allocation' => true, 'schema_and_test_rows_rolled_back' => true, 'payment_history_pagination_implemented' => false]; file_put_contents($s['root'].'/evidence-settlements.json', json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); echo json_encode($report)."\n";