Files
compras/app/Services/ReceiptDocuments.php
2026-11-03 11:51:44 +02:00

102 lines
5.2 KiB
PHP

<?php
namespace App\Services;
use App\Models\AuditLog;
use App\Models\GoodsReceipt;
use App\Models\PurchaseOrder;
use App\Models\ReceiptDocument;
use App\Models\Requisition;
use App\Models\User;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Throwable;
class ReceiptDocuments
{
public function ready(): bool
{
return Schema::hasTable('receipt_documents');
}
public function canAdd(User $actor, GoodsReceipt $receipt): bool
{
$order = PurchaseOrder::query()->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<string,mixed> $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;
}
}
}