environment('payable-testing'), 403, 'Obrigações apenas no ensaio isolado.'); if ($c->getDriverName() !== 'pgsql' || ! preg_match('/^payable_sgf_[a-f0-9]{12}$/D', $c->getDatabaseName()) || $c->getConfig('host') !== '127.0.0.1' || $c->selectOne('select current_database() as d')->d !== $c->getDatabaseName()) { throw new \LogicException('PAYABLE_QA_DATABASE_REFUSED'); } } public function scope(User $u): array { self::guard(); $c = app(CompanyContext::class); abort_unless(auth()->id() === $u->id && $c->userId() === $u->id && $c->unitId(), 403); $ids = $c->identifiers(); app(Permissions::class)->require($u->id, $ids['tenant_id'], $ids['company_id'], 'supplier_payables.view', $c->unitId()); return $c->identifiers() + ['unit_id' => $c->unitId()]; } public function can(User $u, string $a): bool { return DB::table('pa_access')->where($this->scope($u))->where(['user_id' => $u->id, 'action' => $a])->exists(); } public function document(User $u, int $id): array { $row = DB::table('pa_imported_sources')->where($this->scope($u))->where('id', $id)->first() ?? abort(403, 'Origem não mapeada neste contexto.'); return $this->receiptDocument($row); } private function receiptDocument(object $row): array { $s = json_decode($row->payload, true, 64, JSON_THROW_ON_ERROR); abort_unless(hash_equals($row->receipt_hash, hash('sha256', Payables::canonical($s))), 409, 'Recibo de origem divergente.'); $s['local_id'] = (int) $row->id; return $s; } public function engine(User $u): Payables { return $this->buildEngine($u); } private function buildEngine(User $u, ?array $documents = null, ?array $histories = null): Payables { $scope = $this->scope($u); $person = DB::table('pa_access')->where($scope)->where(['user_id' => $u->id, 'action' => 'view'])->value('person'); abort_unless($person, 403); $authorize = function (string $a, bool $lock = true) use ($u, $scope, $person) { self::guard(); if ($lock) { DB::table('companies')->where('tenant_id', $scope['tenant_id'])->where('id', $scope['company_id'])->lockForUpdate()->firstOrFail(); DB::table('users')->where('id', $u->id)->lockForUpdate()->firstOrFail(); } abort_unless(DB::table('pa_access')->where($scope)->where(['user_id' => $u->id, 'action' => 'view'])->value('person') === $person, 409, 'Identidade de pessoa alterada; reabra a proposta.'); abort_unless($this->scope($u) === $scope, 409, 'Contexto mudou.'); app(CompanyContext::class)->selectFor($u->id, $scope['tenant_id'], $scope['company_id'], $scope['unit_id']); abort_unless($this->can($u, 'view') && $this->can($u, $a), 403); }; $source = $documents === null ? fn (int $id) => $this->document($u, $id) : fn (int $id) => $documents[$id] ?? abort(403); $history = $histories === null ? fn (int $id) => app(SupplierHistoryReceipts::class)->payments($u, $id) : function (int $id) use ($histories) { $result = $histories[$id] ?? new \DomainException('Histórico não reconciliado nesta página.'); if ($result instanceof \DomainException) { throw $result; } return $result; }; return new Payables(DB::connection(), $u->id, $person, $scope, fn (string $a) => $authorize($a), $source, app(SupplierHistoryReceipts::class)->available() ? $history : null, fn () => $authorize('view', false)); } public function listing(User $u, array $options = []): array { $own = DB::transactionLevel() === 0; return DB::transaction(function () use ($u, $options, $own) { if ($own) { DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY'); } $scope = $this->scope($u); abort_unless($this->can($u, 'view'), 403); $page = (int) ($options['page'] ?? 1); $perPage = (int) ($options['per_page'] ?? 25); $q = trim((string) ($options['q'] ?? '')); $recognition = $options['recognition'] ?? 'all'; abort_unless($page >= 1 && $page <= 100000 && in_array($perPage, [25, 100], true) && mb_strlen($q) <= 100 && in_array($recognition, ['all', 'recognised', 'unrecognised'], true), 422); $query = DB::table('pa_imported_sources AS r')->where($scope); if ($q !== '') { $needle = '%'.str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $q).'%'; $query->where(fn ($w) => $w->whereRaw("payload->>'number' ILIKE ?", [$needle])->orWhereRaw("payload->>'supplier_name' ILIKE ?", [$needle])); } if ($recognition !== 'all') { $method = $recognition === 'recognised' ? 'whereExists' : 'whereNotExists'; $query->$method(fn ($w) => $w->selectRaw('1')->from('pa_sources AS s')->join('pa_obligations AS o', 'o.source_id', '=', 's.id') ->whereColumn('s.local_source_id', 'r.id')->whereColumn('s.tenant_id', 'r.tenant_id')->whereColumn('s.company_id', 'r.company_id')->whereColumn('s.unit_id', 'r.unit_id') ->whereColumn('o.tenant_id', 'r.tenant_id')->whereColumn('o.company_id', 'r.company_id')->whereColumn('o.unit_id', 'r.unit_id')); } $pager = $query->orderByDesc('r.id')->paginate($perPage, ['r.*'], 'page', $page); $documents = []; foreach ($pager->items() as $row) { $documents[(int) $row->id] = $this->receiptDocument($row); } $provider = app(SupplierHistoryReceipts::class); $histories = $provider->available() ? $provider->page($u, $documents) : []; $d = $this->buildEngine($u, $documents, $histories)->listingPage(array_keys($documents), (int) ($options['proposal_page'] ?? 1), (int) ($options['event_page'] ?? 1), $perPage); $d['pagination']['documents'] = ['page' => $pager->currentPage(), 'last_page' => $pager->lastPage(), 'total' => $pager->total(), 'per_page' => $pager->perPage()]; $d['listing_query'] = ['page' => $page, 'per_page' => $perPage, 'q' => $q, 'recognition' => $recognition, 'proposal_page' => (int) ($options['proposal_page'] ?? 1), 'event_page' => (int) ($options['event_page'] ?? 1)]; $d['permissions'] = ['prepare' => $this->can($u, 'prepare'), 'validate' => $this->can($u, 'validate')]; $d['history_import_available'] = app(SupplierHistoryReceipts::class)->available(); $d['history_decisions_available'] = false; if ($d['history_import_available']) { $d['history_receipt_notice'] = 'Recibo histórico de ensaio: a origem é uma fotografia conferida. Não há leitura histórica em tempo real nem passagem operacional de responsabilidade validada.'; } $d['payments_available'] = app()->environment('payable-testing') && DB::connection()->getSchemaBuilder()->hasTable('ps_payments') && $this->can($u, 'payments_view'); return $d; }); } }