Files
SGF/app/Modules/Purchasing/ObligationController.php
T
2026-11-04 09:37:20 +02:00

72 lines
8.7 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\{Invoice,PurchaseOrder,Requisition};
use Tempo\Purchasing\Embedded\Services\{SupplierPayables,RequisitionWorkflow};
/** Explicit recognition in the original native book; no imported mirror or second writer. */
final class ObligationController
{
private function guard():void { abort_unless(config('receipt_qa.obligations_enabled')===true,404); }
private function hash(array $value):string { return hash('sha256',Payables::canonical($value)); }
private function generation():string { return $this->hash([request()->session()->get('company_context')]); }
private function orders(object $a,array $b) {
return PurchaseOrder::visibleTo($a)->where(fn($q)=>$q->whereIn('id',$b['order_ids']??[])->orWhereIn('requisition_id',RequisitionAccess::ids($b)));
}
private function invoice(int $id,object $a,array $b):Invoice {
return Invoice::where('tenant_id',$a->tenant_id)->whereIn('purchase_order_id',$this->orders($a,$b)->select('id'))->find($id)??abort(403,'Documento fora da origem autorizada.');
}
private function adapter(object $a):SupplierPayables {
$service=app(SupplierPayables::class);$service->readContext($a);return $service;
}
private function state(object $a,Invoice $invoice,SupplierPayables $service):array {
$scope=['tenant_id'=>$a->tenant_id,'company_id'=>0,'unit_id'=>0];
$sources=DB::table('pa_sources')->where($scope)->where('local_source_id',$invoice->id)->select('id');
return ['document'=>$service->document($a,$invoice->id),
'proposals'=>DB::table('pa_proposals')->where($scope)->whereIn('source_id',$sources)->orderByDesc('id')->limit(1)->get()->all(),
'obligations'=>DB::table('pa_obligations')->where($scope)->whereIn('source_id',$sources)->orderBy('id')->get()->all(),
'identity'=>DB::table('pa_access')->where($scope)->where('user_id',$a->id)->orderBy('action')->get(['action','person'])->all()];
}
private function rights():array {
$c=app(\App\Shared\Context\CompanyContext::class);$s=$c->identifiers();$p=app(\App\Modules\Identity\Services\Permissions::class);$rights=[];
foreach(['prepare','validate']as$r)$rights[$r]=$p->has($c->userId(),$s['tenant_id'],$s['company_id'],'purchasing_obligations.'.$r,$c->unitId());return $rights;
}
private function commandId(string $action,int $proposal,Invoice $invoice,object $a):int {
if($action==='prepare')return $invoice->id;
abort_unless(DB::table('pa_proposals as p')->join('pa_sources as s','s.id','=','p.source_id')->where(['p.id'=>$proposal,'p.tenant_id'=>$a->tenant_id,'p.company_id'=>0,'p.unit_id'=>0,'s.tenant_id'=>$a->tenant_id,'s.company_id'=>0,'s.unit_id'=>0,'s.local_source_id'=>$invoice->id])->exists(),403,'Proposta fora do documento autorizado.');return $proposal;
}
private function execute(Payables $engine,array $cut,Invoice $invoice,object $a):array {
try{return $engine->command($cut['action'],$this->commandId($cut['action'],$cut['proposal_id'],$invoice,$a),$cut['reason'],$cut['operation_key']);}
catch(\DomainException|\InvalidArgumentException $e){abort(422,$e->getMessage());}
}
public function screen(Request $r,ReceiptRuntime $rt) {
$this->guard();$rt->run('purchasing_obligations.view',fn($a)=>$this->adapter($a));return response()->view('supplier-obligation-workspace',['context'=>$r->session()->get('company_context.token'),'csrf'=>csrf_token()]);
}
public function index(Request $r,ReceiptRuntime $rt) {
$this->guard();$d=$r->validate(['page'=>'sometimes|integer|min:1|max:10000','status'=>'sometimes|in:pending,validated','search'=>'sometimes|string|max:100']);abort_if(array_diff(array_keys($r->query()),array_keys($d)),422);
return $rt->run('purchasing_obligations.view',function($a,$b)use($d){$this->adapter($a);$q=Invoice::where('tenant_id',$a->tenant_id)->whereIn('purchase_order_id',$this->orders($a,$b)->select('id'));if(isset($d['status']))$q->where('status',$d['status']);if(isset($d['search']))$q->whereRaw('position(? in number)>0',[$d['search']]);return response()->json($q->orderBy('id')->paginate(25,['id','number','status','total_minor','purchase_order_id'],'page',(int)($d['page']??1)));});
}
public function show(int $id,Request $r,ReceiptRuntime $rt) {
$this->guard();$d=$r->validate(['proposal_page'=>'sometimes|integer|min:1|max:10000','event_page'=>'sometimes|integer|min:1|max:10000']);abort_if(array_diff(array_keys($r->query()),array_keys($d)),422);$rights=$this->rights();
return $rt->run('purchasing_obligations.view',function($a,$b)use($id,$d,$rights){$invoice=$this->invoice($id,$a,$b);$service=$this->adapter($a);$out=$service->engine($a)->listingPage([$id],$d['proposal_page']??1,$d['event_page']??1,25);foreach($rights as$right=>$allowed)$out['permissions'][$right]=$allowed&&$service->can($a,$right);return response()->json($out+['revision'=>$this->hash($this->state($a,$invoice,$service))]);});
}
public function preview(int $id,Request $r,ReceiptRuntime $rt) {
$this->guard();$d=$r->validate(['action'=>'required|in:prepare,validate','proposal_id'=>'required|integer|min:0','operation_key'=>'required|uuid','revision'=>'required|string|size:64','reason'=>'required|string|min:10|max:2000']);abort_if(array_diff(array_keys($r->all()),array_keys($d)),422);$d['operation_key']=strtolower($d['operation_key']);$generation=$this->generation();
return $rt->run('purchasing_obligations.'.$d['action'],function($a,$b,$f)use($id,$d,$generation){$invoice=$this->invoice($id,$a,$b);$service=$this->adapter($a);abort_unless(hash_equals($this->hash($this->state($a,$invoice,$service)),$d['revision']),409,'A conferência mudou.');$db=DB::connection();$level=$db->transactionLevel();$db->beginTransaction();try{$result=$this->execute($service->engine($a),$d,$invoice,$a);}finally{while($db->transactionLevel()>$level)$db->rollBack();}
unset($result['proposal_id'],$result['obligation_id']);$cut=$d+['person_revision'=>$this->hash($this->state($a,$invoice,$service)['identity']),'invoice_id'=>$id,'actor'=>$a->id,'binding'=>$f,'generation'=>$generation,'observed'=>time()];return response()->json(['preview'=>Crypt::encryptString(json_encode($cut,JSON_THROW_ON_ERROR)),'impact'=>['result'=>$result,'principal_minor'=>$invoice->total_minor,'due_on'=>null,'due_status'=>'unknown','budget_changed'=>false,'stock_changed'=>false,'payment_created'=>false,'ledger_posted'=>false,'reason'=>$d['reason']]]);});
}
public function confirm(int $id,Request $r,ReceiptRuntime $rt) {
$this->guard();$d=$r->validate(['preview'=>'required|string|max:131072']);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']??'', ['prepare','validate'],true),409);$generation=$this->generation();
return $rt->run('purchasing_obligations.'.$cut['action'],function($a,$b,$f,$origin)use($id,$cut,$generation){$invoice=$this->invoice($id,$a,$b);$service=$this->adapter($a);$engine=$service->engine($a);abort_unless(($cut['invoice_id']??null)===$id&&($cut['actor']??null)===$a->id&&($cut['binding']??null)===$f&&($cut['generation']??null)===$generation&&is_int($cut['observed']??null)&&$cut['observed']>=time()-600&&$cut['observed']<=time(),409,'O contexto ou a prévia mudou.');
// A committed retry still calls the kernel: current source/payment eligibility is rechecked.
$receipt=DB::table('pa_commands')->where(['tenant_id'=>$a->tenant_id,'company_id'=>0,'unit_id'=>0,'actor_id'=>$a->id,'command_key'=>$cut['operation_key']])->first();if($receipt)abort_unless((json_decode($receipt->result,true)['local_source_id']??null)===$id,409);
$before=$this->state($a,$invoice,$service);abort_unless(($cut['person_revision']??null)===$this->hash($before['identity']),409,'Identidade alterada.');if(!$receipt)abort_unless(hash_equals($this->hash($before),$cut['revision']),409,'A base mudou desde a conferência.');
$result=$this->execute($engine,$cut,$invoice,$a);
if(!($result['replayed']??false))app(RequisitionWorkflow::class)->audit($a,'sgf.obligation.command',Requisition::findOrFail(PurchaseOrder::findOrFail($invoice->purchase_order_id)->requisition_id),$before,$origin+['operation_key'=>$cut['operation_key'],'reason'=>$cut['reason'],'result'=>$result,'after'=>$this->state($a,$invoice,$service)]);
return response()->json($result,($result['state']??'')==='conflict'?409:(($result['replayed']??false)?200:201));});
}
}