*/ public function getAvailability(): array { $fresh = config('slots.availability_cache_fresh_seconds'); $ttl = config('slots.availability_cache_ttl_seconds'); return Cache::flexible(self::AVAILABILITY_CACHE_KEY, [$fresh, $ttl], function (): array { return $this->buildAvailabilityPayload(); }); } private function buildAvailabilityPayload(): array { return Slot::query() ->orderBy('id') ->get() ->map(fn (Slot $slot): array => [ 'slot_id' => $slot->id, 'capacity' => $slot->capacity, 'remaining' => $this->availableRemaining($slot), ]) ->all(); } public function createHold(Slot $slot, string $idempotencyKey): Hold { $existing = $this->findHoldByIdempotencyKey($idempotencyKey); if ($existing !== null) { return $existing->load('slot'); } try { return DB::transaction(function () use ($slot, $idempotencyKey): Hold { $lockedSlot = Slot::query()->lockForUpdate()->findOrFail($slot->id); if ($this->availableRemaining($lockedSlot) <= 0) { throw new SlotCapacityExceededException('Slot capacity is exhausted.'); } return Hold::query()->create([ 'slot_id' => $lockedSlot->id, 'idempotency_key' => $idempotencyKey, 'status' => HoldStatus::Held, 'expires_at' => now()->addMinutes(config('slots.hold_ttl_minutes')), ])->load('slot'); }); } catch (UniqueConstraintViolationException) { $existing = $this->findHoldByIdempotencyKey($idempotencyKey); if ($existing === null) { throw new SlotCapacityExceededException('Slot capacity is exhausted.'); } return $existing->load('slot'); } } public function confirmHold(Hold $hold): Hold { return DB::transaction(function () use ($hold) { $lockedHold = Hold::query()->lockForUpdate()->findOrFail($hold->id); if ($lockedHold->status === HoldStatus::Confirmed) { return $lockedHold->load('slot'); } if ($lockedHold->status !== HoldStatus::Held || $lockedHold->isExpired()) { throw new HoldNotAvailableException('Hold cannot be confirmed'); } $slot = Slot::query()->lockForUpdate()->findOrFail($lockedHold->slot_id); if ($slot->remaining < 1) { throw new SlotCapacityExceededException('Slot capacity is exhausted.'); } $slot->remaining--; $slot->save(); $lockedHold->status = HoldStatus::Confirmed; $lockedHold->save(); $this->invalidateAvailabilityCache(); return $lockedHold->load('slot'); }); } public function cancelHold(Hold $hold): Hold { return DB::transaction(function () use ($hold) { $lockedHold = Hold::query()->lockForUpdate()->findOrFail($hold->id); if ($lockedHold->status === HoldStatus::Cancelled) { return $lockedHold->load('slot'); } if ($lockedHold->status === HoldStatus::Confirmed) { $slot = Slot::query()->lockForUpdate()->findOrFail($lockedHold->slot_id); $slot->remaining = min($slot->capacity, $slot->remaining + 1); $slot->save(); } $lockedHold->status = HoldStatus::Cancelled; $lockedHold->save(); $this->invalidateAvailabilityCache(); return $lockedHold->load('slot'); }); } private function availableRemaining(Slot $slot): int { $activeHolds = Hold::query() ->where('slot_id', $slot->id) ->where('status', HoldStatus::Held) ->where('expires_at', '>', now()) ->count(); return max(0, $slot->remaining - $activeHolds); } private function findHoldByIdempotencyKey(string $idempotencyKey): ?Hold { return Hold::query() ->where('idempotency_key', $idempotencyKey) ->first(); } private function invalidateAvailabilityCache(): void { Cache::forget(self::AVAILABILITY_CACHE_KEY); } }