Files
2026-11-11 09:00:17 +02:00

126 lines
14 KiB
PHP

<?php
namespace App\Modules\Purchasing;
use App\Shared\Context\CompanyContext;
use Illuminate\Support\Facades\{Auth,DB,Gate,Schema};
/** Local QA composition of the original receipt workflow, never an HTTP proxy or second writer. */
final class ReceiptRuntime
{
private static array $loaded=[];
private static function loadClass(string $class,string $bundle,array $files):void
{
foreach(['Tempo\\Purchasing\\Embedded\\'=>'app/','Laravel\\Fortify\\'=>'fortify/']as$prefix=>$directory){
if(!str_starts_with($class,$prefix))continue;
$tail=substr($class,strlen($prefix));abort_unless(preg_match('/^[A-Za-z0-9_\\\\]+$/D',$tail),503);
$relative=$directory.str_replace('\\','/',$tail).'.php';$pin=$files[$relative]??null;$file=realpath($bundle.'/'.$relative);$base=realpath($bundle);
abort_unless(is_string($pin)&&preg_match('/^[a-f0-9]{64}$/D',$pin)&&$base&&$file&&str_starts_with(strtolower($file),strtolower($base.DIRECTORY_SEPARATOR))&&!is_link($bundle.'/'.$relative)&&hash_equals($pin,hash_file('sha256',$file)),503,'Dependência do pacote incompatível.');
require_once $file;return;
}
}
public function run(string $right,\Closure $work):mixed
{
abort_unless(config('receipt_qa.enabled')===true,404);
if(str_starts_with($right,'general_ledger.'))abort_unless(config('sgf_single_installation.enabled')===true&&config('sgf_single_installation.ledger_enabled')===true,404);
if(str_starts_with($right,'supplier_payables.'))abort_unless(config('sgf_single_installation.enabled')===true&&config('sgf_single_installation.finance_enabled')===true,404);
if(str_starts_with($right,'stock.')){
abort_unless(config('stock_operations.enabled')===true,404);
$allowed=['stock.view','stock.operations.prepare','stock.operations.confirm'];
if(config('stock_operations.final_enabled')===true)$allowed=[...$allowed,'stock.transfers.receive','stock.lots.review'];
if(config('stock_catalogue.enabled')===true)$allowed=[...$allowed,'stock.items.prepare','stock.items.review'];
if(config('stock_completion.enabled')===true)$allowed=[...$allowed,'stock.planning.view','stock.planning.prepare','stock.planning.review','stock.disposition.view','stock.disposition.prepare','stock.disposition.review'];
abort_unless(in_array($right,$allowed,true),403);
}
if(str_starts_with($right,'purchasing_obligations.'))abort_unless(config('receipt_qa.obligations_enabled')===true,404);
if(str_starts_with($right,'purchasing_invoices.'))abort_unless(config('receipt_qa.invoices_enabled')===true,404);
if(str_starts_with($right,'purchasing_requests.'))abort_unless(config('receipt_qa.requests_enabled')===true,404);
if(str_starts_with($right,'purchasing_orders.'))abort_unless(config('receipt_qa.orders_enabled')===true,404);
if(str_starts_with($right,'purchasing_suppliers.'))abort_unless(config('receipt_qa.suppliers_enabled')===true,404);
if(str_starts_with($right,'purchasing_sourcing.'))abort_unless(config('receipt_qa.sourcing_enabled')===true,404);
abort_unless(app()->environment('budget-testing'),403);
BudgetAdapter::guard();
$host=DB::connection();$db=$host->getDatabaseName();$role=$host->getConfig('username');
abort_unless(preg_match('/^budget_core_sgf_([a-f0-9]{12})$/D',$db,$m)&&$role==='ir_composition_'.$m[1],403);
$run=(string)getenv('SGF_ISOLATED_REGRESSION_RUN');
$hostConfig=$host->getConfig();abort_unless(preg_match('/^[a-f0-9]{12}$/D',$run)&&($hostConfig['host']??null)==='127.0.0.1'&&(int)($hostConfig['port']??0)===55439&&empty($hostConfig['read'])&&empty($hostConfig['write'])&&($hostConfig['search_path']??'public')==='public',403);
$identity=$host->selectOne("SELECT 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->role===$role&&$identity->cluster==='sgf_modular_regression_'.$run,403);
$expected='C:/laragon/data/sgf-tempo/run/modular-regression-'.$run.'/private/receipt-composition-'.$m[1];
$profile=realpath((string)config('receipt_qa.profile'));
abort_unless($profile&&strtolower(str_replace('\\','/',$profile))===strtolower($expected.'/binding.json'),403);
$p=json_decode(file_get_contents($profile),true,64,JSON_THROW_ON_ERROR);
$ctx=app(CompanyContext::class);$scope=$ctx->identifiers()+['unit_id'=>$ctx->unitId()];$user=$ctx->userId();
abort_unless($scope['unit_id']&&request()->user()->id===$user,403);
$key=implode(':',array_values($scope)).':'.$user;$binding=$p['bindings'][$key]??null;
if(config('sgf_single_installation.enabled')===true&&config('sgf_single_installation.bindings_sha256')!==null){
$file=$expected.'/single-bindings.json';$pin=config('sgf_single_installation.bindings_sha256');
abort_unless(is_string($pin)&&preg_match('/^[a-f0-9]{64}$/D',$pin)&&is_file($file)&&!is_link($file)&&hash_equals($pin,hash_file('sha256',$file)),503);
$additional=json_decode(file_get_contents($file),true,32,JSON_THROW_ON_ERROR);
abort_unless(($additional['composition']??null)===$m[1],403);
if(isset($additional['bindings'][$key]))$binding=$additional['bindings'][$key];
}
abort_unless($binding,403,'Origem de recepção não autorizada neste contexto.');
// A Stock-specific map is explicit and pinned; it cannot change purchasing bindings.
if(str_starts_with($right,'stock.')&&config('stock_operations.profile_sha256')!==null){
$mapFile=$expected.'/stock-operations-binding.json';$pin=config('stock_operations.profile_sha256');
abort_unless(is_string($pin)&&preg_match('/^[a-f0-9]{64}$/D',$pin)&&is_file($mapFile)&&!is_link($mapFile)&&hash_equals($pin,hash_file('sha256',$mapFile)),503);
$map=json_decode(file_get_contents($mapFile),true,32,JSON_THROW_ON_ERROR);$warehouses=$map['warehouse_bindings'][$key]??null;
abort_unless(($map['composition']??null)===$m[1]&&is_array($warehouses)&&count($warehouses)>0&&count($warehouses)<=1000&&!array_filter($warehouses,fn($id)=>!is_int($id)||$id<1),403);
$binding['warehouse_ids']=$warehouses;$binding['stock_profile_sha256']=$pin;
}
if(config('receipt_qa.obligations_enabled')===true)abort_unless(($binding['payable_instance']??null)==='qa-stock-ledger-'.$m[1]&&config('common-payables.source_instance')===$binding['payable_instance']&&config('common-payables.history_enabled')===false&&config('common-payables.history_decisions_enabled')===false,403);
$p['context_key']=$key;
$bundle=realpath($expected.'/bundle');$manifest=$bundle.'/manifest.json';
abort_unless($bundle&&hash_equals($p['manifest_sha256'],hash_file('sha256',$manifest)),503);
$files=json_decode(file_get_contents($manifest),true,64,JSON_THROW_ON_ERROR)['files'];
foreach($files as$file=>$pin)abort_unless(preg_match('~^(app|fortify)/[A-Za-z0-9_/]+\.php$~D',$file)&&!str_contains($file,'..')&&is_string($pin)&&preg_match('/^[a-f0-9]{64}$/D',$pin),503);
if(config('receipt_qa.orders_enabled')===true||config('receipt_qa.sourcing_enabled')===true){
$economic=$expected.'/economic';abort_unless(isset($p['economic_manifest_sha256'])&&is_file($economic.'/manifest.json')&&hash_equals($p['economic_manifest_sha256'],hash_file('sha256',$economic.'/manifest.json')),503);
$economicFiles=json_decode(file_get_contents($economic.'/manifest.json'),true,32,JSON_THROW_ON_ERROR)['files'];$names=array_keys($economicFiles);sort($names);abort_unless($names===['ProposalScoring.php','QuotationPricing.php'],503);
foreach($economicFiles as$file=>$pin){abort_unless(is_file($economic.'/'.$file)&&!is_link($economic.'/'.$file)&&hash_equals($pin,hash_file('sha256',$economic.'/'.$file)),503);$class='Tempo\\Purchasing\\Economic\\'.basename($file,'.php');if(class_exists($class,false)){abort_unless(hash_equals($pin,hash_file('sha256',(new \ReflectionClass($class))->getFileName())),503);}else require_once $economic.'/'.$file;}
}
if(config('receipt_qa.requests_enabled')===true){
$internal=$expected.'/internal';abort_unless(isset($p['internal_manifest_sha256'])&&hash_equals($p['internal_manifest_sha256'],hash_file('sha256',$internal.'/manifest.json')),503);$pins=json_decode(file_get_contents($internal.'/manifest.json'),true,32,JSON_THROW_ON_ERROR)['files'];$names=array_keys($pins);sort($names);abort_unless($names===['InternalRequisitions.php','RequisitionFailure.php','RequisitionPort.php'],503);foreach(['RequisitionPort.php','RequisitionFailure.php','InternalRequisitions.php']as$file){$pin=$pins[$file];abort_unless(preg_match('/^[a-f0-9]{64}$/D',$pin)&&!is_link($internal.'/'.$file)&&hash_equals($pin,hash_file('sha256',$internal.'/'.$file)),503);$class='Tempo\\Purchasing\\Internal\\'.basename($file,'.php');if(class_exists($class,false)||interface_exists($class,false)){abort_unless(hash_equals($pin,hash_file('sha256',(new \ReflectionClass($class))->getFileName())),503);}else require_once $internal.'/'.$file;}
}
if(!isset(self::$loaded[$bundle])){
abort_unless(self::$loaded===[],503,'Uma só versão do workflow por processo.');
spl_autoload_register(fn($class)=>self::loadClass($class,$bundle,$files));
Gate::policy(\Tempo\Purchasing\Embedded\Models\GoodsReceipt::class,\Tempo\Purchasing\Embedded\Policies\GoodsReceiptPolicy::class);Gate::policy(\Tempo\Purchasing\Embedded\Models\Supplier::class,\Tempo\Purchasing\Embedded\Policies\SupplierPolicy::class);self::$loaded[$bundle]=true;
app()->singleton(\Tempo\Purchasing\Embedded\Services\ReplenishmentRun::class);
}
$connection=$p['connection'];abort_unless(($connection['driver']??null)==='pgsql'&&($connection['host']??null)==='127.0.0.1'&&(int)($connection['port']??0)===55439
&&($connection['database']??null)==='compras_receipts_test_'.$m[1]&&($connection['username']??null)==='payable_history_native_'.$m[1]
&&empty($connection['url'])&&empty($connection['read'])&&empty($connection['write'])&&($connection['search_path']??null)==='public',403);
config(['database.connections.receipt_native'=>$connection]);$native=DB::connection('receipt_native');
$i=$native->selectOne("SELECT current_user role,current_database() db,current_setting('cluster_name') cluster,rolsuper OR rolcreatedb OR rolcreaterole OR rolreplication OR rolbypassrls elevated FROM pg_roles WHERE rolname=current_user");
abort_unless(!$i->elevated&&$i->role===$connection['username']&&$i->db===$connection['database']&&$i->cluster==='sgf_modular_regression_'.$run,403);
return $host->transaction(function()use($host,$scope,$user,$right,$binding,$work,$p,$native){
if(str_starts_with($right,'stock.')){$host->statement("SET LOCAL lock_timeout='5s'");$host->statement("SET LOCAL statement_timeout='15s'");}
// Hold existing authority until the native transaction ends; no host business writes.
abort_unless($host->table('users')->where('id',$user)->lockForUpdate()->first(),403);
abort_unless($host->table('company_memberships')->where(['user_id'=>$user,'tenant_id'=>$scope['tenant_id'],'company_id'=>$scope['company_id']])->lockForUpdate()->first(),403);
$view=str_starts_with($right,'purchasing_invoices.')?'purchasing_invoices.view':(str_starts_with($right,'purchasing_requests.')?'purchasing_requests.view':(str_starts_with($right,'purchasing_suppliers.')?'purchasing_suppliers.view':(str_starts_with($right,'purchasing_sourcing.')?'purchasing_sourcing.view':(str_starts_with($right,'purchasing_orders.')?'purchasing_orders.view':'purchasing_receipts.view'))));
if(str_starts_with($right,'purchasing_obligations.'))$view='purchasing_obligations.view';
if(str_starts_with($right,'stock.'))$view='stock.view';
if(str_starts_with($right,'general_ledger.'))$view='general_ledger.view';
if(str_starts_with($right,'supplier_payables.'))$view='supplier_payables.view';
foreach(['company.view'=>null,'unit.view'=>$scope['unit_id'],$view=>$scope['unit_id'],$right=>$scope['unit_id']]as$permission=>$unit)
abort_unless($host->table('permission_grants')->where(['user_id'=>$user,'tenant_id'=>$scope['tenant_id'],'company_id'=>$scope['company_id'],'unit_id'=>$unit,'permission'=>$permission])->lockForUpdate()->first(),403);
$default=DB::getDefaultConnection();$guard=Auth::getDefaultDriver();$environment=app()->environment();
config(['auth.guards.receipt_native'=>['driver'=>'session','provider'=>'receipt_native'],'auth.providers.receipt_native'=>['driver'=>'eloquent','model'=>\Tempo\Purchasing\Embedded\Models\User::class]]);
DB::setDefaultConnection('receipt_native');Schema::clearResolvedInstance('db.schema');Auth::shouldUse('receipt_native');app()->instance('env','testing');
try{
$actor=\Tempo\Purchasing\Embedded\Models\User::where('tenant_id',$binding['tenant_id'])->findOrFail($binding['actor_id']);Auth::guard('receipt_native')->setUser($actor);
return $native->transaction(function()use($native,$actor,$binding,$work,$p,$right){
if(str_starts_with($right,'stock.')){$native->statement("SET LOCAL lock_timeout='5s'");$native->statement("SET LOCAL statement_timeout='15s'");}
$native->table('tenants')->where('id',$actor->tenant_id)->lockForUpdate()->firstOrFail();
$version=[$p['context_key'],$binding,$p['manifest_sha256']];$origin=['sgf_context_key'=>$p['context_key'],'workflow_manifest'=>$p['manifest_sha256']];
if(isset($p['internal_manifest_sha256'])){$version[]=$p['internal_manifest_sha256'];$origin['internal_manifest']=$p['internal_manifest_sha256'];}
if(isset($p['economic_manifest_sha256'])){$version[]=$p['economic_manifest_sha256'];$origin['economic_manifest']=$p['economic_manifest_sha256'];}
return $work($actor,$binding,hash('sha256',json_encode($version,JSON_THROW_ON_ERROR)),$origin);
});
}finally{Auth::forgetGuards();Auth::shouldUse($guard);DB::setDefaultConnection($default);Schema::clearResolvedInstance('db.schema');app()->instance('env',$environment);}
});
}
}