clock->now(); $booking = $this->bookings->find_by_id( $booking_id ); if ( null === $booking ) { throw new DomainException( 'booking.not_found', 'Booking not found.' ); } $policy = $this->policy->assert_reschedulable( $booking, $now ); if ( $policy->is_err() ) { throw new DomainException( esc_html( $policy->error_code() ), esc_html( $policy->error_message() ) ); } $service = $this->services->find_by_id( $booking->service_id() ); if ( null === $service ) { throw new DomainException( 'booking.service_missing', 'Service not found.' ); } $agent_id = $booking->agent_id(); if ( null === $agent_id ) { throw new DomainException( 'booking.agent_required', 'Agent is required.' ); } $previous_start = $booking->start_utc(); $unique_lock = 1 === $service->capacity_max(); $zone = $timezone ?? $booking->timezone(); $updated = $this->tx->run( function () use ( $booking, $service, $agent_id, $start_utc, $end_utc, $buffer_start_utc, $buffer_end_utc, $start_local, $zone, $unique_lock, $now ): Booking { $this->bookings->lock_agent_for_update( $agent_id ); $overlapping = $this->bookings->lock_overlapping_for_agent( $agent_id, $buffer_start_utc, $buffer_end_utc, $booking->id() ); if ( $this->conflicts->is_taken( $overlapping, $start_utc, $booking->service_id(), $booking->attendees(), $service->capacity_max(), $booking->id() ) ) { throw new SlotTakenException(); } $booking->reschedule_in_place( $start_utc, $end_utc, $buffer_start_utc, $buffer_end_utc, $start_local, $zone, $now ); $this->bookings->save( $booking, $unique_lock ); return $booking; } ); $this->reminders->cancel_for_booking( (int) $updated->id() ); $this->bus->dispatch( new BookingRescheduled( (int) $updated->id(), $updated->uuid(), $previous_start, $updated->start_utc(), $now ) ); return $updated; } }