Files
SGF/app/Modules/Inventory/StockReadController.php
T
2026-11-02 20:29:34 +02:00

60 lines
4.6 KiB
PHP

<?php
namespace App\Modules\Inventory;
use App\Modules\Identity\Services\Permissions;
use App\Shared\Context\CompanyContext;
use App\Shared\Testing\TestingDatabaseGuard;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
require_once __DIR__.'/../../../packages/inventory-read/bootstrap.php';
final class StockReadController
{
public function index(Request $r, CompanyContext $context, Permissions $permissions)
{
$d = $r->validate(['view' => 'sometimes|in:items,warehouses,balances,movements','search' => 'nullable|string|max:100',
'warehouse_id' => 'sometimes|integer|min:1','page' => 'sometimes|integer|min:1|max:100000','per_page' => 'sometimes|integer|min:1|max:100',
'snapshot' => 'sometimes|string|max:8192']);
abort_if(array_diff(array_keys($r->query()), array_keys($d)) !== [],422);
$filters = ['view' => $d['view'] ?? 'balances','search' => $d['search'] ?? '', 'warehouse_id' => isset($d['warehouse_id']) ? (int) $d['warehouse_id'] : null,
'page' => (int) ($d['page'] ?? 1), 'per_page' => (int) ($d['per_page'] ?? 25)];
$scope = $context->identifiers(); $unit = $context->unitId(); abort_unless($unit,403);
$permissions->require($context->userId(), $scope['tenant_id'], $scope['company_id'], 'stock.view', $unit);
$key = $scope['tenant_id'].':'.$scope['company_id'].':'.$unit;
$binding = config('stock_read.synthetic_sources.'.$key);
if (!$binding) return response()->json(['status' => 'unavailable','message' => 'Fonte autorizada de stock ainda não ligada a este contexto.'],503)->header('Cache-Control','no-store');
// A config edit cannot bind this experimental reader to a client database.
abort_unless(app()->environment('testing'),403); TestingDatabaseGuard::assertSafe();
try {
$connection=DB::connection();
if(isset($binding['connection'])) {
// QA integration may observe the actual native receipt book through
// a SELECT-only role, instead of copying movements to another book.
abort_unless($binding['connection']==='stock_receipt_source'&&($binding['schema']??null)==='public',503,'Fonte de stock incompatível.');
$connection=DB::connection('stock_receipt_source');
abort_unless($connection->getDriverName()==='pgsql'&&$connection->getConfig('host')==='127.0.0.1'&&(int)$connection->getConfig('port')===55439,403);
$i=$connection->selectOne("SELECT current_database() AS name,current_user AS role,current_setting('cluster_name') AS cluster,current_setting('default_transaction_read_only') AS read_only");
$run=(string)getenv('SGF_ISOLATED_REGRESSION_RUN');
abort_unless(preg_match('/^compras_receipts_test_([a-f0-9]{12})$/D',$i->name,$m)&&$i->role==='stock_receipt_read_'.$m[1]
&&preg_match('/^[a-f0-9]{12}$/D',$run)&&$i->cluster==='sgf_modular_regression_'.$run&&$i->read_only==='on',403);
$rights=$connection->selectOne("SELECT rolsuper OR rolcreatedb OR rolcreaterole OR rolreplication OR rolbypassrls AS elevated,
has_table_privilege(current_user,'public.stock_movements','INSERT,UPDATE,DELETE') AS writes FROM pg_roles WHERE rolname=current_user");
abort_if($rights->elevated||$rights->writes,403);
// Bind the token to the actual book too, not just the logical connection name.
$binding['physical_source']=['database'=>$i->name,'role'=>$i->role,'cluster'=>$i->cluster];
} else abort_unless(preg_match('/^sgf_stock_read_[a-f0-9]{12}$/D',$binding['schema']??''),503,'Fonte de stock incompatível.');
abort_unless(($binding['append_only']??false)===true,503,'Fonte de stock incompatível.');
if ($filters['warehouse_id'] !== null) abort_unless(in_array($filters['warehouse_id'], $binding['warehouse_ids'],true),403);
$fingerprint = hash('sha256', json_encode([$scope,$unit,$context->userId(),$binding,array_diff_key($filters,['page'=>true])],JSON_THROW_ON_ERROR));
$result = (new \Tempo\Inventory\InventoryReadSession($connection))->read($binding,$fingerprint,$filters,$d['snapshot'] ?? null);
} catch (\PDOException $e) {
$sqlstate=$e->errorInfo[0] ?? (string)$e->getCode();
if (!str_starts_with($sqlstate,'08')&&!in_array($sqlstate,['28P01','3D000','42P01','42703','42883','57014'],true)) throw $e;
return response()->json(['status'=>'unavailable','message'=>'A fonte de stock está indisponível ou incompatível. Actualize a consulta mais tarde.'],503)->header('Cache-Control','no-store');
}
return response()->json($result)->header('Cache-Control','no-store');
}
}