102 lines
5.2 KiB
PHP
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;
|
|
}
|
|
}
|
|
}
|