find($receipt->purchase_order_id); return $order !== null && ! app(OrganizationAccess::class)->maintenanceOnly($actor) && Gate::forUser($actor)->allows('view', $receipt) && Gate::forUser($actor)->allows('create', [GoodsReceipt::class, $order]); } public function canRead(User $actor, GoodsReceipt $receipt): bool { return ! app(OrganizationAccess::class)->maintenanceOnly($actor) && Gate::forUser($actor)->allows('view', $receipt); } /** @param array $input */ public function add(User $actor, GoodsReceipt $receipt, array $input, UploadedFile $file): ReceiptDocument { abort_unless($this->ready(), 503, 'Anexos indisponíveis nesta instalação.'); $data = Validator::make([...$input, 'file' => $file], [ 'operation_key' => 'required|uuid', 'document_type' => 'required|in:delivery_note,quality,other', 'file' => 'required|file|mimes:pdf,jpg,jpeg,png|extensions:pdf,jpg,jpeg,png|max:2048', ])->validate(); $data['operation_key'] = Str::lower($data['operation_key']); $sha = hash_file('sha256', $file->getPathname()); abort_unless(is_string($sha) && $file->getSize() > 0, 422); $name = Str::substr(Str::replaceMatches('/[^\pL\pN ._()-]/u', '_', basename(str_replace('\\', '/', $file->getClientOriginalName()))), 0, 180); $hash = hash('sha256', json_encode([$receipt->id, $data['document_type'], $sha, $name], JSON_THROW_ON_ERROR)); $newPath = null; try { return DB::transaction(function () use ($actor, $receipt, $data, $file, $sha, $hash, $name, &$newPath) { app(ConflictGuard::class)->lockCompany($actor); $actor = app(CurrentActor::class)->lock($actor); $receipt = GoodsReceipt::query()->lockForUpdate()->findOrFail($receipt->id); abort_unless($this->canAdd($actor, $receipt), 403); $old = AuditLog::query()->whereIn('action', ['receipt.document_added', 'receipt.document_reused'])->where('after->operation_key', $data['operation_key'])->first(); if ($old !== null) { if ($old->user_id !== $actor->id || ($old->after['input_hash'] ?? null) !== $hash) { throw ValidationException::withMessages(['operation_key' => 'Chave utilizada com outro documento.']); } $documentId = $old->after['document_id'] ?? null; abort_unless(is_int($documentId), 409, 'Referência documental inválida.'); return ReceiptDocument::query()->where('goods_receipt_id', $receipt->id)->findOrFail($documentId); } $document = ReceiptDocument::query()->where('goods_receipt_id', $receipt->id)->where('document_type', $data['document_type'])->where('sha256', $sha)->first(); $reused = $document !== null; if ($document === null) { $newPath = $file->store($actor->tenant_id.'/'.$receipt->id, 'receipt_documents'); abort_unless(is_string($newPath), 500); $document = ReceiptDocument::query()->create(['tenant_id' => $actor->tenant_id, 'goods_receipt_id' => $receipt->id, 'uploaded_by' => $actor->id, 'document_type' => $data['document_type'], 'original_name' => $name, 'path' => $newPath, 'mime_type' => $file->getMimeType(), 'size' => $file->getSize(), 'sha256' => $sha, 'created_at' => now()]); } $order = PurchaseOrder::query()->findOrFail($receipt->purchase_order_id); app(RequisitionWorkflow::class)->audit($actor, $reused ? 'receipt.document_reused' : 'receipt.document_added', Requisition::query()->findOrFail($order->requisition_id), null, ['receipt_id' => $receipt->id, 'document_id' => $document->id, 'operation_key' => $data['operation_key'], 'input_hash' => $hash]); return $document; }); } catch (Throwable $error) { if (is_string($newPath)) { // Never delete a file whose metadata may already have committed. try { if (! DB::table('receipt_documents')->where('path', $newPath)->exists()) { Storage::disk('receipt_documents')->delete($newPath); } } catch (Throwable $cleanupError) { report($cleanupError); } } throw $error; } } }