58 lines
8.5 KiB
PHP
58 lines
8.5 KiB
PHP
<?php
|
|
namespace App\Modules\Purchasing;
|
|
|
|
use Illuminate\Http\{Request,UploadedFile};
|
|
use Illuminate\Support\Facades\{Crypt,DB,Gate};
|
|
use Tempo\Finance\Payables\Payables;
|
|
use Tempo\Purchasing\Embedded\Models\{Supplier,SupplierDocument,AuditLog};
|
|
use Tempo\Purchasing\Embedded\Services\{CurrentActor,RequisitionWorkflow};
|
|
|
|
/** Transport around the original upload/review invalidation and private audited download. */
|
|
final class SupplierDocumentsController
|
|
{
|
|
private function guard():void { abort_unless(config('receipt_qa.supplier_documents_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 source(int $id,array $binding,string $context):Supplier { return SupplierAccess::query($binding,$context)->find($id)??abort(404); }
|
|
private function meta(UploadedFile $file):array { return ['sha256'=>hash_file('sha256',$file->getPathname()),'size'=>$file->getSize(),'mime'=>$file->getMimeType(),'name'=>$file->getClientOriginalName()]; }
|
|
private function input(Request $http,object $actor,string $type):Request {
|
|
$files=$http->file('file')?['file'=>$http->file('file')]:[];
|
|
$r=Request::create('/embedded/supplier-documents','POST',['document_type'=>$type],[],$files,['HTTP_ACCEPT'=>'application/json']);$r->setUserResolver(fn($guard=null)=>$actor);if($http->hasSession())$r->setLaravelSession($http->session());return $r;
|
|
}
|
|
private function store(Request $http,object $actor,Supplier $supplier,string $type):SupplierDocument {
|
|
(new \Tempo\Purchasing\Embedded\Http\Controllers\SupplierDocumentController)->store($this->input($http,$actor,$type),$supplier,app(RequisitionWorkflow::class));return $supplier->documents()->orderByDesc('id')->firstOrFail();
|
|
}
|
|
private function document(Supplier $supplier,int $id):SupplierDocument { return $supplier->documents()->find($id)??abort(404); }
|
|
private function integrity(SupplierDocument $document,string $root):void {
|
|
$file=app(SupplierDocumentFiles::class)->path($root,$document->path,(int)$document->tenant_id,(int)$document->supplier_id);
|
|
abort_unless(filesize($file)===(int)$document->size&&hash_equals($document->sha256,hash_file('sha256',$file)),409,'O ficheiro difere da evidência registada.');
|
|
}
|
|
public function index(int $supplier,Request $r,ReceiptRuntime $runtime) {
|
|
$this->guard();$d=$r->validate(['page'=>'sometimes|integer|min:1|max:10000']);abort_if(array_diff(array_keys($r->query()),array_keys($d)),422);
|
|
return $runtime->run('purchasing_suppliers.view',function($a,$b,$f,$o)use($supplier,$d){$s=$this->source($supplier,$b,$o['sgf_context_key']);Gate::authorize('viewDocuments',$s);return response()->json($s->documents()->orderByDesc('id')->paginate(15,['id','supplier_id','document_type','original_name','mime_type','size','sha256','uploaded_by','created_at'],'page',(int)($d['page']??1)))->header('Cache-Control','private, no-store');});
|
|
}
|
|
public function preview(int $supplier,Request $r,ReceiptRuntime $runtime) {
|
|
$this->guard();$d=$r->validate(['operation_key'=>'required|uuid','revision'=>'required|string|size:64','reason'=>'required|string|min:10|max:2000','document_type'=>'required|string|max:30','file'=>'required|file|max:2048']);abort_if(array_diff(array_keys($r->all()),array_keys($d)),422);unset($d['file']);$d['operation_key']=strtolower($d['operation_key']);$generation=$this->generation();$meta=$this->meta($r->file('file'));
|
|
return $runtime->run('purchasing_suppliers.prepare',function($a,$b,$f,$o)use($supplier,$r,$d,$generation,$meta){$s=$this->source($supplier,$b,$o['sgf_context_key']);$a=app(CurrentActor::class)->lock($a,['procurement']);Gate::authorize('update',$s);abort_unless(hash_equals($this->hash([$s->toArray()]),$d['revision']),409,'A ficha mudou. Confira novamente.');$files=app(SupplierDocumentFiles::class);$stage=$files->root(true);$db=DB::connection();$level=$db->transactionLevel();$document=null;$db->beginTransaction();
|
|
try{$files->using($stage,function()use($r,$a,$s,$d,&$document){$document=$this->store($r,$a,$s,$d['document_type']);});$impact=['document'=>$document->toArray(),'supplier_status'=>'pending','supplier_version'=>$s->fresh()->version,'reason'=>$d['reason']];unset($impact['document']['id'],$impact['document']['created_at'],$impact['document']['updated_at']);}
|
|
finally{while($db->transactionLevel()>$level)$db->rollBack();if($document)$files->discard($stage,$document->path,(int)$s->tenant_id,$s->id);}
|
|
$cut=$d+['supplier_id'=>$supplier,'actor'=>$a->id,'binding'=>$f,'generation'=>$generation,'file'=>$meta,'observed'=>time()];return response()->json(['preview'=>Crypt::encryptString(json_encode($cut,JSON_THROW_ON_ERROR)),'impact'=>$impact]);});
|
|
}
|
|
public function confirm(int $supplier,Request $r,ReceiptRuntime $runtime) {
|
|
$this->guard();$d=$r->validate(['preview'=>'required|string|max:131072','file'=>'required|file|max:2048']);abort_if(array_diff(array_keys($r->all()),array_keys($d)),422);try{$cut=json_decode(Crypt::decryptString($d['preview']),true,64,JSON_THROW_ON_ERROR);}catch(\Throwable){abort(409,'Prévia inválida.');}$generation=$this->generation();$meta=$this->meta($r->file('file'));$created=null;
|
|
try{return $runtime->run('purchasing_suppliers.prepare',function($a,$b,$f,$o)use($supplier,$r,$cut,$generation,$meta,&$created){$s=$this->source($supplier,$b,$o['sgf_context_key']);$a=app(CurrentActor::class)->lock($a,['procurement']);Gate::authorize('update',$s);abort_unless(($cut['supplier_id']??null)===$supplier&&($cut['actor']??null)===$a->id&&($cut['binding']??null)===$f&&($cut['generation']??null)===$generation&&($cut['file']??null)===$meta&&is_int($cut['observed']??null)&&$cut['observed']<=time()&&$cut['observed']>=time()-600,409,'O ficheiro, contexto ou prévia mudou.');
|
|
$hash=$this->hash([$supplier,$cut['file'],$cut['document_type'],$cut['reason']]);$files=app(SupplierDocumentFiles::class);$root=$files->root();$old=AuditLog::where('user_id',$a->id)->where('action','sgf.supplier.document')->where('after->operation_key',$cut['operation_key'])->first();
|
|
if($old){abort_unless(($old->after['request_hash']??null)===$hash&&($old->after['binding']??null)===$f,409,'Chave usada com outro conteúdo.');$this->integrity($this->document($s,$old->after['result']['document_id']),$root);return response()->json($old->after['result']+['replayed'=>true]);}
|
|
abort_unless(hash_equals($this->hash([$s->toArray()]),$cut['revision']),409,'A ficha mudou desde a conferência.');$before=$s->toArray();$document=$files->using($root,fn()=>$this->store($r,$a,$s,$cut['document_type']));$created=['root'=>$root,'path'=>$document->path,'tenant'=>(int)$s->tenant_id,'supplier'=>$s->id];$this->integrity($document,$root);$result=['supplier_id'=>$s->id,'document_id'=>$document->id,'document'=>$document->toArray(),'supplier_version'=>$s->fresh()->version,'supplier_status'=>'pending'];
|
|
app(RequisitionWorkflow::class)->audit($a,'sgf.supplier.document',null,$before,$o+['operation_key'=>$cut['operation_key'],'request_hash'=>$hash,'binding'=>$f,'reason'=>$cut['reason'],'result'=>$result]);return response()->json($result+['replayed'=>false],201);});}
|
|
catch(\Throwable $e){
|
|
// Delete only a newly created file after observing that its DB row did not commit.
|
|
// If rollback/commit is uncertain or verification fails, retain it for recovery.
|
|
if($created){try{$exists=DB::connection('receipt_native')->table('supplier_documents')->where(['tenant_id'=>$created['tenant'],'supplier_id'=>$created['supplier'],'path'=>$created['path']])->exists();if(!$exists)app(SupplierDocumentFiles::class)->discard($created['root'],$created['path'],$created['tenant'],$created['supplier']);}catch(\Throwable){/* Preserve evidence if outcome cannot be verified. */}}throw $e;
|
|
}
|
|
}
|
|
public function download(int $supplier,int $document,Request $r,ReceiptRuntime $runtime) {
|
|
$this->guard();abort_if($r->query(),422);return $runtime->run('purchasing_suppliers.view',function($a,$b,$f,$o)use($supplier,$document,$r){$s=$this->source($supplier,$b,$o['sgf_context_key']);Gate::authorize('viewDocuments',$s);$doc=$this->document($s,$document);$files=app(SupplierDocumentFiles::class);$root=$files->root();$this->integrity($doc,$root);return $files->using($root,fn()=>(new \Tempo\Purchasing\Embedded\Http\Controllers\SupplierDocumentController)->download($this->input($r,$a,$doc->document_type),$s,$doc,app(RequisitionWorkflow::class)));});
|
|
}
|
|
}
|