Files
SGF/scripts/payable-evidence-settlements.php
2026-11-04 09:37:20 +02:00

84 lines
7.2 KiB
PHP

<?php
use App\Models\User;
use App\Modules\Purchasing\{SupplierPayables, SupplierPaymentsController};
use App\Shared\Context\CompanyContext;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tempo\Finance\Payables\{Payables, SettlementSchema, Workspace};
$f = json_decode(file_get_contents($s['root'].'/fixture.json'), true, 64, JSON_THROW_ON_ERROR);
$scope = $f['scope']; config(['supplier_payable_history.synthetic_enabled' => 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";