hash([request()->session()->get('company_context')]); } private function source(int $id,array $binding):GoodsReceipt { $receipt=GoodsReceipt::findOrFail($id); $order=PurchaseOrder::where('id',$receipt->purchase_order_id)->where(fn($q)=>$q->whereIn('id',$binding['order_ids'])->orWhereIn('requisition_id',RequisitionAccess::ids($binding)))->first(); abort_unless($order,403);Gate::authorize('viewOrder',[GoodsReceipt::class,$order]);Gate::authorize('view',$receipt);return $receipt; } private function meta(UploadedFile $file):array { return ['sha256'=>hash_file('sha256',$file->getPathname()),'size'=>$file->getSize(),'mime'=>$file->getMimeType(),'name'=>$file->getClientOriginalName()]; } private function integrity(ReceiptDocument $doc,string $root):void { $file=app(SupplierDocumentFiles::class)->path($root,$doc->path,(int)$doc->tenant_id,(int)$doc->goods_receipt_id); abort_unless(filesize($file)===(int)$doc->size&&hash_equals($doc->sha256,hash_file('sha256',$file)),409,'O ficheiro difere da evidência registada.'); } private function same(GoodsReceipt $receipt,array $cut):?ReceiptDocument { return ReceiptDocument::where('goods_receipt_id',$receipt->id)->where('document_type',$cut['document_type'])->where('sha256',$cut['file']['sha256'])->first(); } public function index(int $id,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_receipts.view',function($a,$b)use($id,$d){$receipt=$this->source($id,$b);abort_unless(app(ReceiptDocuments::class)->canRead($a,$receipt),403);return response()->json(ReceiptDocument::where('goods_receipt_id',$id)->orderByDesc('id')->paginate(15,self::FIELDS,'page',(int)($d['page']??1)))->header('Cache-Control','private, no-store');}); } public function preview(int $id,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_receipts.confirm',function($a,$b,$f)use($id,$r,$d,$generation,$meta){$receipt=$this->source($id,$b);$service=app(ReceiptDocuments::class);abort_unless($service->canAdd($a,$receipt),403);abort_unless(hash_equals($this->hash([$receipt->getAttributes()]),$d['revision']),409,'A recepção mudou. Confira novamente.');$files=app(SupplierDocumentFiles::class);$stage=$files->root(true,'receipt');$db=DB::connection();$level=$db->transactionLevel();$document=null;$existing=$this->same($receipt,$d+['file'=>$meta]);$db->beginTransaction(); try{$document=$files->using($stage,fn()=>$service->add($a,$receipt,$d,$r->file('file')),'receipt_documents');$impact=['document'=>$document->only(self::FIELDS),'reused'=>$existing!==null,'receipt_unchanged'=>true,'stock_unchanged'=>true,'reason'=>$d['reason']];unset($impact['document']['id'],$impact['document']['created_at']);} finally{while($db->transactionLevel()>$level)$db->rollBack();if($document&&!$existing)$files->discard($stage,$document->path,(int)$receipt->tenant_id,$id,'receipt_documents');} // A reused document belongs to permanent storage; never delete or stage it. if($existing)$this->integrity($existing,$files->root(false,'receipt')); $cut=$d+['receipt_id'=>$id,'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 $id,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.');}$meta=$this->meta($r->file('file'));$generation=$this->generation();$created=null; try{return $runtime->run('purchasing_receipts.confirm',function($a,$b,$f,$o)use($id,$r,$cut,$meta,$generation,&$created){$receipt=$this->source($id,$b);$service=app(ReceiptDocuments::class);abort_unless($service->canAdd($a,$receipt),403); abort_unless(($cut['receipt_id']??null)===$id&&($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([$id,$cut['file'],$cut['document_type'],$cut['reason']]);$files=app(SupplierDocumentFiles::class);$root=$files->root(false,'receipt');$old=AuditLog::where('action','sgf.receipt.document')->where('after->operation_key',$cut['operation_key'])->first(); if($old){abort_unless($old->user_id===$a->id&&($old->after['request_hash']??null)===$hash&&($old->after['binding']??null)===$f,409,'Chave usada com outro conteúdo.');$doc=ReceiptDocument::where('goods_receipt_id',$id)->findOrFail($old->after['result']['document_id']);$this->integrity($doc,$root);return response()->json($old->after['result']+['replayed'=>true]);} abort_unless(hash_equals($this->hash([$receipt->getAttributes()]),$cut['revision']),409,'A recepção mudou desde a conferência.');$existing=$this->same($receipt,$cut);$document=$files->using($root,fn()=>$service->add($a,$receipt,$cut,$r->file('file')),'receipt_documents');if(!$existing)$created=['root'=>$root,'path'=>$document->path,'tenant'=>(int)$receipt->tenant_id,'receipt'=>$id];$this->integrity($document,$root); $result=['receipt_id'=>$id,'document_id'=>$document->id,'document'=>$document->only(self::FIELDS),'reused'=>$existing!==null,'receipt_unchanged'=>true,'stock_unchanged'=>true];$order=PurchaseOrder::findOrFail($receipt->purchase_order_id);app(RequisitionWorkflow::class)->audit($a,'sgf.receipt.document',Requisition::findOrFail($order->requisition_id),null,$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){if($created){try{$exists=DB::connection('receipt_native')->table('receipt_documents')->where(['tenant_id'=>$created['tenant'],'goods_receipt_id'=>$id,'path'=>$created['path']])->exists();if(!$exists)app(SupplierDocumentFiles::class)->discard($created['root'],$created['path'],$created['tenant'],$id,'receipt_documents');}catch(\Throwable){/* An uncertain commit retains its evidence. */}}throw $e;} } public function download(int $id,int $document,Request $r,ReceiptRuntime $runtime) { $this->guard();abort_if($r->query(),422);return $runtime->run('purchasing_receipts.view',function($a,$b,$f,$o)use($id,$document,$r){$receipt=$this->source($id,$b);$doc=ReceiptDocument::where('goods_receipt_id',$id)->findOrFail($document);$files=app(SupplierDocumentFiles::class);$root=$files->root(false,'receipt');$files->path($root,$doc->path,(int)$doc->tenant_id,$id);$input=Request::create('/embedded/receipt-document','GET');$input->setUserResolver(fn($guard=null)=>$a); $response=$files->using($root,fn()=>(new \Tempo\Purchasing\Embedded\Http\Controllers\ReceiptDocumentController)->download($input,$receipt,$document,app(ReceiptDocuments::class)),'receipt_documents');$order=PurchaseOrder::findOrFail($receipt->purchase_order_id);app(RequisitionWorkflow::class)->audit($a,'sgf.receipt.document_downloaded',Requisition::findOrFail($order->requisition_id),null,$o+['receipt_id'=>$id,'document_id'=>$document,'sha256'=>$doc->sha256]);return $response;}); } }