} $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); } private function nextReference(): string { do { $reference = 'REQ-'.now()->format('Ymd').'-'.Str::upper(Str::random(6)); } while (Reservation::where('reference', $reference)->exists()); return $reference; } }