Files
NextChair/app/Services/Booking/BookingService.php
T
Stefan Lazic 2ef3654841
CI / ci (push) Canceled after 0s
PHP Rebuild
Initial PHP rebuild of the app
2026-09-05 08:21:33 +01:00

97 lines
4.8 KiB
PHP

<?php
namespace App\Services\Booking;
use App\Enums\AppointmentStatus;
use App\Models\Appointment;
use App\Models\AppointmentService;
use App\Models\Business;
use App\Models\CustomerProfile;
use App\Models\GuestAppointmentAccessToken;
use App\Models\Service;
use App\Services\Availability\AvailabilityService;
use App\Services\Notifications\NotificationService;
use App\Enums\NotificationType;
use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use RuntimeException;
class BookingService
{
public function __construct(private readonly AvailabilityService $availability, private readonly NotificationService $notifications) {}
/** @return array{appointment: Appointment, accessToken: string} */
public function create(Business $business, array $input): array
{
$serviceIds = array_values(array_unique($input['serviceIds']));
$services = Service::query()->where('businessId', $business->id)->whereIn('id', $serviceIds)->where('isActive', true)->get();
if ($services->count() !== count($serviceIds)) {
throw new RuntimeException('One or more selected services are no longer available.');
}
$start = CarbonImmutable::parse($input['startAt'])->setTimezone($business->timezone);
$end = $start->addMinutes($services->sum('durationMinutes') + $services->sum('bufferBeforeMinutes') + $services->sum('bufferAfterMinutes'));
$available = $this->availability->slots($business, $start, $serviceIds, $input['staffId'])->first(fn ($slot) => $slot['startAt'] === $start->toIso8601String());
if (! $available) {
throw new RuntimeException('That time has just been booked. Please choose another slot.');
}
$rawToken = Str::random(64);
$appointment = DB::transaction(function () use ($business, $input, $services, $start, $end, $rawToken): Appointment {
$conflict = Appointment::query()
->where('businessId', $business->id)
->where('staffId', $input['staffId'])
->whereNotIn('status', [AppointmentStatus::Cancelled->value, AppointmentStatus::NoShow->value])
->where('startAt', '<', $end->utc())
->where('endAt', '>', $start->utc())
->lockForUpdate()
->exists();
if ($conflict) {
throw new RuntimeException('That time has just been booked. Please choose another slot.');
}
$email = strtolower(trim($input['email']));
$customer = CustomerProfile::query()->where('businessId', $business->id)->where('email', $email)->first();
if (! $customer) $customer = new CustomerProfile(['businessId' => $business->id, 'email' => $email]);
$customer->firstName = trim($input['firstName']);
$customer->lastName = trim($input['lastName']);
$customer->phone = trim($input['phone'] ?? '') ?: $customer->phone;
$customer->isGuest = ! $customer->userId;
$customer->save();
$appointment = Appointment::create([
'businessId' => $business->id,
'customerId' => $customer->id,
'staffId' => $input['staffId'],
'status' => $business->bookingRule?->requireApproval ? AppointmentStatus::Pending : AppointmentStatus::Confirmed,
'startAt' => $start->utc(),
'endAt' => $end->utc(),
'notes' => $input['notes'] ?? null,
]);
foreach ($services as $service) {
AppointmentService::create(['appointmentId' => $appointment->id, 'serviceId' => $service->id, 'priceAtBooking' => $service->price, 'durationAtBookingMinutes' => $service->durationMinutes]);
}
GuestAppointmentAccessToken::create(['appointmentId' => $appointment->id, 'tokenHash' => hash('sha256', $rawToken), 'expiresAt' => now()->addMonths(6)]);
return $appointment;
});
$appointment->load(['customer', 'staff.user', 'services.service']);
$notificationType = $appointment->status === AppointmentStatus::Pending ? NotificationType::BookingPendingApproval : NotificationType::BookingConfirmed;
$this->notifications->create(
$business,
$notificationType,
$appointment->status === AppointmentStatus::Pending
? 'Your booking request is waiting for the shop to approve.'
: 'Your appointment is confirmed.',
"booking-created:{$appointment->id}",
$appointment->customer->userId,
$appointment->customer->id,
$appointment->id,
"/book/confirmation",
$business->name,
);
return ['appointment' => $appointment, 'accessToken' => $rawToken];
}
}