Shop, menu, and booking storefronts with OIDC SSO, Pay checkout, and merchant.ladill.com deployment workflow. Co-authored-by: Cursor <cursoragent@cursor.com>
117 lines
3.7 KiB
PHP
117 lines
3.7 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Merchant;
|
|
|
|
use App\Models\QrBooking;
|
|
use App\Models\QrCode;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Collection;
|
|
|
|
class BookingSlotService
|
|
{
|
|
/** @var list<string> */
|
|
private const DAY_KEYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
|
|
|
|
/**
|
|
* @return list<array{starts_at: string, ends_at: string, label: string}>
|
|
*/
|
|
public function availableSlots(QrCode $qrCode, string $date, int $serviceIndex): array
|
|
{
|
|
$content = $qrCode->content();
|
|
$services = $content['services'] ?? [];
|
|
$service = $services[$serviceIndex] ?? null;
|
|
if (! is_array($service)) {
|
|
return [];
|
|
}
|
|
|
|
$duration = max(15, (int) ($service['duration_minutes'] ?? 30));
|
|
$day = Carbon::parse($date)->startOfDay();
|
|
if ($day->isPast() && ! $day->isToday()) {
|
|
return [];
|
|
}
|
|
|
|
$windows = $this->windowsForDay($content, $day);
|
|
if ($windows === []) {
|
|
return [];
|
|
}
|
|
|
|
$booked = QrBooking::query()
|
|
->where('qr_code_id', $qrCode->id)
|
|
->whereIn('status', [QrBooking::STATUS_PENDING, QrBooking::STATUS_CONFIRMED])
|
|
->whereDate('starts_at', $day->toDateString())
|
|
->get(['starts_at', 'ends_at']);
|
|
|
|
$slots = [];
|
|
foreach ($windows as $window) {
|
|
$cursor = $day->copy()->setTimeFromTimeString($window['start']);
|
|
$windowEnd = $day->copy()->setTimeFromTimeString($window['end']);
|
|
|
|
while ($cursor->copy()->addMinutes($duration)->lte($windowEnd)) {
|
|
$slotEnd = $cursor->copy()->addMinutes($duration);
|
|
if ($day->isToday() && $cursor->lte(now())) {
|
|
$cursor->addMinutes($duration);
|
|
continue;
|
|
}
|
|
|
|
if (! $this->overlapsBooked($cursor, $slotEnd, $booked)) {
|
|
$slots[] = [
|
|
'starts_at' => $cursor->toIso8601String(),
|
|
'ends_at' => $slotEnd->toIso8601String(),
|
|
'label' => $cursor->format('g:i A'),
|
|
];
|
|
}
|
|
|
|
$cursor->addMinutes($duration);
|
|
}
|
|
}
|
|
|
|
return $slots;
|
|
}
|
|
|
|
/** @return list<array{start: string, end: string}> */
|
|
private function windowsForDay(array $content, Carbon $day): array
|
|
{
|
|
$key = self::DAY_KEYS[$day->dayOfWeek];
|
|
$availability = $content['availability'] ?? [];
|
|
$windows = $availability[$key] ?? [];
|
|
|
|
if (! is_array($windows) || $windows === []) {
|
|
$opens = (string) ($content['opens_at'] ?? '09:00');
|
|
$closes = (string) ($content['closes_at'] ?? '17:00');
|
|
$activeDays = $content['active_days'] ?? ['mon', 'tue', 'wed', 'thu', 'fri'];
|
|
|
|
if (! in_array($key, (array) $activeDays, true)) {
|
|
return [];
|
|
}
|
|
|
|
return [['start' => $opens, 'end' => $closes]];
|
|
}
|
|
|
|
$normalized = [];
|
|
foreach ($windows as $window) {
|
|
if (! is_array($window)) {
|
|
continue;
|
|
}
|
|
$start = trim((string) ($window['start'] ?? ''));
|
|
$end = trim((string) ($window['end'] ?? ''));
|
|
if ($start !== '' && $end !== '') {
|
|
$normalized[] = ['start' => $start, 'end' => $end];
|
|
}
|
|
}
|
|
|
|
return $normalized;
|
|
}
|
|
|
|
/** @param Collection<int, QrBooking> $booked */
|
|
private function overlapsBooked(Carbon $start, Carbon $end, Collection $booked): bool
|
|
{
|
|
foreach ($booked as $booking) {
|
|
if ($start < $booking->ends_at && $end > $booking->starts_at) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|