192 lines
7.3 KiB
PHP
192 lines
7.3 KiB
PHP
<?php
|
|
|
|
namespace App\Domain\Reservations\Services;
|
|
|
|
use App\Domain\Audit\Models\AuditLog;
|
|
use App\Domain\Inventory\Models\SnipeAsset;
|
|
use App\Domain\Reservations\Enums\ReservationItemStatus;
|
|
use App\Domain\Reservations\Enums\ReservationStatus;
|
|
use App\Domain\Reservations\Models\Reservation;
|
|
use App\Models\User;
|
|
use Carbon\CarbonImmutable;
|
|
use Illuminate\Auth\Access\AuthorizationException;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class ReservationService
|
|
{
|
|
public function __construct(private readonly AvailabilityService $availability) {}
|
|
|
|
/**
|
|
* @param array{purpose:string, usage_location:string, starts_at:string, ends_at:string, asset_ids:list<int>} $data
|
|
*/
|
|
public function createPending(User $requester, array $data): Reservation
|
|
{
|
|
$startsAt = CarbonImmutable::parse($data['starts_at']);
|
|
$endsAt = CarbonImmutable::parse($data['ends_at']);
|
|
|
|
return DB::transaction(function () use ($requester, $data, $startsAt, $endsAt): Reservation {
|
|
$assets = SnipeAsset::query()
|
|
->whereKey($data['asset_ids'])
|
|
->lockForUpdate()
|
|
->get();
|
|
|
|
if ($assets->count() !== count(array_unique($data['asset_ids']))) {
|
|
throw ValidationException::withMessages(['asset_ids' => 'Um ou mais equipamentos já não estão disponíveis.']);
|
|
}
|
|
|
|
$unavailable = $assets->filter(
|
|
fn (SnipeAsset $asset): bool => ! $this->availability->isAvailable($asset, $startsAt, $endsAt)
|
|
);
|
|
|
|
if ($unavailable->isNotEmpty()) {
|
|
throw ValidationException::withMessages([
|
|
'asset_ids' => 'Conflito de disponibilidade: '.$unavailable->pluck('name')->join(', ').'.',
|
|
]);
|
|
}
|
|
|
|
$reservation = Reservation::create([
|
|
'reference' => $this->nextReference(),
|
|
'requester_id' => $requester->id,
|
|
'purpose' => $data['purpose'],
|
|
'usage_location' => $data['usage_location'],
|
|
'starts_at' => $startsAt,
|
|
'ends_at' => $endsAt,
|
|
'status' => ReservationStatus::PendingApproval,
|
|
'approval_deadline' => now()->addHours(48),
|
|
]);
|
|
|
|
foreach ($assets as $asset) {
|
|
$reservation->items()->create([
|
|
'snipe_asset_id' => $asset->id,
|
|
'starts_at' => $startsAt,
|
|
'ends_at' => $endsAt,
|
|
'status' => ReservationItemStatus::Held,
|
|
]);
|
|
}
|
|
|
|
AuditLog::create([
|
|
'actor_id' => $requester->id,
|
|
'event' => 'reservation.submitted',
|
|
'auditable_type' => $reservation->getMorphClass(),
|
|
'auditable_id' => $reservation->id,
|
|
'after' => $reservation->only(['reference', 'purpose', 'usage_location', 'starts_at', 'ends_at', 'status']),
|
|
'metadata' => ['asset_ids' => $assets->modelKeys()],
|
|
]);
|
|
|
|
return $reservation->load('items.asset');
|
|
}, 3);
|
|
}
|
|
|
|
public function cancel(Reservation $reservation, User $actor, string $reason): Reservation
|
|
{
|
|
if ($reservation->requester_id !== $actor->id) {
|
|
throw new AuthorizationException('Apenas o requisitante pode cancelar este pedido.');
|
|
}
|
|
|
|
return DB::transaction(function () use ($reservation, $actor, $reason): Reservation {
|
|
$lockedReservation = Reservation::query()
|
|
->whereKey($reservation->id)
|
|
->lockForUpdate()
|
|
->firstOrFail();
|
|
|
|
if (! $lockedReservation->status->canTransitionTo(ReservationStatus::Cancelled)) {
|
|
throw ValidationException::withMessages([
|
|
'reason' => 'Esta requisição já não pode ser cancelada.',
|
|
]);
|
|
}
|
|
|
|
$previousStatus = $lockedReservation->status->value;
|
|
$lockedReservation->update([
|
|
'status' => ReservationStatus::Cancelled,
|
|
'cancelled_by' => $actor->id,
|
|
'cancelled_at' => now(),
|
|
'cancellation_reason' => $reason,
|
|
]);
|
|
$lockedReservation->items()
|
|
->whereIn('status', ReservationItemStatus::blockingValues())
|
|
->update(['status' => ReservationItemStatus::Cancelled]);
|
|
|
|
AuditLog::create([
|
|
'actor_id' => $actor->id,
|
|
'event' => 'reservation.cancelled',
|
|
'auditable_type' => $lockedReservation->getMorphClass(),
|
|
'auditable_id' => $lockedReservation->id,
|
|
'before' => ['status' => $previousStatus],
|
|
'after' => [
|
|
'status' => ReservationStatus::Cancelled->value,
|
|
'cancellation_reason' => $reason,
|
|
],
|
|
]);
|
|
|
|
return $lockedReservation->load('items.asset');
|
|
}, 3);
|
|
}
|
|
|
|
public function expirePending(int $limit = 100): int
|
|
{
|
|
$reservationIds = Reservation::query()
|
|
->whereIn('status', [
|
|
ReservationStatus::PendingApproval,
|
|
ReservationStatus::AwaitingInformation,
|
|
])
|
|
->whereNotNull('approval_deadline')
|
|
->where('approval_deadline', '<=', now())
|
|
->oldest('approval_deadline')
|
|
->limit($limit)
|
|
->pluck('id');
|
|
|
|
$expired = 0;
|
|
|
|
foreach ($reservationIds as $reservationId) {
|
|
$didExpire = DB::transaction(function () use ($reservationId): bool {
|
|
$reservation = Reservation::query()
|
|
->whereKey($reservationId)
|
|
->lockForUpdate()
|
|
->first();
|
|
|
|
if ($reservation === null
|
|
|| $reservation->approval_deadline === null
|
|
|| $reservation->approval_deadline->isFuture()
|
|
|| ! $reservation->status->canTransitionTo(ReservationStatus::Expired)) {
|
|
return false;
|
|
}
|
|
|
|
$previousStatus = $reservation->status->value;
|
|
$reservation->update(['status' => ReservationStatus::Expired]);
|
|
$reservation->items()
|
|
->whereIn('status', ReservationItemStatus::blockingValues())
|
|
->update(['status' => ReservationItemStatus::Cancelled]);
|
|
|
|
AuditLog::create([
|
|
'actor_id' => null,
|
|
'event' => 'reservation.expired',
|
|
'auditable_type' => $reservation->getMorphClass(),
|
|
'auditable_id' => $reservation->id,
|
|
'before' => ['status' => $previousStatus],
|
|
'after' => ['status' => ReservationStatus::Expired->value],
|
|
'metadata' => ['approval_deadline' => $reservation->approval_deadline->toIso8601String()],
|
|
]);
|
|
|
|
return true;
|
|
}, 3);
|
|
|
|
if ($didExpire) {
|
|
$expired++;
|
|
}
|
|
}
|
|
|
|
return $expired;
|
|
}
|
|
|
|
private function nextReference(): string
|
|
{
|
|
do {
|
|
$reference = 'REQ-'.now()->format('Ymd').'-'.Str::upper(Str::random(6));
|
|
} while (Reservation::where('reference', $reference)->exists());
|
|
|
|
return $reference;
|
|
}
|
|
}
|