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

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