97 lines
4.8 KiB
PHP
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];
|
|
}
|
|
}
|