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
← All changes | app/Modules/MCP/Support/SlotResolver.php +27 -63 2.4.0 → 2.5.0 View file →
@@ -9,39 +9,23 @@
9 9
10 10 /**
11 11 * Thin wrapper over the slot engine for the MCP tools.
12 12 *
13 - * Deliberately thin. Availability is the one answer an agent must never get
14 - * differently from what a visitor sees on the booking page, so this class
15 - * computes nothing: it resolves the same service the public page resolves
16 - * (TimeSlotServiceHandler::initService, which returns the round-robin /
17 - * collective / one-off / multi variants for Pro event types), calls the same
18 - * method, and then only reshapes and trims the result.
19 - *
20 - * The reshaping is the point. getAvailableSpots() returns one array per slot,
21 - * keyed by full timestamp — a 30-day window on a 30-minute event is ~480 of
22 - * those, which is roughly 12k tokens of an agent's context for a single call.
23 - * Keyed by date with bare "HH:MM" strings, the same information is about a
24 - * tenth of that. See docs/mcp-server-spec.md §10.
13 + * An agent must see the same availability as the booking page, so this
14 + * computes nothing. It calls the same service the public page uses
15 + * (TimeSlotServiceHandler::initService) and only reshapes and trims the
16 + * result: slots keyed by date as "HH:MM" strings cost about a tenth of the
17 + * engine's per-slot arrays in tokens. See docs/mcp-server-spec.md §10.
25 18 */
26 19 class SlotResolver
27 20 {
28 - /**
29 - * Hard ceiling on a slot query, in days. A caller asking for a year of
30 - * availability does not want a year of availability in one response; it
31 - * wants a smaller question it has not thought of yet.
32 - */
21 + // Hard ceiling on a slot query, in days.
33 22 const MAX_RANGE_DAYS = 62;
34 23
35 24 /**
36 - * Hard ceiling on slots in one response, enforced at a whole-date boundary.
37 - *
38 - * A 62-day window on a busy event is ~1,400 slots ≈ 3,500 tokens, which
39 - * overruns the budget in docs/mcp-server-spec.md §10 by more than double.
40 - * Measured at ~8.6 bytes per slot, 600 keeps a full response near 1,500
41 - * tokens. The cap is never silent: the response says it truncated and names
42 - * the first date it left out, so the agent asks for the next window instead
43 - * of concluding the calendar ends there.
25 + * Hard ceiling on slots in one response, applied at a whole-date boundary.
26 + * At ~8.6 bytes per slot, 600 keeps a response near 1,500 tokens (the
27 + * budget in docs/mcp-server-spec.md §10). Truncation is always reported.
44 28 */
45 29 const MAX_SLOTS = 600;
46 30
47 31 /**
@@ -57,12 +41,10 @@
57 41 */
58 42 public static function getSlots(CalendarSlot $event, $from, $to, $timezone, $duration = null, $hostId = null)
59 43 {
60 44 if ($event->status !== 'active') {
61 - // The slot engine does not check event status — BookingController
62 - // does, before it ever calls the engine. Without mirroring that here
63 - // a draft event reports a full calendar of bookable times that the
64 - // public page would refuse, and an agent would try to book into it.
45 + // The slot engine doesn't check status (BookingController does), so
46 + // a draft event would otherwise show slots the public page refuses.
65 47 return [
66 48 'slots' => [],
67 49 'reason' => sprintf(
68 50 /* translators: %s: the event type's current status */
@@ -81,14 +63,10 @@
81 63 $slots = [];
82 64 $spotsRemaining = [];
83 65 $lastError = null;
84 66
85 - // The engine is month-bounded: getAvailableSpots() derives its end date
86 - // via getMaxBookableDateTime(), which clamps to the last day of the
87 - // START date's month, because the booking page renders one month at a
88 - // time. A single call for 23 Aug – 5 Sep therefore returns August only
89 - // and reports nothing for September — which an agent reads as "fully
90 - // booked" rather than "not asked". So walk the range a month at a time
67 + // getAvailableSpots() only returns the start date's month (the booking
68 + // page renders one month at a time), so walk the range month by month
91 69 // and merge. MAX_RANGE_DAYS keeps this to at most three calls.
92 70 $cursor = $from;
93 71
94 72 while ($cursor <= $to) {
@@ -94,11 +72,10 @@
94 72 while ($cursor <= $to) {
95 73 $spots = $service->getAvailableSpots($cursor . ' 00:00:00', $timezone, $duration, $hostId);
96 74
97 75 if (is_wp_error($spots)) {
98 - // One month being unusable (its window is past the event's
99 - // bookable range) says nothing about the others — keep going and
100 - // only surface the error if no month yields anything.
76 + // A month past the bookable range says nothing about the others.
77 + // Only surface the error if no month yields anything.
101 78 $lastError = $spots;
102 79 } else {
103 80 self::mergeMonth((array) $spots, $from, $to, $slots, $spotsRemaining);
104 81 }
@@ -104,10 +81,9 @@
104 81 }
105 82
106 83 $next = gmdate('Y-m-01', strtotime(gmdate('Y-m-01', strtotime($cursor)) . ' +1 month')); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
107 84
108 - // Guard against a non-advancing cursor: an infinite loop inside a
109 - // request is worse than a wrong answer.
85 + // Guard against a non-advancing cursor.
110 86 if ($next <= $cursor) {
111 87 break;
112 88 }
113 89
@@ -126,13 +102,11 @@
126 102 return self::truncate($slots, $spotsRemaining);
127 103 }
128 104
129 105 /**
130 - * Apply MAX_SLOTS at a whole-date boundary and say so when it bites.
106 + * Apply MAX_SLOTS at a whole-date boundary, since half a day would read
107 + * as a half-booked day.
131 108 *
132 - * Whole dates rather than a flat slot count: half a Tuesday reads as a
133 - * Tuesday that is half booked, which is a different and wrong answer.
134 - *
135 109 * @param array $slots
136 110 * @param array $spotsRemaining
137 111 * @return array
138 112 */
@@ -172,14 +146,11 @@
172 146 return $result;
173 147 }
174 148
175 149 /**
176 - * Fold one month's raw engine output into the accumulating result, trimmed
177 - * to the requested window.
150 + * Fold one month's engine output into the result, trimmed to the window.
151 + * The engine can snap its start back to the 1st, so both bounds are trimmed.
178 152 *
179 - * getAvailableSpots() can also snap its start back to the first of the month,
180 - * so the lower bound needs trimming as well as the upper.
181 - *
182 153 * @param array $spots raw engine output
183 154 * @param string $from
184 155 * @param string $to
185 156 * @param array $slots accumulator, by reference
@@ -206,12 +177,10 @@
206 177 if (!in_array($time, $times, true)) {
207 178 $times[] = $time;
208 179 }
209 180
210 - // `remaining` is false on every event that does not track spots,
211 - // which is most of them. Only build the parallel map when there
212 - // is something in it — an always-present map of nulls is pure
213 - // context cost.
181 + // `remaining` is false on events that don't track spots (most),
182 + // so only build the map when there is something to put in it.
214 183 if (isset($slot['remaining']) && $slot['remaining'] !== false && $slot['remaining'] !== null) {
215 184 $spotsRemaining[$date][$time] = (int) $slot['remaining'];
216 185 }
217 186 }
@@ -225,11 +194,10 @@
225 194
226 195 /**
227 196 * Is one specific slot bookable right now?
228 197 *
229 - * Runs the same engine as getSlots() rather than scanning its output, so the
230 - * answer reflects the state at the moment of asking — this is the check a
231 - * write path relies on, and a cached list is exactly what it must not trust.
198 + * Asks the engine directly rather than scanning getSlots() output, because
199 + * write paths rely on this and need the current state.
232 200 *
233 201 * @param CalendarSlot $event
234 202 * @param string $startUtc 'Y-m-d H:i:s' in UTC, already validated by
235 203 * MCPHelper::toUtc()
@@ -296,12 +264,10 @@
296 264 * @return array|\WP_Error [$from, $to]
297 265 */
298 266 public static function resolveRange($from, $to)
299 267 {
300 - // Absent and unparseable are different questions. Both used to
301 - // normalise to '', so `from: "next tuesday"` fell through to the
302 - // default window and came back as a confident answer about the wrong
303 - // fortnight. Matches BookingTools::dateRange().
268 + // An unparseable date is an error, not a request for the default
269 + // window. Matches BookingTools::dateRange().
304 270 foreach (['from' => $from, 'to' => $to] as $key => $value) {
305 271 if (self::suppliedDate($value) && !self::normalizeDate($value)) {
306 272 return MCPHelper::error(
307 273 'invalid_date',
@@ -366,10 +332,8 @@
366 332 if (!$date) {
367 333 return '';
368 334 }
369 335
370 - // Y-m-d only. strtotime() would also accept "next tuesday", "+1 year"
371 - // and "5", resolving them against the current instant and answering a
372 - // question nobody asked.
336 + // Y-m-d only. strtotime() would accept "next tuesday" or "5".
373 337 return MCPHelper::isRealDate($date) ? $date : '';
374 338 }
375 339 }