user()->id;$generation=$this->hash($r->session()->get('company_context',[])); abort_unless($p['composition']===$physical['composition']&&$p['scope']===['tenant_id'=>1,'company_id'=>2,'unit_id'=>2],403); try{return $rt->run($right,function($a,$b,$binding)use($fn,$p,$host,$generation){ $a=app(CurrentActor::class)->lock($a);abort_if(app(OrganizationAccess::class)->maintenanceOnly($a),403); $review=$host===$p['reviewer_host_id'];abort_unless(in_array($host,[$p['dispatcher_host_id'],$p['reviewer_host_id']],true)&&$a->id===($review?$p['reviewer_native_id']:$p['dispatcher_native_id'])&&in_array($p['warehouse_id'],$b['warehouse_ids'],true),403); return $fn($a,$p,$host,$generation,$binding,$review); });}catch(\Illuminate\Database\QueryException $e){if(!in_array($e->errorInfo[0]??null,['55P03','57014'],true))throw $e;return response()->json(['message'=>'Operação ocupada. Recupere a mesma confirmação.'],503)->header('Retry-After','5');} } private function family(string $family):string{abort_unless(in_array($family,['policy','supplier_return'],true),422);return $family==='policy'?'stock.planning':'stock.disposition';} private function query(array $p,string $family){return app(SupplyPlanning::class)->latest($family==='policy'?'policy':'stock_event')->where('tenant_id',1)->where('stock_item_id',$p['stock_item_id'])->where('warehouse_id',$p['warehouse_id']);} private function basis(array $p):array { return ['policy'=>$this->hash($p),'balance'=>app(StockWorkflow::class)->balance($p['stock_item_id'],$p['warehouse_id']), 'revision'=>(int)SupplyRecord::where('tenant_id',1)->where('stock_item_id',$p['stock_item_id'])->where('warehouse_id',$p['warehouse_id'])->max('id'), 'item'=>StockItem::findOrFail($p['stock_item_id'])->only(['id','code','name','unit']), 'warehouse'=>Warehouse::findOrFail($p['warehouse_id'])->only(['id','code','name'])]; } public function screen(Request $r,ReceiptRuntime $rt){return $this->run($r,$rt,'stock.planning.view',fn()=>response()->view('stock-completion-workspace',['csrf'=>csrf_token(),'context'=>$r->session()->get('company_context.token')])->header('Cache-Control','no-store'));} public function index(Request $r,ReceiptRuntime $rt) { $d=$r->validate(['family'=>'required|in:policy,supplier_return','page'=>'sometimes|integer|min:1|max:100000','per_page'=>'sometimes|integer|in:25,100','state'=>'nullable|in:draft,returned,approved,suspended,executed','record_key'=>'sometimes|uuid']);abort_if(array_diff(array_keys($r->query()),array_keys($d)),422); return $this->run($r,$rt,$this->family($d['family']).'.view',function($a,$p,$host,$g,$binding,$review)use($d){ abort_unless($d['family']==='policy'?app(SupplyPlanning::class)->allows($a,'supply_read'):app(InventoryDispositions::class)->allows($a,'read'),403); $q=isset($d['record_key'])?SupplyRecord::where('tenant_id',1)->where('kind',$d['family']==='policy'?'policy':'stock_event')->where('stock_item_id',$p['stock_item_id'])->where('warehouse_id',$p['warehouse_id'])->where('record_key',$d['record_key']):$this->query($p,$d['family']);if(!empty($d['state']))$q->where('state',$d['state']); $page=$q->orderByDesc('id')->paginate((int)($d['per_page']??25),['*'],'page',(int)($d['page']??1)); $basis=$this->basis($p);return response()->json(['basis'=>$basis,'revision'=>$this->hash($basis),'reviewer'=>$review,'records'=>$page,'receipt_item_id'=>$p['receipt_item_id'],'capabilities'=>['prepare'=>!$review,'review'=>$review,'execute'=>!$review]])->header('Cache-Control','no-store'); }); } private function right(array $d):string{return $d['action']==='execute'?'stock.operations.confirm':$this->family($d['family']).'.'.($d['action']==='save'?'prepare':'review');} public function preview(Request $r,ReceiptRuntime $rt) { $d=$r->validate(['family'=>'required|in:policy,supplier_return','action'=>'required|in:save,approve,return,suspend,execute','operation_key'=>'required|uuid','record_key'=>'nullable|uuid','version'=>'required|integer|min:0','revision'=>'required|string|size:64','reason'=>'required|string|min:10|max:4000','data'=>'present|array']);abort_if(array_diff(array_keys($r->all()),array_keys($d)),422);$d['operation_key']=strtolower($d['operation_key']); return $this->run($r,$rt,$this->right($d),function($a,$p,$host,$g,$binding,$review)use($d){ abort_unless($d['revision']===$this->hash($this->basis($p)),409,'O estado mudou. Confira novamente.');$db=DB::connection();$level=$db->transactionLevel();$db->beginTransaction(); try{$result=$this->execute($a,$p,$d,$review);}finally{while($db->transactionLevel()>$level)$db->rollBack();} $cut=['input'=>$d,'host'=>$host,'native'=>$a->id,'generation'=>$g,'binding'=>$binding,'observed'=>time(),'impact'=>$result['impact']]; return response()->json(['preview'=>Crypt::encryptString(json_encode($cut,JSON_THROW_ON_ERROR)),'impact'=>$result['impact']])->header('Cache-Control','no-store'); }); } public function confirm(Request $r,ReceiptRuntime $rt) { $r->validate(['preview'=>'required|string|max:131072']);abort_if(array_diff(array_keys($r->all()),['preview']),422); try{$cut=json_decode(Crypt::decryptString($r->input('preview')),true,64,JSON_THROW_ON_ERROR);}catch(\Throwable){abort(409,'Prévia inválida.');} abort_unless(is_array($cut['input']??null),409); return $this->run($r,$rt,$this->right($cut['input']),function($a,$p,$host,$g,$binding,$review)use($cut){ abort_unless($cut['host']===$host&&$cut['native']===$a->id&&$cut['generation']===$g&&$cut['binding']===$binding&&$cut['observed']>=time()-600&&$cut['observed']<=time(),409,'Contexto ou prévia expirados.');$d=$cut['input'];$db=DB::connection(); $old=$db->table('audit_logs')->where(['tenant_id'=>1,'user_id'=>$a->id,'action'=>'sgf.stock.completion.command'])->where('after->operation_key',$d['operation_key'])->first(); if($old){$v=json_decode($old->after,true);abort_unless($v['request_hash']===$this->hash($d)&&$v['host_actor_id']===$host&&($v['policy']??null)===$this->hash($p),409);return response()->json($v['result']+['replayed'=>true]);} $before=$this->basis($p);abort_unless($this->hash($before)===$d['revision'],409,'Estado alterado desde a conferência.');$result=$this->execute($a,$p,$d,$review);abort_unless($result['impact']===$cut['impact'],409,'Impacto alterado.'); app(RequisitionWorkflow::class)->audit($a,'sgf.stock.completion.command',null,$before,['operation_key'=>$d['operation_key'],'request_hash'=>$this->hash($d),'host_actor_id'=>$host,'policy'=>$this->hash($p),'reason'=>$d['reason'],'result'=>$result,'after'=>$this->basis($p)]); return response()->json($result+['replayed'=>false],201)->header('Cache-Control','no-store'); }); } public function receipt(Request $r,ReceiptRuntime $rt,string $key) { abort_if($r->query()!==[],422);abort_unless(preg_match('/^[a-f0-9-]{36}$/D',$key),422); return $this->run($r,$rt,'stock.planning.view',function($a,$p,$host)use($key){ $row=DB::table('audit_logs')->where(['tenant_id'=>1,'user_id'=>$a->id,'action'=>'sgf.stock.completion.command'])->where('after->operation_key',$key)->first();abort_unless($row,404);$v=json_decode($row->after,true);abort_unless($v['host_actor_id']===$host&&$v['policy']===$this->hash($p),403); return response()->json(['result'=>$v['result'],'reason'=>$v['reason'],'created_at'=>$row->created_at,'historical_receipt_only'=>true,'operation_executed'=>false])->header('Cache-Control','no-store'); }); } private function execute(object $a,array $p,array $d,bool $review):array { $family=$d['family'];$action=$d['action'];abort_unless($family==='policy'?in_array($action,['save','approve','return','suspend'],true):in_array($action,['save','approve','return','execute'],true),422); abort_unless($review===in_array($action,['approve','return','suspend'],true),403); abort_if(SupplyRecord::where('operation_key',$d['operation_key'])->exists(),409,'Chave utilizada. Recupere a confirmação original.'); if(!empty($d['record_key'])){$old=$this->query($p,$family)->where('record_key',$d['record_key'])->firstOrFail();abort_unless($old->version===$d['version'],409,'Versão alterada.');} else abort_unless($d['version']===0&&$action==='save',409); if($action==='save'){ abort_unless(($d['data']['stock_item_id']??null)===$p['stock_item_id']&&($d['data']['warehouse_id']??null)===$p['warehouse_id'],403); if($family==='supplier_return')abort_unless(($d['data']['kind']??null)==='supplier_return'&&($d['data']['receipt_item_id']??null)===$p['receipt_item_id']&&empty($d['data']['lot_id']),403); else abort_unless(($d['data']['forecast_method']??null)==='manual'&&empty($d['data']['agreement_key'])&&($d['data']['origin']??null)==='local'&&($d['data']['purpose']??null)==='internal',422); if(!empty($d['data']['predecessor_key']))abort_unless($this->query($p,'policy')->where('record_key',$d['data']['predecessor_key'])->where('version',$d['data']['predecessor_version']??0)->exists(),403); } // No automatic purchasing rule may act on this selected QA source. abort_if(app(SupplyPlanning::class)->latest('auto_rule')->where('stock_item_id',$p['stock_item_id'])->where('warehouse_id',$p['warehouse_id'])->where('state','approved')->exists(),409,'Existe regra automática. É necessária conferência própria.'); $envelope=array_intersect_key($d,array_flip(['operation_key','record_key','version','action','reason','data']));$before=app(StockWorkflow::class)->balance($p['stock_item_id'],$p['warehouse_id']); $record=$family==='policy'?app(SupplyPlanning::class)->save($a,'policy',$envelope):app(InventoryDispositions::class)->save($a,$envelope); $after=app(StockWorkflow::class)->balance($p['stock_item_id'],$p['warehouse_id']); return ['record_id'=>$record->id,'record_key'=>$record->record_key,'version'=>$record->version,'impact'=>['family'=>$family,'action'=>$action,'state'=>$record->state,'before_milli'=>$before['quantity'],'after_milli'=>$after['quantity'],'physical_change_milli'=>$after['quantity']-$before['quantity'],'financial_credit_created'=>false,'financial_posting'=>false]]; } }