89 lines
8.3 KiB
PHP
89 lines
8.3 KiB
PHP
<?php
|
|
namespace App\Modules\Inventory;
|
|
|
|
use App\Modules\Purchasing\BudgetAdapter;
|
|
use App\Modules\Identity\Services\Permissions;
|
|
use App\Shared\Context\CompanyContext;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
require_once __DIR__.'/../../../packages/inventory-read/bootstrap.php';
|
|
|
|
/** Observe the composed receipt book; never borrow its writer or tenant mutex. */
|
|
final class CompositionStockRead
|
|
{
|
|
public function index(Request $r, CompanyContext $context, Permissions $permissions)
|
|
{
|
|
abort_unless(config('receipt_qa.stock_read_enabled') === true && config('receipt_qa.enabled') === true,404);
|
|
abort_unless(app()->environment('budget-testing'),403);
|
|
BudgetAdapter::guard();
|
|
$d=$r->validate(['view'=>'sometimes|in:items,warehouses,balances,movements,period','search'=>'nullable|string|max:100',
|
|
'warehouse_id'=>'sometimes|integer|min:1','page'=>'sometimes|integer|min:1|max:100000',
|
|
'per_page'=>'sometimes|integer|in:25,100','snapshot'=>'sometimes|string|max:8192',
|
|
'from'=>'required_if:view,period|prohibited_unless:view,period|date_format:Y-m-d|after_or_equal:1900-01-01|before_or_equal:9998-12-31',
|
|
'to'=>'required_if:view,period|prohibited_unless:view,period|date_format:Y-m-d|after_or_equal:from|before_or_equal:9998-12-31']);
|
|
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)];
|
|
if($filters['view']==='period'){
|
|
abort_unless(config('receipt_qa.stock_period_enabled')===true,404);
|
|
$filters+=['from'=>$d['from'],'to'=>$d['to']];
|
|
}else abort_if(array_key_exists('from',$d)||array_key_exists('to',$d),422);
|
|
$host=DB::connection();$c=$host->getConfig();$run=(string)getenv('SGF_ISOLATED_REGRESSION_RUN');
|
|
abort_unless(preg_match('/^budget_core_sgf_([a-f0-9]{12})$/D',$host->getDatabaseName(),$m)
|
|
&&($c['username']??null)==='ir_composition_'.$m[1]&&preg_match('/^[a-f0-9]{12}$/D',$run)
|
|
&&($c['host']??null)==='127.0.0.1'&&(int)($c['port']??0)===55439&&empty($c['url'])&&empty($c['read'])&&empty($c['write'])&&($c['search_path']??'public')==='public',403);
|
|
$identity=$host->selectOne("SELECT current_database() db,current_user role,current_setting('cluster_name') cluster,rolsuper OR rolcreatedb OR rolcreaterole OR rolreplication OR rolbypassrls elevated FROM pg_roles WHERE rolname=current_user");
|
|
abort_unless(!$identity->elevated&&$identity->db===$host->getDatabaseName()&&$identity->role===$c['username']&&$identity->cluster==='sgf_modular_regression_'.$run,403);
|
|
$scope=$context->identifiers();$unit=$context->unitId();$user=$context->userId();
|
|
abort_unless($unit&&$r->user()&&(int)$r->user()->id===$user,403);
|
|
// Hold only shared host authority locks. Unrelated readers remain concurrent.
|
|
try {
|
|
return $host->transaction(function()use($host,$scope,$unit,$user,$permissions,$m,$run,$r,$filters,$d){
|
|
$host->statement("SET LOCAL lock_timeout='2s'");
|
|
$host->statement("SET LOCAL statement_timeout='3s'");
|
|
abort_unless($host->table('users')->where('id',$user)->sharedLock()->first(),403);
|
|
abort_unless($host->table('company_memberships')->where($scope+['user_id'=>$user])->sharedLock()->first(),403);
|
|
foreach(['company.view'=>null,'unit.view'=>$unit,'stock.view'=>$unit]as$right=>$u){
|
|
abort_unless($host->table('permission_grants')->where($scope+['user_id'=>$user,'unit_id'=>$u,'permission'=>$right])->sharedLock()->first(),403);
|
|
$permissions->require($user,$scope['tenant_id'],$scope['company_id'],$right,$u);
|
|
}
|
|
$root='C:/laragon/data/sgf-tempo/run/modular-regression-'.$run.'/private/receipt-composition-'.$m[1];
|
|
$profile=realpath($root.'/stock-binding.json');
|
|
abort_unless($profile&&!is_link($root.'/stock-binding.json')&&strtolower(str_replace('\\','/',$profile))===strtolower($root.'/stock-binding.json'),503,'Fonte de stock indisponível.');
|
|
$p=json_decode(file_get_contents($profile),true,32,JSON_THROW_ON_ERROR);
|
|
$key=implode(':',[$scope['tenant_id'],$scope['company_id'],$unit,$user]);
|
|
$binding=$p['bindings'][$key]??null;abort_unless(is_array($binding),403);
|
|
abort_unless(is_int($binding['source_tenant_id']??null)&&$binding['source_tenant_id']>0
|
|
&&is_array($binding['warehouse_ids']??null)&&count($binding['warehouse_ids'])>0&&count($binding['warehouse_ids'])<=1000
|
|
&&!array_filter($binding['warehouse_ids'],fn($id)=>!is_int($id)||$id<1),503,'Fonte de stock incompatível.');
|
|
$c=$p['connection']??[];
|
|
abort_unless(($c['driver']??null)==='pgsql'&&($c['host']??null)==='127.0.0.1'&&(int)($c['port']??0)===55439
|
|
&&($c['database']??null)==='compras_receipts_test_'.$m[1]&&($c['username']??null)==='stock_receipt_read_'.$m[1]
|
|
&&empty($c['url'])&&empty($c['read'])&&empty($c['write'])&&($c['search_path']??null)==='public',403);
|
|
// Purge avoids retaining an earlier profile/credential after a configuration change.
|
|
DB::purge('composition_stock');config(['database.connections.composition_stock'=>$c]);$source=DB::connection('composition_stock');
|
|
$i=$source->selectOne("SELECT current_database() db,current_user role,current_setting('cluster_name') cluster,current_setting('default_transaction_read_only') read_only,rolsuper OR rolcreatedb OR rolcreaterole OR rolreplication OR rolbypassrls elevated FROM pg_roles WHERE rolname=current_user");
|
|
abort_unless(!$i->elevated&&$i->db===$c['database']&&$i->role===$c['username']&&$i->cluster==='sgf_modular_regression_'.$run&&$i->read_only==='on',403);
|
|
$rights=$source->selectOne("SELECT EXISTS(SELECT FROM pg_tables WHERE schemaname='public' AND (has_table_privilege(current_user,quote_ident(schemaname)||'.'||quote_ident(tablename),'INSERT,UPDATE,DELETE,TRUNCATE,REFERENCES,TRIGGER') OR (tablename NOT IN ('stock_items','warehouses','stock_movements') AND has_table_privilege(current_user,quote_ident(schemaname)||'.'||quote_ident(tablename),'SELECT')))) forbidden,
|
|
EXISTS(SELECT FROM pg_trigger WHERE tgrelid='public.stock_movements'::regclass AND tgname='composition_stock_append_only' AND tgenabled='O' AND NOT tgisinternal AND (tgtype::int & 26)=26) append_only");
|
|
abort_if($rights->forbidden,403);abort_unless($rights->append_only,503,'Fonte de stock incompatível.');
|
|
abort_unless(($binding['schema']??null)==='public'&&($binding['append_only']??false)===true,503);
|
|
if($filters['warehouse_id']!==null)abort_unless(in_array($filters['warehouse_id'],$binding['warehouse_ids'],true),403);
|
|
$binding['physical_source']=[$i->db,$i->role,$i->cluster];
|
|
$fingerprint=hash('sha256',json_encode([$scope,$unit,$user,$r->session()->get('company_context.token'),$binding,array_diff_key($filters,['page'=>true])],JSON_THROW_ON_ERROR));
|
|
$result=$filters['view']==='period'
|
|
?(new StockPeriodReport($source))->read($binding,$fingerprint,$filters,$d['snapshot']??null)
|
|
:(new \Tempo\Inventory\InventoryReadSession($source))->read($binding,$fingerprint,$filters,$d['snapshot']??null);
|
|
$result['capabilities']['receipt_trace']=config('receipt_qa.stock_trace_enabled')===true
|
|
&&$permissions->has($user,$scope['tenant_id'],$scope['company_id'],'purchasing_receipts.view',$unit);
|
|
return response()->json($result)->header('Cache-Control','no-store');
|
|
});
|
|
}catch(\PDOException $e){
|
|
$state=$e->errorInfo[0]??(string)$e->getCode();
|
|
if(!str_starts_with($state,'08')&&!in_array($state,['28P01','3D000','42P01','42703','42883','55P03','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');
|
|
}
|
|
}
|
|
}
|