services->find_by_id( $command->service_id ); if ( null === $service ) { throw new DomainException( 'booking.service_missing', 'Service not found.' ); } if ( $command->attendees < $service->capacity_min() || $command->attendees > $service->capacity_max() ) { throw new DomainException( 'booking.attendees_capacity', 'Attendees outside service capacity.' ); } if ( $command->agent_id < 1 ) { throw new DomainException( 'booking.agent_required', 'Agent is required.' ); } if ( $command->end_utc <= $command->start_utc ) { throw new DomainException( 'booking.range', 'end_utc must be after start_utc.' ); } $hold_minutes = $command->hold_minutes ?? 0; $unique_lock = 1 === $service->capacity_max(); $now = $this->clock->now(); $booking = $this->tx->run( function () use ( $command, $service, $hold_minutes, $unique_lock, $now ): Booking { $this->bookings->lock_agent_for_update( $command->agent_id ); $overlapping = $this->bookings->lock_overlapping_for_agent( $command->agent_id, $command->buffer_start_utc, $command->buffer_end_utc, null ); if ( $this->conflicts->is_taken( $overlapping, $command->start_utc, $command->service_id, $command->attendees, $service->capacity_max(), null ) ) { throw new SlotTakenException(); } $customer = $this->customers->resolve( $command->customer_first_name, $command->customer_last_name, $command->customer_email, $command->customer_phone ); $customer_id = (int) $customer->id(); $currency = $command->price->currency(); $order = new Order( null, Uuid::generate(), $customer_id, OrderStatus::Pending, $command->price, Money::zero( $currency ), Money::zero( $currency ), $command->price, Money::zero( $currency ), null, OrderSource::Frontend, null, null, null, $now, $now, ); $this->orders->save( $order ); $status = BookingStatus::Pending; if ( $command->confirmed && $hold_minutes <= 0 ) { $status = $service->requires_approval() ? BookingStatus::Approved : BookingStatus::Confirmed; } $booking = new Booking( null, Uuid::generate(), (int) $order->id(), $customer_id, $command->service_id, $command->duration_id, $command->agent_id, $command->location_id, $command->start_utc, $command->end_utc, $command->buffer_start_utc, $command->buffer_end_utc, $command->timezone, $command->start_local, $status, $command->attendees, $command->price, $command->notes, null, null, null, null, null, null, null, $now, $now, ); if ( $hold_minutes > 0 && ! $command->confirmed ) { $booking->place_hold( $now->modify( '+' . $hold_minutes . ' minutes' ), $now ); } $this->bookings->save( $booking, $unique_lock ); return $booking; } ); $this->bus->dispatch( new BookingCreated( (int) $booking->id(), $booking->uuid(), $booking->customer_id(), $booking->order_id(), $now ) ); return $booking; } }