Initial PHP rebuild of the app
This commit is contained in:
690 files changed
+33944
-12046
No files matched your search
@@ -0,0 +1,96 @@
|
||||
<?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];
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user