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

59 lines
7.1 KiB
PHP

<?php
namespace App\Modules\Purchasing;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\{DB,Gate};
use Tempo\Finance\Payables\Payables;
use Tempo\Purchasing\Embedded\Models\{Requisition,Quotation,PurchaseAward,PurchaseOrder,GoodsReceipt,Invoice,AuditLog};
use Tempo\Purchasing\Embedded\Services\SupplierPayables;
/** Read-only view of existing facts, with independent authority for every stage. */
final class DossierController
{
private function guard():void { abort_unless(config('receipt_qa.dossiers_enabled')===true&&config('receipt_qa.requests_enabled')===true,404); }
private function source(int $id,array $binding,object $actor):Requisition {
abort_unless($actor->hasFunctions(['requester','procurement','finance','department_head'])&&RequisitionAccess::allowed($id,$binding),403);
return Requisition::visibleTo($actor)->find($id)??abort(403,'Requisição fora do contexto autorizado.');
}
private function page($query,array $columns,string $name,array $input):array {
$page=$query->orderBy('id')->paginate(25,$columns,$name,(int)($input[$name]??1));
return ['accessible'=>true,'data'=>$page->items(),'pagination'=>['page'=>$page->currentPage(),'last_page'=>$page->lastPage(),'total'=>$page->total(),'per_page'=>25]];
}
public function screen(Request $r,ReceiptRuntime $runtime) {
$this->guard();$runtime->run('purchasing_requests.view',function($a){abort_unless($a->hasFunctions(['requester','procurement','finance','department_head']),403);});
return response()->view('purchasing-dossier',['context'=>$r->session()->get('company_context.token')]);
}
public function index(Request $r,ReceiptRuntime $runtime) {
$this->guard();$d=$r->validate(['page'=>'sometimes|integer|min:1|max:10000','status'=>'sometimes|in:draft,pending,approved,rejected','search'=>'sometimes|string|max:100']);abort_if(array_diff(array_keys($r->query()),array_keys($d)),422);
return $runtime->run('purchasing_requests.view',function($a,$b)use($d){abort_unless($a->hasFunctions(['requester','procurement','finance','department_head']),403);$q=Requisition::visibleTo($a)->whereIn('id',RequisitionAccess::ids($b));if(isset($d['status']))$q->where('status',$d['status']);if(isset($d['search']))$q->whereRaw('position(lower(?) in lower(number))>0',[$d['search']]);return response()->json($q->orderBy('id')->paginate(25,['id','number','status','process_version'],'page',(int)($d['page']??1)))->header('Cache-Control','no-store');});
}
public function show(int $id,Request $r,ReceiptRuntime $runtime) {
$this->guard();$rules=[];foreach(['quotes','awards','orders','receipts','invoices','obligations','events']as$key)$rules[$key.'_page']='sometimes|integer|min:1|max:10000';$d=$r->validate($rules);abort_if(array_diff(array_keys($r->query()),array_keys($d)),422);
$host=DB::connection();$c=app(\App\Shared\Context\CompanyContext::class);$scope=$c->identifiers()+['unit_id'=>$c->unitId(),'user_id'=>$c->userId()];
return $runtime->run('purchasing_requests.view',function($a,$b,$fingerprint)use($id,$d,$host,$scope){
$request=$this->source($id,$b,$a);$allowed=[];
foreach(['sourcing','orders','receipts','invoices','obligations']as$stage){$flag=$stage==='receipts'?'enabled':$stage.'_enabled';$allowed[$stage]=config('receipt_qa.'.$flag)===true&&$host->table('permission_grants')->where($scope)->where('permission','purchasing_'.$stage.'.view')->lockForUpdate()->first()!==null;}
$viewer=$a->hasFunctions(['admin','procurement','finance','department_head']);$finance=$a->hasFunctions(['admin','finance']);$allowed['sourcing']=$allowed['sourcing']&&$viewer;$allowed['orders']=$allowed['orders']&&$viewer;$allowed['receipts']=$allowed['receipts']&&Gate::allows('viewAny',GoodsReceipt::class);$allowed['invoices']=$allowed['invoices']&&$finance;$allowed['obligations']=$allowed['obligations']&&$finance;
$orders=fn()=>PurchaseOrder::where('tenant_id',$a->tenant_id)->where('requisition_id',$id)->select('id');
$invoices=fn()=>Invoice::where('tenant_id',$a->tenant_id)->whereIn('purchase_order_id',$orders());
$restricted=['accessible'=>false];$stages=['quotes'=>$restricted,'awards'=>$restricted,'orders'=>$restricted,'receipts'=>$restricted,'invoices'=>$restricted,'obligations'=>$restricted];$patterns=['requisition.%','sgf.request.command'];
if($allowed['sourcing']){$stages['quotes']=$this->page(Quotation::where('requisition_id',$id),['id','reference','supplier_id','total_minor','created_by','created_at'],'quotes_page',$d);$stages['awards']=$this->page(PurchaseAward::where('requisition_id',$id),['id','quotation_id','status','total_minor','created_by','created_at'],'awards_page',$d);array_push($patterns,'quotation.%','award.%','sgf.sourcing.command');}
if($allowed['orders']){$stages['orders']=$this->page(PurchaseOrder::visibleTo($a)->where('requisition_id',$id),['id','number','status','total_minor','issued_by','issued_at'],'orders_page',$d);array_push($patterns,'order.%','sgf.order.command');}
if($allowed['receipts']){$stages['receipts']=$this->page(GoodsReceipt::whereIn('purchase_order_id',$orders()),['id','number','purchase_order_id','status','received_on','received_by','confirmed_at'],'receipts_page',$d);array_push($patterns,'receipt.%','sgf.receipt.command','sgf.receipt.document','sgf.receipt.document_downloaded');}
if($allowed['invoices']){$stages['invoices']=$this->page($invoices(),['id','number','purchase_order_id','status','total_minor','created_by','validated_by','validated_at'],'invoices_page',$d);array_push($patterns,'invoice.%','sgf.invoice.command');}
if($allowed['obligations']){
$service=app(SupplierPayables::class);$service->readContext($a);
// The window is on source invoices, not the complete financial universe.
$window=$invoices()->orderBy('id')->paginate(25,['id'],'obligations_page',(int)($d['obligations_page']??1));
$positions=$service->engine($a)->positionsPage($window->getCollection()->pluck('id')->map(fn($v)=>(int)$v)->all());
$stages['obligations']=['accessible'=>true,'data'=>$positions['obligations'],'pagination'=>['page'=>$window->currentPage(),'last_page'=>$window->lastPage(),'total_sources'=>$window->total(),'per_page'=>25],'source_window'=>true];$patterns[]='sgf.obligation.command';
}
$events=AuditLog::where('requisition_id',$id)->where(function($q)use($patterns){foreach($patterns as$pattern)$q->orWhere('action','like',$pattern);});$history=$this->page($events,['id','user_id','action','created_at'],'events_page',$d);
$view=['request'=>$request->only(['id','number','status','process_version','requester_id','total_minor']),'stages'=>$stages,'events'=>$history,'read_only'=>true,'revision_scope'=>'visible_pages_only'];
$view=app(ProcessNavigation::class)->decorate($view,$allowed,$id);
$view['view_revision']=hash('sha256',Payables::canonical([$fingerprint,$view]));
return response()->json($view)->header('Cache-Control','no-store');
});
}
}