PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 1.5.02
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v1.5.02
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 / TimeSlotService.php

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

1,035 lines 37.1 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\Calendar;
7 use FluentBooking\App\Models\CalendarSlot;
8 use FluentBooking\App\Models\Availability;
9 use FluentBooking\Framework\Support\Arr;
10 use FluentBooking\Framework\Support\DateTime;
11
12 class TimeSlotService
13 {
14 protected $calendarSlot;
15
16 protected $calendar;
17
18 protected $hostId = null;
19
20 protected $groupedSlots = [];
21
22 public function __construct(Calendar $calendar, CalendarSlot $calendarSlot)
23 {
24 $this->hostId = null;
25 $this->groupedSlots = [];
26 $this->calendar = $calendar;
27 $this->calendarSlot = $calendarSlot;
28 }
29
30 public function getDates($fromDate = false, $toDate = false, $duration = null, $isDoingBooking = false, $timeZone = 'UTC')
31 {
32 $duration = $this->calendarSlot->getDuration($duration);
33
34 $fromDate = $fromDate ?: gmdate('Y-m-d'); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
35 $toDate = $toDate ?: gmdate('Y-m-t 23:59:59', strtotime($fromDate)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
36
37 $ranges = $this->getCurrentDateRange($fromDate, $toDate);
38
39 $bookedSlots = $this->getBookedSlots([$fromDate, $toDate], 'UTC', $isDoingBooking);
40
41 $ranges = $this->maybeBookingFrequencyLimitRanges($ranges, $bookedSlots);
42 $ranges = $this->maybeBookingDurationLimitRanges($ranges, $bookedSlots, $duration);
43
44 $cutOutTime = DateTimeHelper::getTimestamp() + $this->calendarSlot->getCutoutSeconds();
45
46 $maxBookingTime = strtotime($this->calendarSlot->getMaxBookableDateTime($fromDate, $timeZone, 'Y-m-d H:i:s'));
47
48 $timezoneInfo = $this->getTimezoneInfo();
49
50 $rangedSlots = $this->getRangedValidSlots($ranges, $duration, $bookedSlots, $cutOutTime, $maxBookingTime, $timezoneInfo);
51
52 if (!$this->groupedSlots) {
53 return $rangedSlots;
54 }
55
56 return $this->adjustGroupedSlots($ranges, $rangedSlots, $cutOutTime, $maxBookingTime, $timezoneInfo);
57 }
58
59 protected function getRangedValidSlots($ranges, $duration, $bookedSlots, $cutOutTime, $maxBookingTime, $timezoneInfo, $rangedSlots = [], $hostId = null)
60 {
61 $period = $duration * 60;
62
63 $hostId = $hostId ?: $this->hostId;
64
65 $bufferTime = $this->calendarSlot->getTotalBufferTime() * 60;
66
67 $daySlots = $this->getWeekDaySlots($duration, $hostId);
68
69 $dateOverrides = $this->calendarSlot->getDateOverrides($hostId);
70
71 list($scheduleTimezone, $dstTime) = $timezoneInfo;
72
73 $todayDate = gmdate('Y-m-d'); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
74
75 $lastDate = end($ranges);
76
77 foreach ($ranges as $date) {
78 $day = strtolower(gmdate('D', strtotime($date))); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
79
80 $availableSlots = $daySlots[$day] ?? [];
81
82 $availableSlots = $this->maybeDateOverrides($dateOverrides, $availableSlots, $date, $duration);
83
84 if (!$availableSlots) {
85 continue;
86 }
87
88 $currentBookedSlots = $bookedSlots[$date] ?? [];
89
90 $isToday = $date === $todayDate;
91
92 $isLastDay = $date === $lastDate;
93
94 $validSlots = [];
95
96 foreach ($availableSlots as $start) {
97 $end = gmdate('H:i', strtotime($start) + $period); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
98 $endDate = $start < $end ? $date : gmdate('Y-m-d', strtotime($date) + 86400); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
99
100 $slot = [
101 'start' => $date . ' ' . $start . ':00',
102 'end' => $endDate . ' ' . $end . ':00'
103 ];
104
105 $slot = $this->maybeDayLightSavingSlot($slot, $dstTime, $scheduleTimezone);
106
107 if ($isToday && strtotime($slot['start']) < $cutOutTime) {
108 continue;
109 }
110
111 if ($isLastDay && strtotime($slot['end']) > $maxBookingTime) {
112 continue;
113 }
114
115 $isSlotAvailable = $this->isSlotAvailable($slot, $currentBookedSlots, $bufferTime, $hostId);
116
117 if ($isSlotAvailable) {
118 $validSlots[] = $slot;
119 }
120 }
121
122 if ($validSlots) {
123 $currentSlots = $rangedSlots[$date] ?? [];
124 $rangedSlots[$date] = $this->mergeAndSortSlots($currentSlots, $validSlots);
125 }
126 }
127
128 return $rangedSlots;
129 }
130
131 protected function isSlotAvailable(&$slot, $currentBookedSlots, $bufferTime, $hostId)
132 {
133 if (!$currentBookedSlots) {
134 return true;
135 }
136
137 $startTimeStamp = strtotime($slot['start']);
138 $endTimeStamp = strtotime($slot['end']);
139
140 foreach ($currentBookedSlots as $bookedSlot) {
141 $bookedStart = strtotime($bookedSlot['start']);
142 $bookedEnd = strtotime($bookedSlot['end']);
143
144 if (Arr::get($bookedSlot, 'source')) {
145 $bookedStart = $bookedStart - $bufferTime;
146 $bookedEnd = $bookedEnd + $bufferTime;
147 }
148
149 if (
150 ($startTimeStamp >= $bookedStart && $startTimeStamp < $bookedEnd) ||
151 ($endTimeStamp > $bookedStart && $endTimeStamp <= $bookedEnd) ||
152 ($startTimeStamp <= $bookedStart && $endTimeStamp > $bookedStart) ||
153 ($startTimeStamp < $bookedEnd && $endTimeStamp >= $bookedEnd)
154 ) {
155 if (!Arr::get($bookedSlot, 'remaining')) {
156 return false;
157 }
158 $slot['remaining'] = $bookedSlot['remaining'];
159 }
160 }
161
162 return true;
163 }
164
165 protected function adjustGroupedSlots($ranges, $rangedSlots, $cutOutTime, $maxBookingTime, $timezoneInfo)
166 {
167 list($scheduleTimezone, $dstTime) = $timezoneInfo;
168
169 $todayDate = gmdate('Y-m-d'); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
170
171 $lastDate = end($ranges);
172
173 $addedDates = [];
174
175 foreach ($this->groupedSlots as $slot) {
176 $date = gmdate('Y-m-d', strtotime($slot['start']));
177 if ($todayDate == $date && strtotime($slot['start']) < $cutOutTime) {
178 continue;
179 }
180
181 if ($lastDate == $date && strtotime($slot['end']) > $maxBookingTime) {
182 continue;
183 }
184
185 if (!isset($rangedSlots[$date])) {
186 $rangedSlots[$date] = [];
187 $rangedSlots[$date][] = $slot;
188 continue;
189 }
190
191 $addedDates[$date] = $date;
192
193 $rangedSlots[$date][] = $slot;
194 }
195
196 foreach ($addedDates as $date) {
197 $dateSlots = $rangedSlots[$date];
198
199 usort($dateSlots, function ($a, $b) {
200 return strtotime($a['start']) - strtotime($b['start']);
201 });
202
203 $rangedSlots[$date] = $dateSlots;
204 }
205
206 return $rangedSlots;
207 }
208
209 public function isSpotAvailable($fromTime, $toTime, $duration = null, $hostId = null)
210 {
211 $this->hostId = $hostId;
212
213 $fromTimeStamp = strtotime($fromTime);
214 $toTimeStamp = strtotime($toTime);
215
216 $fromTime = gmdate('Y-m-d 00:00:00', $fromTimeStamp); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
217 $toTime = gmdate('Y-m-d 23:59:59', $toTimeStamp); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
218
219 $duration = $this->calendarSlot->getDuration($duration);
220
221 $slots = $this->getDates($fromTime, $toTime, $duration, true);
222
223 $fromDate = gmdate('Y-m-d', $fromTimeStamp); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
224 $toDate = gmdate('Y-m-d', $toTimeStamp); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
225
226 $availableSlots = $slots[$fromDate] ?? [];
227
228 if ($fromDate != $toDate) {
229 $availableSlots = array_merge($availableSlots, $slots[$toDate] ?? []);
230 }
231
232 $left = 0;
233 $right = count($availableSlots) - 1;
234
235 while ($left <= $right) {
236 $mid = $left + (($right - $left) >> 1);
237
238 $midStartTime = strtotime($availableSlots[$mid]['start']);
239 $midEndTime = strtotime($availableSlots[$mid]['end']);
240
241 if ($fromTimeStamp == $midStartTime && $toTimeStamp == $midEndTime) {
242 return true;
243 } elseif ($fromTimeStamp > $midStartTime) {
244 $left = $mid + 1;
245 } else {
246 $right = $mid - 1;
247 }
248 }
249
250 return false;
251 }
252
253 protected function getCurrentDateRange($startDate = false, $endDate = false)
254 {
255 if (!$startDate) {
256 $startDate = gmdate('Y-m-d'); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
257 }
258
259 if (!$endDate) {
260 $endDate = gmdate('Y-m-t 23:59:59', strtotime($startDate)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
261 }
262
263 $currentDate = strtotime($startDate);
264 $endDate = strtotime($endDate);
265 $oneDay = 24 * 60 * 60;
266
267 $dateArray = [];
268
269 while ($currentDate <= $endDate) {
270 $dateArray[] = gmdate('Y-m-d', $currentDate); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
271 $currentDate += $oneDay;
272 }
273
274 return $dateArray;
275 }
276
277 protected function bookSlot($eventId, $start, $end, $remaining = 0, $source = null)
278 {
279 return [
280 'event_id' => $eventId,
281 'start' => $start,
282 'end' => $end,
283 'remaining' => $remaining,
284 'source' => $source
285 ];
286 }
287
288 protected function getBookedSlots($dateRange, $toTimeZone = 'UTC', $isDoingBooking = false)
289 {
290 if ($toTimeZone != 'UTC') {
291 $dateRange[0] = DateTimeHelper::convertToUtc($dateRange[0], $toTimeZone);
292 $dateRange[1] = DateTimeHelper::convertToUtc($dateRange[1], $toTimeZone);
293 }
294
295 $hostIds = $this->calendarSlot->getHostIds($this->hostId);
296 $status = ['pending', 'reserved', 'approved', 'scheduled', 'completed'];
297
298 $bookings = Booking::with(['calendar_event'])
299 ->whereHas('hosts', function ($query) use ($hostIds) {
300 $query->whereIn('user_id', $hostIds);
301 })
302 ->whereBetween('start_time', $dateRange)
303 ->orderBy('start_time', 'ASC')
304 ->whereIn('status', $status)
305 ->get()
306 ->groupBy('group_id');
307
308 $maxBooking = $this->calendarSlot->getMaxBookingPerSlot();
309
310 $isGroupBooking = $maxBooking > 1;
311
312 $books = $this->processBookings($bookings, $toTimeZone, $maxBooking, $isGroupBooking);
313
314 $books = apply_filters('fluent_booking/local_booked_events', $books, $this->calendarSlot, $toTimeZone, $dateRange, $isDoingBooking);
315
316 $remoteBookings = apply_filters('fluent_booking/remote_booked_events', [], $this->calendarSlot, $toTimeZone, $dateRange, $this->hostId, $isDoingBooking);
317
318 $books = $this->processRemoteBookings($books, $remoteBookings);
319
320 return apply_filters('fluent_booking/booked_events', $books, $this->calendarSlot, $toTimeZone, $dateRange, $isDoingBooking);
321 }
322
323 protected function processBookings($bookings, $toTimeZone, $maxBooking, $isGroupBooking)
324 {
325 $books = [];
326 foreach ($bookings as $booking) {
327 $booked = $booking->count();
328 $booking = $booking[0];
329
330 $remaining = 0;
331 if ($this->calendarSlot->id == $booking->event_id) {
332 if ($booking->status == 'reserved') {
333 continue;
334 }
335 $remaining = max(0, $maxBooking - $booked);
336 }
337
338 if ($toTimeZone != 'UTC') {
339 $booking->start_time = DateTimeHelper::convertToTimeZone($booking->start_time, 'UTC', $toTimeZone);
340 $booking->end_time = DateTimeHelper::convertToTimeZone($booking->end_time, 'UTC', $toTimeZone);
341 }
342
343 $date = gmdate('Y-m-d', strtotime($booking->start_time)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
344
345 $books[$date] = $books[$date] ?? [];
346
347 $bufferTime = $booking->calendar_event->getTotalBufferTime();
348 if ($bufferTime) {
349 $beforeBufferTime = gmdate('Y-m-d H:i:s', strtotime($booking->start_time . " -$bufferTime minutes")); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
350 $afterBufferTime = gmdate('Y-m-d H:i:s', strtotime($booking->end_time . " +$bufferTime minutes")); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
351 if ($remaining) {
352 if ($beforeBufferTime < $booking->start_time) {
353 $books[$date][] = $this->bookSlot(null, $beforeBufferTime, $booking->start_time);
354 }
355 if ($afterBufferTime > $booking->end_time) {
356 $books[$date][] = $this->bookSlot(null, $booking->end_time, $afterBufferTime);
357 }
358 } else {
359 $booking->start_time = $beforeBufferTime;
360 $booking->end_time = $afterBufferTime;
361 }
362 }
363
364 $rangedItems = $this->createDateRangeArrayFromSlotConfig([
365 'event_id' => $booking->event_id,
366 'start' => $booking->start_time,
367 'end' => $booking->end_time,
368 'remaining' => $remaining
369 ]);
370
371 $eventIdAdded = false;
372 foreach ($rangedItems as $date => $slot) {
373 if ($isGroupBooking && $remaining && $this->calendarSlot->id == $booking->event_id) {
374 $this->groupedSlots[] = $slot;
375 if ($eventIdAdded) {
376 $slot['event_id'] = null;
377 }
378 $eventIdAdded = true;
379 }
380 $books[$date] = $books[$date] ?? [];
381 $books[$date][] = $slot;
382 }
383 }
384
385 return $books;
386 }
387
388 protected function processRemoteBookings($books, $remoteBookings)
389 {
390 if (!$remoteBookings) {
391 return $books;
392 }
393
394 foreach ($remoteBookings as $slot) {
395 $rangedItems = $this->createDateRangeArrayFromSlotConfig([
396 'start' => $slot['start'],
397 'end' => $slot['end'],
398 'source' => $slot['source']
399 ]);
400
401 foreach ($rangedItems as $rangedDate => $rangedSlot) {
402 $books[$rangedDate] = $books[$rangedDate] ?? [];
403
404 if (!$this->isLocalBooking($books[$rangedDate], $rangedSlot)) {
405 $books[$rangedDate][] = $rangedSlot;
406 }
407 }
408 }
409 return $books;
410 }
411
412 protected function getWeekDaySlots($duration, $hostId = null)
413 {
414 $period = $duration * 60;
415
416 $hostId = $hostId ?: $this->hostId;
417
418 $interval = $this->calendarSlot->getSlotInterval($duration) * 60;
419
420 $weeklySlots = $this->calendarSlot->getWeeklySlots($hostId);
421
422 $items = $this->getEnabledSlots($weeklySlots);
423
424 // create range of each day slots from $items array above with $period minutes interval
425 $formattedSlots = [];
426 $days = array_keys($items);
427 foreach ($items as $day => &$slots) {
428 $daySlots = [];
429 foreach ($slots as $slot) {
430 $slot['end'] = ($slot['end'] == '00:00') ? '24:00' : $slot['end'];
431 $start = strtotime($slot['start']);
432 $end = strtotime($slot['end']);
433
434 while ($start + $period <= $end) {
435 $daySlots[] = gmdate('H:i', $start); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
436 $start += $interval;
437 }
438
439 if ($slot['end'] == '24:00' && $start < $end) {
440 $daySlots = $this->handleNextDaySlot($daySlots, $items, $end, $start, $interval, $period, $day, $days);
441 }
442 }
443 if ($daySlots) {
444 $formattedSlots[$day] = $daySlots;
445 }
446 }
447
448 return $formattedSlots;
449 }
450
451 private function getEnabledSlots($weeklySlots)
452 {
453 $items = [];
454
455 foreach ($weeklySlots as $weekDay => $weeklySlot) {
456 if ($weeklySlot['enabled'] || !empty($weeklySlot['slots'])) {
457 $items[$weekDay] = $weeklySlot['slots'];
458 }
459 }
460
461 return $items;
462 }
463
464 protected function handleNextDaySlot($daySlots, &$items, $start, $end, $interval, $period, $day, $days)
465 {
466 $nextDayIndex = array_search($day, $days) + 1;
467
468 if (isset($days[$nextDayIndex])) {
469 $nextDay = $items[$days[$nextDayIndex]];
470
471 if ($nextDay && $nextDay[0]['start'] == '00:00') {
472 $nextDayEnd = strtotime($nextDay[0]['end']);
473 $reserveTime = $end - $start;
474
475 while ($period - $reserveTime <= $nextDayEnd) {
476 $startTime = gmdate('H:i', $start); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
477 $nextDayStart = gmdate('H:i', $interval - $reserveTime); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
478 $daySlots[] = $startTime;
479
480 if ($nextDayStart < $startTime) {
481 $items[$days[$nextDayIndex]][0]['start'] = $nextDayStart;
482 break;
483 }
484
485 $start += $interval;
486 $reserveTime = $end - $start;
487 }
488 }
489 }
490
491 return $daySlots;
492 }
493
494 protected function maybeDateOverrides($dateOverrides, $availableSlots, $date, $duration)
495 {
496 if (!$dateOverrides) {
497 return $availableSlots;
498 }
499
500 list($overrideSlots, $overrideDays) = $dateOverrides;
501
502 if ($overrideDays && isset($overrideDays[$date])) {
503 $availableSlots = $this->removeOverrideSlots($availableSlots, $overrideDays[$date]);
504 }
505
506 if ($overrideSlots && isset($overrideSlots[$date])) {
507 $flatOverrideSlots = $this->convertSlotSetsToFlat($overrideSlots, $date, $duration);
508 $availableSlots = array_merge($availableSlots, $flatOverrideSlots);
509 $availableSlots = $this->sortDaySlots($availableSlots);
510 }
511
512 return $availableSlots;
513 }
514
515 protected function convertSlotSetsToFlat(&$overrideSlots, $date, $duration = null)
516 {
517 $period = $this->calendarSlot->getDuration($duration) * 60;
518
519 $interval = $this->calendarSlot->getSlotInterval($duration) * 60;
520
521 $formattedSlots = [];
522
523 $slotSets = $overrideSlots[$date];
524
525 foreach ($slotSets as $slot) {
526 $slot['end'] = ($slot['end'] == '00:00') ? '24:00' : $slot['end'];
527 $start = strtotime($slot['start']);
528 $end = strtotime($slot['end']);
529
530 while ($start + $period <= $end) {
531 $formattedSlots[] = gmdate('H:i', $start); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
532 $start += $interval;
533 }
534
535 if ($slot['end'] == '24:00' && $start < $end) {
536 $formattedSlots = $this->handleNextDayOverrideSlot($formattedSlots, $start, $end, $interval, $period, $overrideSlots, $date);
537 }
538 }
539
540 return $formattedSlots;
541 }
542
543 protected function handleNextDayOverrideSlot($formattedSlots, $start, $end, $interval, $period, &$overrideSlots, $date)
544 {
545 $nextDayIndex = gmdate('Y-m-d', strtotime($date) + 86400); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
546
547 if (isset($overrideSlots[$nextDayIndex])) {
548 $nextDay = $overrideSlots[$nextDayIndex];
549
550 if ($nextDay && $nextDay[0]['start'] == '00:00') {
551 $nextDayEnd = strtotime($nextDay[0]['end']);
552 $reserveTime = $end - $start;
553
554 while ($period - $reserveTime <= $nextDayEnd) {
555 $startTime = gmdate('H:i', $start); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
556 $nextDayStart = gmdate('H:i', $interval - $reserveTime); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
557 $formattedSlots[] = $startTime;
558
559 if ($startTime > $nextDayStart) {
560 $overrideSlots[$nextDayIndex][0]['start'] = $nextDayStart;
561 break;
562 }
563
564 $start += $interval;
565 $reserveTime = $end - $start;
566 }
567 }
568 }
569
570 return $formattedSlots;
571 }
572
573 protected function removeOverrideSlots($availableSlots, $overrideDay)
574 {
575 if (!$availableSlots || !$overrideDay) {
576 return $availableSlots;
577 }
578
579 $startTime = strtotime($overrideDay['start']);
580 $endTime = strtotime($overrideDay['end']);
581
582 $filteredSlots = array_filter($availableSlots, function ($slot) use ($startTime, $endTime) {
583 return strtotime($slot) < $startTime || strtotime($slot) >= $endTime;
584 });
585
586 return $filteredSlots;
587 }
588
589 public function getAvailableSpots($startDate, $timeZone = 'UTC', $duration = null, $hostId = null)
590 {
591 $this->hostId = $hostId;
592
593 $event = $this->calendarSlot;
594 $calendar = $this->calendar;
595 $duration = $event->getDuration($duration);
596
597 $startDate = $this->adjustStartDate($startDate, $timeZone);
598
599 $isDisplaySpots = $event->is_display_spots;
600 $isMultiGuest = $event->isMultiGuestEvent();
601 $endDate = $event->getMaxBookableDateTime($startDate, $timeZone);
602 $startDate = $event->getMinBookableDateTime($startDate, $timeZone);
603
604 $maxBooking = false;
605 if ($isDisplaySpots && $isMultiGuest) {
606 $maxBooking = $event->getMaxBookingPerSlot();
607 }
608
609 if (strtotime($startDate) > strtotime($endDate)) {
610 return new \WP_Error('invalid_date_range', __('Invalid date range', 'fluent-booking'));
611 }
612
613 $slots = $this->getDates($startDate, $endDate, $duration, false, $timeZone);
614
615 return $this->convertSpots($slots, $startDate, 'UTC', $timeZone, $maxBooking);
616 }
617
618 protected function adjustStartDate($startDate, $timeZone)
619 {
620 $requestedDate = $startDate;
621
622 $startDate = DateTimeHelper::convertToUtc($startDate, $timeZone);
623 $currentDateTime = gmdate('Y-m-d H:i:s'); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
624
625 if (strtotime($startDate) < strtotime($currentDateTime)) {
626 $startDate = $currentDateTime;
627 }
628
629 // Extract month and year from the timezone converted start date and requested date
630 list($startDateMonth, $startDateYear) = $this->extractMonthAndYear($startDate);
631 list($requestedDateMonth, $requestedDateYear) = $this->extractMonthAndYear($requestedDate);
632
633 if ($startDateYear < $requestedDateYear || $startDateMonth < $requestedDateMonth) {
634 $startDate = gmdate('Y-m-01 00:00:00', strtotime($requestedDate)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
635 }
636
637 return $startDate;
638 }
639
640 protected function convertSpots($slots, $startDate, $fromTimeZone = 'UTC', $toTimeZone = 'UTC', $maxBooking = false)
641 {
642 $minBookableTimestamp = strtotime($startDate);
643
644 $convertedSpots = [];
645 foreach ($slots as $spots) {
646 foreach ($spots as $spot) {
647 $start = $spot['start'];
648 $end = $spot['end'];
649
650 if (strtotime($start) < $minBookableTimestamp) {
651 continue;
652 }
653
654 if ($fromTimeZone != $toTimeZone) {
655 $start = DateTimeHelper::convertToTimeZone($spot['start'], 'UTC', $toTimeZone);
656 $end = DateTimeHelper::convertToTimeZone($spot['end'], 'UTC', $toTimeZone);
657 }
658
659 $spotStartDate = gmdate('Y-m-d', strtotime($start)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
660
661 $convertedSpots[$spotStartDate] = $convertedSpots[$spotStartDate] ?? [];
662
663 $remainingSlots = $maxBooking ? Arr::get($spot, 'remaining', $maxBooking) : false;
664
665 $convertedSpots[$spotStartDate][$start] = [
666 'start' => $start,
667 'end' => $end,
668 'remaining' => $remainingSlots,
669 ];
670 }
671 }
672
673 $convertedSpots = array_map(function ($spots) {
674 return array_values($spots);
675 }, $convertedSpots);
676
677 return $convertedSpots;
678 }
679
680 private function extractMonthAndYear($date)
681 {
682 $month = gmdate('m', strtotime($date)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
683 $year = gmdate('Y', strtotime($date)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
684 return [$month, $year];
685 }
686
687 protected function createDateRangeArrayFromSlotConfig($slotConfig = [])
688 {
689 if (empty($slotConfig['start']) || empty($slotConfig['end'])) {
690 return [];
691 }
692
693 $startTime = $slotConfig['start'];
694 $endTime = $slotConfig['end'];
695 if (gmdate('Ymd', strtotime($startTime)) == gmdate('Ymd', strtotime($endTime))) {
696 return [
697 gmdate('Y-m-d', strtotime($startTime)) => $this->bookSlot(Arr::get($slotConfig, 'event_id'), $startTime, $endTime, Arr::get($slotConfig, 'remaining'), Arr::get($slotConfig, 'source'))
698 ];
699 }
700
701 $start = new \DateTime($startTime);
702 $end = new \DateTime($endTime);
703
704 // Set the end time to the end of the day if it's set to the beginning of a day
705 if ($end->format('H:i:s') === '00:00:00') {
706 $end->modify('-1 second'); // This will set the time to 23:59:59 of the previous day
707 }
708
709 $interval = new \DateInterval('P1D');
710 $dateRange = new \DatePeriod($start, $interval, $end);
711
712 $rangeArray = [];
713 foreach ($dateRange as $date) {
714 $dateKey = $date->format('Y-m-d');
715
716 if ($date->format('Y-m-d') === $start->format('Y-m-d')) {
717 $rangeArray[$dateKey] = $this->bookSlot(Arr::get($slotConfig, 'event_id'), $startTime, $date->format('Y-m-d 23:59:59'), Arr::get($slotConfig, 'remaining'), Arr::get($slotConfig, 'source'));
718 } elseif ($date->format('Y-m-d') === $end->format('Y-m-d')) {
719 $rangeArray[$dateKey] = $this->bookSlot(Arr::get($slotConfig, 'event_id'), $date->format('Y-m-d 00:00:00'), $endTime, Arr::get($slotConfig, 'remaining'), Arr::get($slotConfig, 'source'));
720 } else {
721 $rangeArray[$dateKey] = $this->bookSlot(Arr::get($slotConfig, 'event_id'), $date->format('Y-m-d 00:00:00'), $date->format('Y-m-d 23:59:59'), Arr::get($slotConfig, 'remaining'), Arr::get($slotConfig, 'source'));
722 }
723 }
724
725 // Add the last day if it was not included in the loop
726 if ($end->format('Y-m-d') !== $start->format('Y-m-d')) {
727 $lastDayKey = $end->format('Y-m-d');
728 $rangeArray[$lastDayKey] = $this->bookSlot(Arr::get($slotConfig, 'event_id'), $end->format('Y-m-d 00:00:00'), $endTime, Arr::get($slotConfig, 'remaining'), Arr::get($slotConfig, 'source'));
729 }
730
731 return $rangeArray;
732 }
733
734 private function maybeBookingFrequencyLimitRanges($ranges, $bookedSlots)
735 {
736 if (!$ranges) {
737 return $ranges;
738 }
739
740 $isBookingFrequencyEnabled = !!Arr::get($this->calendarSlot->settings, 'booking_frequency.enabled');
741 if (!$isBookingFrequencyEnabled) {
742 return $ranges;
743 }
744
745 $keyedFrequenceyLimits = [];
746 $frequenceyLimits = Arr::get($this->calendarSlot->settings, 'booking_frequency.limits', []);
747 foreach ($frequenceyLimits as $limit) {
748 if (!empty($limit['value'])) {
749 $keyedFrequenceyLimits[$limit['unit']] = $limit['value'];
750 }
751 }
752
753 // Per Month Booking Frequency Limit Hanlder
754 if (!empty($keyedFrequenceyLimits['per_month'])) {
755 $startDate = gmdate('Y-m-01 00:00:00', strtotime(min($ranges)));
756 $endDate = gmdate('Y-m-t 23:59:59', strtotime(min($ranges)));
757
758 $monthlyLimit = (int)$keyedFrequenceyLimits['per_month'];
759
760 $monthlyCount = $this->getBookingsTotal($startDate, $endDate);
761
762 if ($monthlyCount >= $monthlyLimit) {
763 return [];
764 }
765 }
766
767 // Per Week Booking Frequency Limit Hanlder
768 if (!empty($keyedFrequenceyLimits['per_week'])) {
769
770 if (!$ranges) {
771 return [];
772 }
773
774 $weeklyLimit = (int)$keyedFrequenceyLimits['per_week'];
775 $filledWeeks = $this->getFilledWeeks(min($ranges), max($ranges));
776 foreach ($filledWeeks as $filledWeek) {
777
778 $weeklyCount = $this->getBookingsTotal($filledWeek[0] . ' 00:00:00', $filledWeek[6] . ' 23:59:59');
779
780 if ($weeklyCount >= $weeklyLimit) {
781 $ranges = array_filter($ranges, function ($rangeDate) use ($filledWeek) {
782 return !in_array($rangeDate, $filledWeek);
783 });
784
785 if (!$ranges) {
786 return [];
787 }
788 }
789 }
790 }
791
792 // Per Day Booking Frequency Limit Hanlder
793 if (!empty($keyedFrequenceyLimits['per_day'])) {
794 $perDayLimit = $keyedFrequenceyLimits['per_day'];
795 foreach ($ranges as $rangeIndex => $rangeDate) {
796 if (!isset($bookedSlots[$rangeDate])) {
797 continue;
798 }
799
800 $dayBooked = array_filter($bookedSlots[$rangeDate], function ($slot) {
801 return Arr::get($slot, 'event_id') == $this->calendarSlot->id;
802 });
803
804 if (!$dayBooked) {
805 continue;
806 }
807
808 if (count($dayBooked) >= $perDayLimit) {
809 unset($ranges[$rangeIndex]);
810 }
811
812 if (!$ranges) {
813 return [];
814 }
815 }
816 }
817
818 return $ranges;
819 }
820
821 private function maybeBookingDurationLimitRanges($ranges, $bookedSlots, $duration)
822 {
823 if (!$ranges) {
824 return $ranges;
825 }
826
827 if (!Arr::get($this->calendarSlot->settings, 'booking_duration.enabled')) {
828 return $ranges;
829 }
830
831 $limits = Arr::get($this->calendarSlot->settings, 'booking_duration.limits', []);
832
833 $keyedLimits = [];
834 foreach ($limits as $limit) {
835 if (!empty($limit['value'])) {
836 $keyedLimits[$limit['unit']] = (int)$limit['value'];
837 }
838 }
839
840 // Per Month Booking Frequency Limit Hanlder
841 if (!empty($keyedLimits['per_month'])) {
842 $startDate = gmdate('Y-m-01 00:00:00', strtotime(min($ranges)));
843 $endDate = gmdate('Y-m-t 23:59:59', strtotime(min($ranges)));
844
845 $monthlyDuration = $this->getBookingDurationTotal($startDate, $endDate);
846
847 if ($monthlyDuration + $duration > $keyedLimits['per_month']) {
848 $ranges = [];
849 }
850 }
851
852 // Per Week Booking Frequency Limit Hanlder
853 if (!empty($keyedLimits['per_week'])) {
854 $weeklyLimit = (int)$keyedLimits['per_week'];
855 $filledWeeks = $this->getFilledWeeks(min($ranges), max($ranges));
856 foreach ($filledWeeks as $filledWeek) {
857 $weeklyDuration = $this->getBookingDurationTotal($filledWeek[0] . ' 00:00:00', $filledWeek[6] . ' 23:59:59');
858
859 if ($weeklyDuration + $duration > $weeklyLimit) {
860 $ranges = array_filter($ranges, function ($rangeDate) use ($filledWeek) {
861 return !in_array($rangeDate, $filledWeek);
862 });
863
864 if (!$ranges) {
865 return [];
866 }
867 }
868 }
869 }
870
871 // Per Day Booking Frequency Limit Hanlder
872 if (!empty($keyedLimits['per_day'])) {
873 $perDayLimit = $keyedLimits['per_day'];
874 foreach ($ranges as $rangeIndex => $rangeDate) {
875 if (!isset($bookedSlots[$rangeDate])) {
876 continue;
877 }
878
879 $dayDurarion = array_reduce($bookedSlots[$rangeDate], function ($carry, $slot) {
880 if (Arr::get($slot, 'event_id') == $this->calendarSlot->id) {
881 $carry += (int)((strtotime($slot['end']) - strtotime($slot['start'])) / 60);
882 }
883 return $carry;
884 }, 0);
885
886 if (!$dayDurarion) {
887 continue;
888 }
889
890 if ($dayDurarion + $duration > $perDayLimit) {
891 unset($ranges[$rangeIndex]);
892 }
893 }
894 }
895
896 return $ranges;
897 }
898
899 public function getFilledWeeks($from, $to, $weekStart = '')
900 {
901 $weekStart = $weekStart ? $weekStart : Arr::get(Helper::getGlobalSettings(), 'administration.start_day', 'sun');
902
903 $startDate = new DateTime($from);
904 $endDate = new DateTime($to);
905
906 if (strtolower($startDate->format('D')) != $weekStart) {
907 $startDate->modify('last ' . $weekStart);
908 }
909
910 $weeks = [];
911
912 while ($startDate <= $endDate) {
913 // get all days in this week
914 $week = [];
915 for ($i = 0; $i < 7; $i++) {
916 $week[] = $startDate->format('Y-m-d');
917 $startDate->modify('+1 day');
918 }
919 $weeks[] = $week;
920 }
921
922 return $weeks;
923 }
924
925 protected function getBookingsTotal($start, $end)
926 {
927 if ($this->calendarSlot->event_type == 'group') {
928 return Booking::query()
929 ->where('event_id', $this->calendarSlot->id)
930 ->whereBetween('start_time', [$start, $end])
931 ->whereIn('status', ['scheduled', 'completed'])
932 ->groupBy('group_id')
933 ->count();
934 }
935
936 return Booking::query()
937 ->where('event_id', $this->calendarSlot->id)
938 ->whereBetween('start_time', [$start, $end])
939 ->whereIn('status', ['scheduled', 'completed'])
940 ->count();
941 }
942
943 protected function getBookingDurationTotal($start, $end)
944 {
945 if ($this->calendarSlot->event_type == 'group') {
946 return Booking::query()
947 ->select(['group_id', 'slot_minutes'])
948 ->where('event_id', $this->calendarSlot->id)
949 ->whereBetween('start_time', [$start, $end])
950 ->whereIn('status', ['scheduled', 'completed'])
951 ->groupBy('group_id')
952 ->get()
953 ->sum('slot_minutes');
954 }
955
956 return Booking::query()
957 ->where('event_id', $this->calendarSlot->id)
958 ->whereBetween('start_time', [$start, $end])
959 ->whereIn('status', ['scheduled', 'completed'])
960 ->sum('slot_minutes');
961 }
962
963 protected function getTimezoneInfo($hostId = null)
964 {
965 $hostId = $hostId ?: $this->hostId;
966
967 $scheduleTimezone = $this->calendarSlot->getScheduleTimezone($hostId);
968
969 $dstTime = DateTimeHelper::getDaylightSavingTime($scheduleTimezone);
970
971 return [$scheduleTimezone, $dstTime];
972 }
973
974 protected function maybeDayLightSavingSlot($slot, $dstTime, $scheduleTimezone, $adjustSign = '-')
975 {
976 if (!$dstTime) {
977 return $slot;
978 }
979
980 $slot['start'] = $this->maybeDayLightSavingTime($slot['start'], $dstTime, $scheduleTimezone, $adjustSign);
981 $slot['end'] = $this->maybeDayLightSavingTime($slot['end'], $dstTime, $scheduleTimezone, $adjustSign);
982
983 return $slot;
984 }
985
986 protected function maybeDayLightSavingTime($time, $dstTime, $timezone, $adjustSign = '+')
987 {
988 $scheduleTime = DateTimeHelper::convertToTimeZone($time, 'UTC', $timezone);
989 if (DateTimeHelper::isDaylightSavingActive($scheduleTime, $timezone)) {
990 $time = gmdate('Y-m-d H:i:s', strtotime($time . " $adjustSign $dstTime minutes")); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
991 }
992
993 return $time;
994 }
995
996 protected function isLocalBooking($bookings, $slot)
997 {
998 if (empty($bookings)) {
999 return false;
1000 }
1001
1002 foreach ($bookings as $book) {
1003 if ($book['start'] == $slot['start'] && $book['end'] == $slot['end']) {
1004 return true;
1005 }
1006 }
1007
1008 return false;
1009 }
1010
1011 protected function mergeAndSortSlots($currentSlots, $validSlots)
1012 {
1013 if (!$currentSlots) {
1014 return $validSlots;
1015 }
1016
1017 $mergedSlots = array_merge($currentSlots, $validSlots);
1018
1019 usort($mergedSlots, function ($a, $b) {
1020 return strtotime($a['start']) - strtotime($b['start']);
1021 });
1022
1023 return $mergedSlots;
1024 }
1025
1026 protected function sortDaySlots($daySlots)
1027 {
1028 usort($daySlots, function ($a, $b) {
1029 return strtotime($a) - strtotime($b);
1030 });
1031
1032 return $daySlots;
1033 }
1034 }
1035