42 lines
9.3 KiB
PHP
42 lines
9.3 KiB
PHP
<?php
|
|
namespace App\Modules\Purchasing;
|
|
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\{Crypt,DB};
|
|
use Tempo\Finance\Payables\Payables;
|
|
use Tempo\Purchasing\Embedded\Models\{Supplier,AuditLog};
|
|
use Tempo\Purchasing\Embedded\Services\{CurrentActor,SupplierParticipation,RequisitionWorkflow};
|
|
|
|
/** QA transport around the original supplier controller and FormRequest. */
|
|
final class SupplierController
|
|
{
|
|
private function guard():void{abort_unless(config('receipt_qa.suppliers_enabled')===true,404);}
|
|
private function viewer(object $actor):void{abort_unless($actor->hasFunctions(['admin','procurement','finance','department_head']),403);}
|
|
private function hash(mixed $data):string{return hash('sha256',Payables::canonical([$data]));}
|
|
private function source(int $id,array $b,object $a,string $context):?Supplier{$this->viewer($a);return $id===0?null:SupplierAccess::query($b,$context)->findOrFail($id);}
|
|
private function revision(?Supplier $s):string{return $this->hash($s?->toArray());}
|
|
private function rights():array{$c=app(\App\Shared\Context\CompanyContext::class);$s=$c->identifiers();$p=app(\App\Modules\Identity\Services\Permissions::class);$out=[];foreach(['prepare','review']as$r)$out[$r]=$p->has($c->userId(),$s['tenant_id'],$s['company_id'],'purchasing_suppliers.'.$r,$c->unitId());return $out;}
|
|
private function right(string $action):string{return $action==='review'?'review':'prepare';}
|
|
private function dispatch(string $action,array $data,?Supplier $supplier,object $actor,Request $http):array{
|
|
$controller=new \Tempo\Purchasing\Embedded\Http\Controllers\SupplierController;$workflow=app(RequisitionWorkflow::class);
|
|
$class=$action==='review'?Request::class:\Tempo\Purchasing\Embedded\Http\Requests\SaveSupplierRequest::class;
|
|
$input=$class::create('/embedded/suppliers','POST',$data,[],[],['HTTP_ACCEPT'=>'application/json']);
|
|
$input->setUserResolver(fn($guard=null)=>$actor);if($http->hasSession())$input->setLaravelSession($http->session());
|
|
if($action!=='review'){
|
|
$input->setContainer(app())->setRedirector(app('redirect'));
|
|
$route=new \Illuminate\Routing\Route(['POST'],'embedded/suppliers/{supplier}',fn()=>null);$route->bind($input);if($supplier)$route->setParameter('supplier',$supplier);else $route->forgetParameter('supplier');$input->setRouteResolver(fn()=>$route);
|
|
abort_if(array_diff(array_keys($data),array_keys($input->rules())),422);$input->validateResolved();
|
|
}else abort_if(array_diff(array_keys($data),['status','comment','version']),422);
|
|
if($action==='create'){$controller->store($input,$workflow);$supplier=Supplier::where('nuit',$data['nuit'])->firstOrFail();}
|
|
elseif($action==='update')$controller->update($input,$supplier,$workflow);
|
|
else $controller->review($input,$supplier,$workflow);
|
|
$supplier->refresh();return ['result'=>['supplier_id'=>$supplier->id,'status'=>$supplier->status,'version'=>$supplier->version],'impact'=>['operation'=>$action,'before'=>$action==='create'?null:$supplier->getOriginal(),'after'=>$supplier->toArray()]];
|
|
}
|
|
public function screen(Request $r,ReceiptRuntime $rt){$this->guard();$rt->run('purchasing_suppliers.view',fn($a)=>$this->viewer($a));return response()->view('supplier-workspace',['context'=>$r->session()->get('company_context.token'),'csrf'=>csrf_token()]);}
|
|
public function index(Request $r,ReceiptRuntime $rt){$this->guard();$rights=$this->rights();$d=$r->validate(['page'=>'sometimes|integer|min:1|max:10000','search'=>'nullable|string|max:100','status'=>'nullable|in:pending,active,suspended']);abort_if(array_diff(array_keys($r->query()),array_keys($d)),422);return $rt->run('purchasing_suppliers.view',function($a,$b,$f,$o)use($d,$rights){$this->viewer($a);$q=SupplierAccess::query($b,$o['sgf_context_key']);if($d['search']??'')$q->where(fn($w)=>$w->where('name','ilike','%'.$d['search'].'%')->orWhere('nuit','like','%'.$d['search'].'%'));if($d['status']??'')$q->where('status',$d['status']);$part=app(SupplierParticipation::class);return response()->json(['suppliers'=>$q->orderBy('name')->orderBy('id')->paginate(15,['id','name','nuit','status','version'],'page',(int)($d['page']??1)),'new_revision'=>$this->revision(null),'can_prepare'=>$rights['prepare']&&$part->canPrepare($a)]);});}
|
|
public function show(int $supplier,ReceiptRuntime $rt){$this->guard();abort_unless($supplier>0,404);$rights=$this->rights();return $rt->run('purchasing_suppliers.view',function($a,$b,$f,$o)use($supplier,$rights){$s=$this->source($supplier,$b,$a,$o['sgf_context_key']);$part=app(SupplierParticipation::class);return response()->json(['supplier'=>$s,'revision'=>$this->revision($s),'documents_enabled'=>config('receipt_qa.supplier_documents_enabled')===true&&\Illuminate\Support\Facades\Gate::allows('viewDocuments',$s),'geography_enabled'=>config('geography.enabled')===true,'capabilities'=>['prepare'=>$rights['prepare']&&$part->canPrepare($a),'review'=>$rights['review']&&$part->canReview($a,$s)]]);});}
|
|
public function history(int $supplier,Request $r,ReceiptRuntime $rt){$this->guard();abort_unless($supplier>0,404);$d=$r->validate(['page'=>'sometimes|integer|min:1|max:10000']);return $rt->run('purchasing_suppliers.view',function($a,$b,$f,$o)use($supplier,$d){$s=$this->source($supplier,$b,$a,$o['sgf_context_key']);return response()->json(AuditLog::where('tenant_id',$s->tenant_id)->where(function($q)use($supplier){$q->where(fn($w)=>$w->whereIn('action',['supplier.created','supplier.updated','supplier.reviewed'])->where('after->id',$supplier))->orWhere(fn($w)=>$w->where('action','supplier.document_uploaded')->where('after->document->supplier_id',$supplier))->orWhere(fn($w)=>$w->where('action','supplier.document_downloaded')->where('after->supplier_id',$supplier))->orWhere(fn($w)=>$w->where('action','sgf.supplier.document')->where('after->result->supplier_id',$supplier))->orWhere(fn($w)=>$w->where('action','sgf.supplier.command')->where('after->result->supplier_id',$supplier));})->orderByDesc('id')->paginate(15,['id','user_id','action','created_at','before','after'],'page',(int)($d['page']??1)));});}
|
|
public function preview(int $supplier,Request $r,ReceiptRuntime $rt){$this->guard();$d=$r->validate(['operation_key'=>'required|uuid','revision'=>'required|string|size:64','action'=>'required|in:create,update,review','data'=>'required|array']);abort_if(array_diff(array_keys($r->all()),array_keys($d))||strlen(json_encode($d['data']))>16384,422);abort_unless(($supplier===0)===($d['action']==='create'),422);$d['operation_key']=strtolower($d['operation_key']);return $rt->run('purchasing_suppliers.'.$this->right($d['action']),function($a,$b,$f,$o)use($supplier,$r,$d){$s=$this->source($supplier,$b,$a,$o['sgf_context_key']);$a=app(CurrentActor::class)->lock($a,[$d['action']==='review'?'finance':'procurement']);abort_unless(hash_equals($this->revision($s),$d['revision']),409,'A ficha mudou. Confira novamente.');$before=$s?->toArray();$db=DB::connection();$level=$db->transactionLevel();$db->beginTransaction();try{$out=$this->dispatch($d['action'],$d['data'],$s,$a,$r);}finally{while($db->transactionLevel()>$level)$db->rollBack();}$out['impact']['before']=$before;if($d['action']==='create')unset($out['impact']['after']['id'],$out['impact']['after']['created_at'],$out['impact']['after']['updated_at']);$cut=$d+['supplier_id'=>$supplier,'actor'=>$a->id,'binding'=>$f,'observed'=>time()];return response()->json(['preview'=>Crypt::encryptString(json_encode($cut,JSON_THROW_ON_ERROR)),'impact'=>$out['impact']]);});}
|
|
public function confirm(int $supplier,Request $r,ReceiptRuntime $rt){$this->guard();$d=$r->validate(['preview'=>'required|string|max:65536']);abort_if(array_diff(array_keys($r->all()),['preview']),422);try{$cut=json_decode(Crypt::decryptString($d['preview']),true,64,JSON_THROW_ON_ERROR);}catch(\Throwable){abort(409,'Prévia inválida.');}abort_unless(in_array($cut['action']??'', ['create','update','review'],true),409);return $rt->run('purchasing_suppliers.'.$this->right($cut['action']),function($a,$b,$f,$o)use($supplier,$r,$cut){$s=$this->source($supplier,$b,$a,$o['sgf_context_key']);$a=app(CurrentActor::class)->lock($a,[$cut['action']==='review'?'finance':'procurement']);abort_unless(($cut['supplier_id']??null)===$supplier&&($cut['actor']??null)===$a->id&&($cut['binding']??null)===$f&&is_int($cut['observed']??null)&&$cut['observed']<=time()&&$cut['observed']>=time()-600,409,'A prévia ou contexto mudou.');$hash=$this->hash([$supplier,$cut['action'],$cut['data']]);$old=AuditLog::where('user_id',$a->id)->where('action','sgf.supplier.command')->where('after->operation_key',$cut['operation_key'])->first();if($old){abort_unless(($old->after['request_hash']??null)===$hash&&($old->after['binding_revision']??null)===$f,409,'Chave usada com outros dados.');return response()->json($old->after['result']+['replayed'=>true]);}abort_unless(hash_equals($this->revision($s),$cut['revision']),409,'A ficha mudou desde a conferência.');$before=$s?->toArray();$out=$this->dispatch($cut['action'],$cut['data'],$s,$a,$r);app(RequisitionWorkflow::class)->audit($a,'sgf.supplier.command',null,$before,$o+['supplier_scope'=>SupplierAccess::scope($o['sgf_context_key']),'operation_key'=>$cut['operation_key'],'binding_revision'=>$f,'request_hash'=>$hash,'command'=>$cut['action'],'result'=>$out['result']]);return response()->json($out['result']+['replayed'=>false],201);});}
|
|
}
|