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

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