PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 1.5.01
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v1.5.01
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.01, at app/Services/TimeSlotService.php

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