PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / trunk
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution vtrunk
2.4.0 2.3.0 2.2.5 2.2.0 2.1.2 2.1.1 trunk 1.10.0 1.10.01 1.10.02 1.5.0 1.5.01 1.5.02 1.5.1 1.5.10 1.5.20 1.5.21 1.5.22 1.5.23 1.5.24 1.5.25 1.6.0 1.7.0 1.7.1 1.7.2 All 33 releases
fluent-booking / app / Services / RescheduleService.php

RescheduleService.php in Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution trunk, at app/Services/RescheduleService.php

148 lines 6.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBooking\App\Services;
4
5 use FluentBooking\App\Models\Booking;
6 use FluentBooking\App\Models\CalendarSlot;
7
8 /**
9 * Moves an existing booking to a new time.
10 *
11 * This logic used to live inline in FrontEndHandler::handleRescheduling(), where
12 * it was reachable only through the public booking form and signalled every
13 * failure with wp_send_json() — which made it impossible to call from anywhere
14 * else without terminating the request. It is extracted here so the public form
15 * and programmatic callers (the MCP server among them) run the exact same steps
16 * and emit the exact same hooks. Failures come back as WP_Error; the caller
17 * decides how to render them.
18 *
19 * Behaviour is intentionally identical to the original inline version: a
20 * reschedule changes the time and leaves `status` alone.
21 *
22 * @since 2.2.6
23 */
24 class RescheduleService
25 {
26 /**
27 * @param Booking $booking The booking being moved.
28 * @param CalendarSlot $calendarEvent The event the booking belongs to.
29 * @param string $startTime New start time, UTC 'Y-m-d H:i:s'.
30 * @param string $timezone The attendee's IANA timezone.
31 * @param array $args {
32 * @type string $reason Rescheduling reason, stored as booking meta.
33 * @type int $host_user_id Resolved host for round-robin events.
34 * @type string $source Where the request came from, used in the
35 * activity log. Defaults to 'Web UI'.
36 * @type string $rescheduled_by Force 'host' or 'guest' instead of
37 * deriving it from the current user.
38 * }
39 *
40 * @return Booking|\WP_Error The updated booking, or the reason it was refused.
41 */
42 public static function reschedule(Booking $booking, CalendarSlot $calendarEvent, $startTime, $timezone, $args = [])
43 {
44 // The booking must be rescheduled against its own event. Reject mixed-object
45 // requests where the posted event_id differs from the booking's event so the
46 // availability validation cannot be performed under a different event than the
47 // one actually being modified.
48 if ((int) $booking->event_id !== (int) $calendarEvent->id) {
49 return new \WP_Error('invalid_reschedule_request', __('Invalid rescheduling request', 'fluent-booking'), ['status' => 422]);
50 }
51
52 $rescheduleBy = isset($args['rescheduled_by']) ? $args['rescheduled_by'] : self::resolveRescheduledBy($booking);
53
54 if ($rescheduleBy == 'guest' && !$booking->canReschedule()) {
55 return new \WP_Error('reschedule_not_allowed', $booking->getRescheduleMessage(), ['status' => 422]);
56 }
57
58 if ($startTime == $booking->start_time) {
59 return new \WP_Error('same_time', __('Sorry! you can not reschedule to the same time.', 'fluent-booking'), ['status' => 422]);
60 }
61
62 $endDateTime = gmdate('Y-m-d H:i:s', strtotime($startTime) + ($booking->slot_minutes * 60)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
63
64 $previousBooking = clone $booking;
65
66 $reason = isset($args['reason']) ? sanitize_textarea_field($args['reason']) : '';
67
68 // The pivot is synced before the row is saved, so a failure between them
69 // left hosts() naming the new host while the row said the old one.
70 try {
71 Helper::dbTransaction(function () use ($booking, $rescheduleBy, $startTime, $timezone, $endDateTime, $previousBooking, $reason, $args) {
72 // Below the guards: both return, so writing above them stamped a
73 // reschedule that never happened, and NotificationHandler reads
74 // this to pick the host- or attendee-worded email.
75 $booking->updateMeta('rescheduled_by_type', $rescheduleBy);
76
77 if ($booking->isMultiGuestBooking()) {
78 // Need to handle group booking type here
79 // check for existing group
80 $parent = Booking::where('status', 'scheduled')
81 ->where('event_id', $booking->event_id)
82 ->where('start_time', $startTime)
83 ->orderBy('id', 'ASC')
84 ->first();
85
86 if ($parent) {
87 $booking->group_id = $parent->group_id;
88 } else {
89 $booking->group_id = Helper::getNextBookingGroup();
90 }
91 }
92
93 if ($booking->isRoundRobinBooking() && !empty($args['host_user_id'])) {
94 $hostId = (int) $args['host_user_id'];
95 $booking->host_user_id = $hostId;
96 $booking->hosts()->sync([$hostId]);
97 }
98
99 $booking->start_time = $startTime;
100 $booking->person_time_zone = $timezone;
101 $booking->end_time = $endDateTime;
102 $booking->save();
103
104 $booking->updateMeta('previous_meeting_time', $previousBooking->start_time);
105
106 if ($reason) {
107 $booking->updateMeta('reschedule_reason', $reason);
108 }
109 });
110 } catch (\Throwable $e) {
111 return new \WP_Error('reschedule_failed', __('The booking could not be moved and was left where it was.', 'fluent-booking'), ['status' => 500]);
112 }
113
114 $source = isset($args['source']) ? $args['source'] : __('Web UI', 'fluent-booking');
115
116 do_action('fluent_booking/log_booking_activity', [
117 'booking_id' => $booking->id,
118 'type' => 'info',
119 'status' => 'closed',
120 'title' => __('Meeting Rescheduled', 'fluent-booking'),
121 /* translators: %1$s is the user who rescheduled the meeting, %2$s is where the request came from, %3$s is the previous date and time in UTC. */
122 'description' => sprintf(__('Meeting has been rescheduled by %1$s from %2$s. Previous date time: %3$s (UTC)', 'fluent-booking'), $rescheduleBy, $source, $previousBooking->start_time)
123 ]);
124
125 do_action('fluent_booking/after_booking_rescheduled', $booking, $previousBooking, $calendarEvent);
126
127 return $booking;
128 }
129
130 /**
131 * A reschedule is "by host" when the current user is one of the booking's
132 * hosts or holds site-wide booking permissions; otherwise it is the guest
133 * acting on their own booking link.
134 *
135 * @return string 'host'|'guest'
136 */
137 public static function resolveRescheduledBy(Booking $booking)
138 {
139 $hostIds = $booking->getHostIds();
140
141 if (in_array(get_current_user_id(), $hostIds) || PermissionManager::userCan(['manage_all_data', 'manage_all_bookings'])) {
142 return 'host';
143 }
144
145 return 'guest';
146 }
147 }
148