PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.5.0
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.5.0
2.5.0 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 All 34 releases
fluent-booking / app / Modules / MCP / Tools / SlotTools.php

SlotTools.php in Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution 2.5.0, at app/Modules/MCP/Tools/SlotTools.php

295 lines 10.9 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\Modules\MCP\Tools;
4
5 use FluentBooking\App\Models\CalendarSlot;
6 use FluentBooking\App\Modules\MCP\Support\AvailabilityDiagnostics;
7 use FluentBooking\App\Modules\MCP\Support\MCPHelper;
8 use FluentBooking\App\Modules\MCP\Support\PermissionGate;
9 use FluentBooking\App\Modules\MCP\Support\SlotResolver;
10 use FluentBooking\App\Services\PermissionManager;
11 use FluentBooking\Framework\Support\Arr;
12
13 defined('ABSPATH') || exit;
14
15 /**
16 * Availability tools. A single-slot check is a `start_time` parameter on
17 * get-available-slots rather than a separate tool, to save schema budget.
18 */
19 class SlotTools
20 {
21 public static function definitions()
22 {
23 return [
24 'fluent-booking/get-available-slots' => [
25 'label' => __('Get available slots', 'fluent-booking'),
26 'description' => __('Bookable times for an event type, keyed by date in the requested timezone. Pass start_time instead of a range to check one specific slot. Uses the same engine as the public booking page.', 'fluent-booking'),
27 'input_schema' => [
28 'type' => 'object',
29 'properties' => [
30 'event_id' => [
31 'type' => 'integer',
32 'description' => __('The event type to check.', 'fluent-booking'),
33 ],
34 'from' => [
35 'type' => 'string',
36 'description' => __('First date to check, Y-m-d. Defaults to today.', 'fluent-booking'),
37 ],
38 'to' => [
39 'type' => 'string',
40 'description' => __('Last date to check, Y-m-d. Defaults to 14 days out; 62 days maximum.', 'fluent-booking'),
41 ],
42 'start_time' => [
43 'type' => 'string',
44 'description' => __('Check one slot instead of a range: Y-m-d H:i:s in the given timezone.', 'fluent-booking'),
45 ],
46 'timezone' => [
47 'type' => 'string',
48 'description' => __('IANA timezone the times are returned in. Defaults to the site timezone.', 'fluent-booking'),
49 ],
50 'duration' => [
51 'type' => 'integer',
52 'description' => __('Minutes, for events that offer several durations. Defaults to the event default.', 'fluent-booking'),
53 ],
54 'host_id' => [
55 'type' => 'integer',
56 'description' => __('Restrict to one host on a team event.', 'fluent-booking'),
57 ],
58 ],
59 'required' => ['event_id'],
60 ],
61 'annotations' => [
62 'title' => __('Get available slots', 'fluent-booking'),
63 'readonly' => true,
64 ],
65 'permission_callback' => [PermissionGate::class, 'readGate'],
66 'execute_callback' => [self::class, 'getSlots'],
67 ],
68
69 'fluent-booking/diagnose-availability' => [
70 'label' => __('Diagnose availability', 'fluent-booking'),
71 'description' => __('Explain why an event type is or is not offering slots. Returns every rule that can remove slots with its configured value, and attributes each empty date to the specific rule responsible.', 'fluent-booking'),
72 'input_schema' => [
73 'type' => 'object',
74 'properties' => [
75 'event_id' => [
76 'type' => 'integer',
77 'description' => __('The event type to investigate.', 'fluent-booking'),
78 ],
79 'from' => [
80 'type' => 'string',
81 'description' => __('First date to investigate, Y-m-d. Defaults to today.', 'fluent-booking'),
82 ],
83 'to' => [
84 'type' => 'string',
85 'description' => __('Last date to investigate, Y-m-d. Defaults to 14 days out; 62 days maximum.', 'fluent-booking'),
86 ],
87 'timezone' => [
88 'type' => 'string',
89 'description' => __('IANA timezone the dates are interpreted in.', 'fluent-booking'),
90 ],
91 'host_id' => [
92 'type' => 'integer',
93 'description' => __('Investigate one host on a team event.', 'fluent-booking'),
94 ],
95 ],
96 'required' => ['event_id'],
97 ],
98 'annotations' => [
99 'title' => __('Diagnose availability', 'fluent-booking'),
100 'readonly' => true,
101 ],
102 'permission_callback' => [PermissionGate::class, 'readGate'],
103 'execute_callback' => [self::class, 'diagnose'],
104 ],
105 ];
106 }
107
108 /**
109 * @param array $params
110 * @return array|\WP_Error
111 */
112 public static function diagnose($params = [])
113 {
114 $event = self::resolveEvent($params);
115
116 if (is_wp_error($event)) {
117 return $event;
118 }
119
120 $timezone = MCPHelper::resolveTimezone(Arr::get($params, 'timezone', ''));
121
122 $range = SlotResolver::resolveRange(
123 Arr::get($params, 'from', ''),
124 Arr::get($params, 'to', '')
125 );
126
127 if (is_wp_error($range)) {
128 return $range;
129 }
130
131 list($from, $to) = $range;
132
133 $hostId = SlotResolver::validateHostId($event, Arr::get($params, 'host_id'));
134
135 if (is_wp_error($hostId)) {
136 return $hostId;
137 }
138
139 $report = AvailabilityDiagnostics::run($event, $from, $to, $timezone, $hostId);
140
141 $failed = [];
142
143 foreach ($report['checks'] as $check) {
144 if (empty($check['passed'])) {
145 $failed[] = $check['check'];
146 }
147 }
148
149 $nextStep = $failed
150 ? sprintf(
151 /* translators: %s: comma-separated names of the configuration checks that failed */
152 __('These checks fail on their own: %s. Fix those before looking at individual dates.', 'fluent-booking'),
153 implode(', ', $failed)
154 )
155 : '';
156
157 return MCPHelper::success($report, ['timezone' => $timezone], $nextStep);
158 }
159
160 /**
161 * @param array $params
162 * @return array|\WP_Error
163 */
164 public static function getSlots($params = [])
165 {
166 $event = self::resolveEvent($params);
167
168 if (is_wp_error($event)) {
169 return $event;
170 }
171
172 $timezone = MCPHelper::resolveTimezone(Arr::get($params, 'timezone', ''));
173 $duration = absint(Arr::get($params, 'duration')) ?: null;
174
175 $hostId = SlotResolver::validateHostId($event, Arr::get($params, 'host_id'));
176
177 if (is_wp_error($hostId)) {
178 return $hostId;
179 }
180
181 $startTime = sanitize_text_field((string) Arr::get($params, 'start_time', ''));
182
183 if ($startTime) {
184 // Same strict conversion create-booking uses, so a value reported
185 // as free can always be booked. strtotime() would accept a bare
186 // date or "next tuesday".
187 $startUtc = MCPHelper::toUtc($startTime, $timezone);
188
189 if (is_wp_error($startUtc)) {
190 return $startUtc;
191 }
192
193 $check = SlotResolver::checkSlot($event, $startUtc, $timezone, $duration, $hostId);
194
195 if (is_wp_error($check)) {
196 return $check;
197 }
198
199 return MCPHelper::success(
200 array_merge(['event_id' => (int) $event->id], $check),
201 ['timezone' => $timezone]
202 );
203 }
204
205 $range = SlotResolver::resolveRange(
206 Arr::get($params, 'from', ''),
207 Arr::get($params, 'to', '')
208 );
209
210 if (is_wp_error($range)) {
211 return $range;
212 }
213
214 list($from, $to) = $range;
215
216 $result = SlotResolver::getSlots($event, $from, $to, $timezone, $duration, $hostId);
217
218 if (is_wp_error($result)) {
219 return $result;
220 }
221
222 $data = array_merge(
223 [
224 'event_id' => (int) $event->id,
225 'event_type' => $event->event_type,
226 'duration' => (int) $event->getDuration($duration),
227 'from' => $from,
228 'to' => $to,
229 ],
230 $result
231 );
232
233 // False on an indefinite range, so only included when it constrains.
234 $maxLookup = $event->getMaxLookUpDate();
235
236 if ($maxLookup) {
237 $data['bookable_until'] = $maxLookup;
238 }
239
240 $nextStep = '';
241
242 if (empty($result['slots'])) {
243 $nextStep = __('No slots in this window. Call diagnose-availability with the same event_id to see which rule removed them.', 'fluent-booking');
244 }
245
246 return MCPHelper::success($data, ['timezone' => $timezone], $nextStep);
247 }
248
249 /**
250 * Load the event and confirm the caller may see it.
251 *
252 * Slots are public, but this is an operator tool, so the admin read gate
253 * still applies.
254 *
255 * @param array $params
256 * @return CalendarSlot|\WP_Error
257 */
258 private static function resolveEvent($params)
259 {
260 $eventId = absint(Arr::get($params, 'event_id'));
261
262 if (!$eventId) {
263 return MCPHelper::error(
264 'missing_identifier',
265 __('event_id is required. Call get-booking-context or get-event-types to find one.', 'fluent-booking')
266 );
267 }
268
269 $event = CalendarSlot::find($eventId);
270
271 if (!$event) {
272 return MCPHelper::error(
273 'event_not_found',
274 __('No event type matched that id.', 'fluent-booking')
275 );
276 }
277
278 if (!PermissionManager::canReadCalendar($event->calendar_id)) {
279 return MCPHelper::error(
280 'permission_denied',
281 __('You do not have access to this event type.', 'fluent-booking')
282 );
283 }
284
285 if (!$event->calendar) {
286 return MCPHelper::error(
287 'event_not_found',
288 __('This event type has no calendar attached, so availability cannot be computed.', 'fluent-booking')
289 );
290 }
291
292 return $event;
293 }
294 }
295