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/AvailabilityDiagnostics.php +27 -70 2.4.0 → 2.5.0 View file →
@@ -12,32 +12,22 @@
12 12
13 13 /**
14 14 * Why does this event show no slots?
15 15 *
16 - * The highest-volume FluentBooking support question, answered in one call.
17 - *
18 - * Design constraint: this class does NOT re-derive availability. Slot maths
19 - * lives in TimeSlotService and a second implementation would eventually
20 - * disagree with the first, which for a diagnostic is worse than useless —
21 - * it would confidently explain an outcome that never happened. So the approach
22 - * is: take the real engine's output as ground truth, read the configuration
23 - * through the model's own accessors, and *attribute* each empty date to the
24 - * first rule that accounts for it, in the order the engine applies them.
25 - *
26 - * Attribution order matters and mirrors TimeSlotService::getDates():
16 + * This does not recompute availability; a second implementation would drift
17 + * from TimeSlotService. It takes the engine's real output and attributes each
18 + * empty date to the first rule that explains it, in the engine's order
19 + * (TimeSlotService::getDates()):
27 20 * event active → bookable window → date override closes the day →
28 21 * weekday has no hours → frequency cap reached → every slot booked →
29 22 * minimum notice (today only)
30 23 *
31 - * A date that survives all of those and still has no slots is reported as
32 - * `unexplained` rather than guessed at. An honest "I don't know" is worth more
33 - * to whoever is holding the support ticket than a plausible wrong answer.
24 + * A date none of these explain is reported as `unexplained`, not guessed at.
34 25 */
35 26 class AvailabilityDiagnostics
36 27 {
37 28 /**
38 - * Statuses that occupy a slot. Mirrors TimeSlotService::getBookedSlots() —
39 - * a booking in any of these states blocks its time.
29 + * Statuses that block a slot. Mirrors TimeSlotService::getBookedSlots().
40 30 */
41 31 const BLOCKING_STATUSES = ['pending', 'reserved', 'approved', 'scheduled', 'completed'];
42 32
43 33 /**
@@ -51,12 +41,10 @@
51 41 public static function run(CalendarSlot $event, $from, $to, $timezone, $hostId = null)
52 42 {
53 43 $scheduleTimezone = $event->getScheduleTimezone($hostId);
54 44
55 - // Stored hours are UTC. Every check below reports them under the
56 - // schedule's own timezone, so convert once here rather than labelling
57 - // raw UTC as local — the same call AvailabilityService makes when it
58 - // renders a schedule for the admin.
45 + // Stored hours are UTC; report them in the schedule's timezone, as
46 + // AvailabilityService does for the admin.
59 47 $weeklySlots = SanitizeService::weeklySchedules(
60 48 (array) $event->getWeeklySlots($hostId),
61 49 'UTC',
62 50 $scheduleTimezone
@@ -79,16 +67,10 @@
79 67 $counts[$date] = count($times);
80 68 $totalSlots += count($times);
81 69 }
82 70
83 - // Two different questions, previously answered by one number:
84 - // - "is the per-day cap reached" is per EVENT TYPE (booking_frequency
85 - // is an event-type setting), and
86 - // - "is the day full" is per HOST, because any booking on any event
87 - // occupies the host's time.
88 - // Using the host-wide count for both reported `daily_cap_reached` on a
89 - // day where the cap was nowhere near, whenever the host happened to be
90 - // busy on some other event type.
71 + // The per-day cap counts this event type only; "day is full" counts
72 + // everything on the host's calendar.
91 73 $bookingsByDate = self::bookingCountsByDate($event, $from, $to, $hostId, $timezone);
92 74 $eventBookingsByDate = self::bookingCountsByDate($event, $from, $to, $hostId, $timezone, true);
93 75
94 76 $window = self::bookableWindow($event, $from, $timezone);
@@ -135,15 +117,12 @@
135 117 ];
136 118 }
137 119
138 120 /**
139 - * The configuration audit: every rule that can remove slots, with the value
140 - * it is actually set to.
121 + * Every rule that can remove slots, with its current value.
141 122 *
142 - * `passed` answers "is this rule permitting anything at all", not "did it
143 - * remove something" — a buffer of 15 minutes passes even though it does
144 - * remove slots, because it is configured sanely. A failing check is one
145 - * that on its own explains an empty calendar.
123 + * A check fails only when it alone explains an empty calendar; a buffer
124 + * that removes some slots still passes.
146 125 *
147 126 * @return array
148 127 */
149 128 private static function checks(CalendarSlot $event, $weeklySlots, $overrideDays, $overrideSlots, $window, $frequencyCaps, $scheduleTimezone, $from, $to, $hostId)
@@ -286,12 +265,10 @@
286 265 continue;
287 266 }
288 267
289 268 $day = strtolower(gmdate('D', strtotime($date))); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
290 - // Host-wide: anything on the host's calendar occupies their time.
269 + // Host-wide count, and this event type's count for its own cap.
291 270 $booked = isset($bookingsByDate[$date]) ? (int) $bookingsByDate[$date] : 0;
292 - // This event type only: booking_frequency is an event-type setting,
293 - // so it must be measured against this event's own bookings.
294 271 $onEvent = isset($eventBookingsByDate[$date]) ? (int) $eventBookingsByDate[$date] : 0;
295 272
296 273 $reason = 'unexplained';
297 274 $detail = __('No rule in this report accounts for this date being empty. Check host-level schedules and connected calendars.', 'fluent-booking');
@@ -354,14 +331,11 @@
354 331 return $explained;
355 332 }
356 333
357 334 /**
358 - * Bookings per date that occupy time on this event's hosts.
335 + * Bookings per local date on this event's hosts, using the engine's host
336 + * set and blocking statuses.
359 337 *
360 - * Counted against the same host set and the same blocking statuses the slot
361 - * engine uses, so "3 bookings" here means the same three the engine removed
362 - * slots for.
363 - *
364 338 * @return array date => count
365 339 */
366 340 private static function bookingCountsByDate(CalendarSlot $event, $from, $to, $hostId, $timezone, $thisEventOnly = false)
367 341 {
@@ -374,11 +348,10 @@
374 348 $query = Booking::whereHas('hosts', function ($query) use ($hostIds) {
375 349 $query->whereIn('user_id', $hostIds);
376 350 })
377 351 ->whereIn('status', self::BLOCKING_STATUSES)
378 - // The window is a LOCAL one — empty_dates walks dates in $timezone —
379 - // so the UTC column has to be bounded by the UTC instants those
380 - // local days start and end at, not by the bare date strings.
352 + // The dates are local to $timezone, so bound the UTC column by
353 + // those days' UTC start and end.
381 354 ->where('start_time', '>=', MCPHelper::dayBoundaryToUtc($from, $timezone, false))
382 355 ->where('start_time', '<=', MCPHelper::dayBoundaryToUtc($to, $timezone, true));
383 356
384 357 if ($thisEventOnly) {
@@ -387,9 +360,9 @@
387 360
388 361 $counts = [];
389 362
390 363 foreach ($query->get(['id', 'start_time']) as $booking) {
391 - // Bucketed by the LOCAL date, for the same reason.
364 + // Bucket by local date too.
392 365 $date = DateTimeHelper::convertFromUtc($booking->start_time, $timezone, 'Y-m-d');
393 366
394 367 $counts[$date] = isset($counts[$date]) ? $counts[$date] + 1 : 1;
395 368 }
@@ -413,14 +386,11 @@
413 386 ];
414 387 }
415 388
416 389 /**
417 - * Weekdays with hours, as day => "09:00-17:00, 18:00-20:00".
390 + * Weekdays with hours, as day => "09:00-17:00, 18:00-20:00". Strings keep
391 + * the payload small; nobody parses these.
418 392 *
419 - * Rendered as strings rather than nested arrays: this is read by a human
420 - * through an agent, and three keys per slot per day would triple the
421 - * payload for information nobody acts on programmatically.
422 - *
423 393 * @return array
424 394 */
425 395 private static function enabledWeekdays($weeklySlots)
426 396 {
@@ -452,13 +422,10 @@
452 422 return $days;
453 423 }
454 424
455 425 /**
456 - * Minutes as something a person reads without arithmetic.
426 + * Minutes in the largest whole unit, e.g. 43200 → "30 days".
457 427 *
458 - * "43200 minutes" is technically the notice period and practically useless
459 - * to whoever is holding the support ticket; "30 days" is the same fact.
460 - *
461 428 * @param int $minutes
462 429 * @return string
463 430 */
464 431 private static function humanizeMinutes($minutes)
@@ -496,13 +463,11 @@
496 463 return Arr::isTrue($schedule, 'enabled') && !empty(Arr::get($schedule, 'slots', []));
497 464 }
498 465
499 466 /**
500 - * Date overrides inside the queried window, flagged by what they do.
467 + * Date overrides inside the window. One without slots closes the day; one
468 + * with slots replaces that day's hours.
501 469 *
502 - * An override present in the day-block list with no replacement slots closes
503 - * the day; one with slots replaces that day's hours.
504 - *
505 470 * @return array
506 471 */
507 472 private static function overridesInRange($overrideDays, $overrideSlots, $from, $to)
508 473 {
@@ -563,19 +528,11 @@
563 528 return implode(', ', $parts);
564 529 }
565 530
566 531 /**
567 - * External busy time, asked of the engine rather than inferred.
568 - *
569 - * `fluent_booking/remote_booked_events` is the exact filter
570 - * TimeSlotService::getBookedSlots() applies to pull Google/Outlook/Apple/
571 - * CalDAV busy blocks into the slot calculation, so running it here reports
572 - * what the engine actually saw — not what a guess at where connections are
573 - * stored would suggest. This is the usual answer when every other check
574 - * passes and slots are still missing.
575 - *
576 - * Counts and providers only. Pulling the titles of a host's private calendar
577 - * events into an agent's context is not this tool's job.
532 + * External calendar busy time, via the same `fluent_booking/remote_booked_events`
533 + * filter TimeSlotService::getBookedSlots() uses, so it reports what the engine saw.
534 + * Counts and providers only; private event titles stay out of the agent's context.
578 535 *
579 536 * @param CalendarSlot $event
580 537 * @param string $from
581 538 * @param string $to