PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.2.0
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.2.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 2.2.0, at app/Services/TimeSlotService.php

1,110 lines 40.4 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);
38 $ranges = $this->maybeBookingDurationLimitRanges($ranges, $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 $rangedSlots = $this->maybeBookingPerDayLimitSlots($rangedSlots, $bookedSlots, $duration);
49
50 return $rangedSlots;
51 }
52
53 protected function getRangedValidSlots($ranges, $duration, $bookedSlots, $cutOutTime, $maxBookingTime, $timezoneInfo, $rangedSlots = [], $hostId = null)
54 {
55 $period = $duration * 60;
56
57 $hostId = $hostId ?: $this->hostId;
58
59 $bufferTime = $this->calendarSlot->getTotalBufferTime() * 60;
60
61 $daySlots = $this->getWeekDaySlots($duration, $hostId);
62
63 $dateOverrides = $this->calendarSlot->getDateOverrides($hostId);
64
65 list($scheduleTimezone, $dstTime) = $timezoneInfo;
66
67 $todayDate = gmdate('Y-m-d'); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
68
69 $lastDate = end($ranges);
70
71 foreach ($ranges as $date) {
72 $day = strtolower(gmdate('D', strtotime($date))); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
73
74 $availableSlots = $daySlots[$day] ?? [];
75
76 $availableSlots = $this->maybeDateOverrides($dateOverrides, $availableSlots, $date, $duration);
77
78 if (!$availableSlots) {
79 continue;
80 }
81
82 $isToday = $date === $todayDate;
83
84 $isLastDay = $date === $lastDate;
85
86 $validSlots = [];
87
88 $validDstDateSlots = [];
89
90 foreach ($availableSlots as $start) {
91 $end = gmdate('H:i', strtotime($start) + $period); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
92 $endDate = $start < $end ? $date : gmdate('Y-m-d', strtotime($date) + 86400); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
93
94 $slot = [
95 'start' => $date . ' ' . $start . ':00',
96 'end' => $endDate . ' ' . $end . ':00'
97 ];
98
99 $slot = $this->maybeDayLightSavingSlot($slot, $dstTime, $scheduleTimezone);
100
101 $slotDate = gmdate('Y-m-d', strtotime($slot['start'])); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
102
103 $currentBookedSlots = $bookedSlots[$slotDate] ?? [];
104
105 if ($isToday && strtotime($slot['start']) < $cutOutTime) {
106 continue;
107 }
108
109 if ($isLastDay && strtotime($slot['end']) > $maxBookingTime) {
110 break;
111 }
112
113 $isSlotAvailable = $this->isSlotAvailable($slot, $currentBookedSlots, $bufferTime, $hostId);
114
115 if ($isSlotAvailable) {
116 if ($slotDate != $date) {
117 $validDstDateSlots[] = $slot;
118 } else {
119 $validSlots[] = $slot;
120 }
121 }
122 }
123
124 if ($validSlots) {
125 $currentSlots = $rangedSlots[$date] ?? [];
126 $rangedSlots[$date] = $this->mergeAndSortSlots($currentSlots, $validSlots);
127 }
128
129 if ($validDstDateSlots) {
130 $slotDate = gmdate('Y-m-d', strtotime($validDstDateSlots[0]['start'])); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
131 $currentSlots = $rangedSlots[$slotDate] ?? [];
132 $rangedSlots[$slotDate] = $this->mergeAndSortSlots($currentSlots, $validDstDateSlots);
133 }
134 }
135
136 return $rangedSlots;
137 }
138
139 protected function isSlotAvailable(&$slot, $currentBookedSlots, $bufferTime, $hostId)
140 {
141 if (!$currentBookedSlots) {
142 return true;
143 }
144
145 $startTimeStamp = strtotime($slot['start']);
146 $endTimeStamp = strtotime($slot['end']);
147
148 foreach ($currentBookedSlots as $bookedSlot) {
149 $bookedStart = strtotime($bookedSlot['start']);
150 $bookedEnd = strtotime($bookedSlot['end']);
151
152 if (Arr::get($bookedSlot, 'source')) {
153 $bookedStart = $bookedStart - $bufferTime;
154 $bookedEnd = $bookedEnd + $bufferTime;
155 }
156
157 if (
158 ($startTimeStamp >= $bookedStart && $startTimeStamp < $bookedEnd) ||
159 ($endTimeStamp > $bookedStart && $endTimeStamp <= $bookedEnd) ||
160 ($startTimeStamp <= $bookedStart && $endTimeStamp > $bookedStart) ||
161 ($startTimeStamp < $bookedEnd && $endTimeStamp >= $bookedEnd)
162 ) {
163 if (!Arr::get($bookedSlot, 'remaining')) {
164 return false;
165 }
166 $slot['remaining'] = $bookedSlot['remaining'];
167 }
168 }
169
170 return true;
171 }
172
173 public function isSpotAvailable($fromTime, $toTime, $duration = null, $hostId = null)
174 {
175 $this->hostId = $hostId;
176
177 $fromTimeStamp = strtotime($fromTime);
178 $toTimeStamp = strtotime($toTime);
179
180 $duration = $this->calendarSlot->getDuration($duration);
181
182 list($scheduleTimezone, $dstTime) = $this->getTimezoneInfo();
183
184 $fromStartTime = $this->maybeDayLightSavingTime($fromTime, $dstTime, $scheduleTimezone);
185 $toEndTime = $this->maybeDayLightSavingTime($toTime, $dstTime, $scheduleTimezone);
186
187 $fromTime = gmdate('Y-m-d 00:00:00', strtotime($fromStartTime)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
188 $toTime = gmdate('Y-m-d 23:59:59', strtotime($toEndTime)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
189
190 $slots = $this->getDates($fromTime, $toTime, $duration, true);
191
192 $fromDate = gmdate('Y-m-d', $fromTimeStamp); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
193 $toDate = gmdate('Y-m-d', $toTimeStamp); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
194
195 $availableSlots = $slots[$fromDate] ?? [];
196
197 if ($fromDate != $toDate) {
198 $availableSlots = array_merge($availableSlots, $slots[$toDate] ?? []);
199 }
200
201 return $this->isSlotExists($availableSlots, $fromTimeStamp, $toTimeStamp);
202 }
203
204 protected function isSlotExists($availableSlots, $fromTimeStamp, $toTimeStamp)
205 {
206 $left = 0;
207 $right = count($availableSlots) - 1;
208
209 while ($left <= $right) {
210 $mid = $left + (($right - $left) >> 1);
211 $slot = $availableSlots[$mid];
212
213 $slotStartTime = strtotime($slot['start']);
214 $slotEndTime = strtotime($slot['end']);
215
216 if ($fromTimeStamp == $slotStartTime && $toTimeStamp == $slotEndTime) {
217 return $slot;
218 } elseif ($fromTimeStamp > $slotStartTime) {
219 $left = $mid + 1;
220 } else {
221 $right = $mid - 1;
222 }
223 }
224
225 return false;
226 }
227
228 protected function getCurrentDateRange($startDate = false, $endDate = false)
229 {
230 if (!$startDate) {
231 $startDate = gmdate('Y-m-d'); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
232 }
233
234 if (!$endDate) {
235 $endDate = gmdate('Y-m-t 23:59:59', strtotime($startDate)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
236 }
237
238 $currentDate = strtotime($startDate);
239 $endDate = strtotime($endDate) + 1; // add 1s in case end is 23:59:59
240 $oneDay = 24 * 60 * 60;
241
242 $dateArray = [];
243
244 while ($currentDate <= $endDate) {
245 $dateArray[] = gmdate('Y-m-d', $currentDate); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
246 $currentDate += $oneDay;
247 }
248
249 return $dateArray;
250 }
251
252 protected function bookSlot($eventId, $start, $end, $remaining = 0, $source = null)
253 {
254 return [
255 'event_id' => $eventId,
256 'start' => $start,
257 'end' => $end,
258 'remaining' => $remaining,
259 'source' => $source
260 ];
261 }
262
263 protected function getBookedSlots($dateRange, $toTimeZone = 'UTC', $isDoingBooking = false)
264 {
265 if ($toTimeZone != 'UTC') {
266 $dateRange[0] = DateTimeHelper::convertToUtc($dateRange[0], $toTimeZone);
267 $dateRange[1] = DateTimeHelper::convertToUtc($dateRange[1], $toTimeZone);
268 }
269
270 $hostIds = $this->calendarSlot->getHostIds($this->hostId);
271 $status = ['pending', 'reserved', 'approved', 'scheduled', 'completed'];
272
273 $bookings = Booking::with(['calendar_event'])
274 ->whereHas('hosts', function ($query) use ($hostIds) {
275 $query->whereIn('user_id', $hostIds);
276 })
277 ->where(function ($query) use ($dateRange) {
278 $query->whereBetween('start_time', $dateRange)
279 ->orWhereBetween('end_time', $dateRange);
280 })
281 ->orderBy('start_time', 'ASC')
282 ->whereIn('status', $status)
283 ->get()
284 ->groupBy('group_id');
285
286 $maxBooking = $this->calendarSlot->getMaxBookingPerSlot();
287
288 $isGroupBooking = $maxBooking > 1;
289
290 $books = $this->processBookings($bookings, $toTimeZone, $maxBooking, $isGroupBooking);
291
292 $books = apply_filters('fluent_booking/local_booked_events', $books, $this->calendarSlot, $toTimeZone, $dateRange, $isDoingBooking);
293
294 $remoteBookings = apply_filters('fluent_booking/remote_booked_events', [], $this->calendarSlot, $toTimeZone, $dateRange, $this->hostId, $isDoingBooking);
295
296 $books = $this->processRemoteBookings($books, $remoteBookings);
297
298 return apply_filters('fluent_booking/booked_events', $books, $this->calendarSlot, $toTimeZone, $dateRange, $isDoingBooking);
299 }
300
301 protected function processBookings($bookings, $toTimeZone, $maxBooking, $isGroupBooking)
302 {
303 $books = [];
304 foreach ($bookings as $booking) {
305 $booked = $booking->count();
306 $booking = $booking[0];
307
308 $remaining = 0;
309 if ($this->calendarSlot->id == $booking->event_id) {
310 if ($booking->status == 'reserved') {
311 continue;
312 }
313 $remaining = max(0, $maxBooking - $booked);
314 }
315
316 if ($toTimeZone != 'UTC') {
317 $booking->start_time = DateTimeHelper::convertToTimeZone($booking->start_time, 'UTC', $toTimeZone);
318 $booking->end_time = DateTimeHelper::convertToTimeZone($booking->end_time, 'UTC', $toTimeZone);
319 }
320
321 $date = gmdate('Y-m-d', strtotime($booking->start_time)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
322
323 $books[$date] = $books[$date] ?? [];
324
325 $bufferTime = $booking->calendar_event->getTotalBufferTime();
326 if ($bufferTime) {
327 $beforeBufferTime = gmdate('Y-m-d H:i:s', strtotime($booking->start_time . " -$bufferTime minutes")); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
328 $afterBufferTime = gmdate('Y-m-d H:i:s', strtotime($booking->end_time . " +$bufferTime minutes")); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
329 if ($remaining) {
330 if ($beforeBufferTime < $booking->start_time) {
331 $books[$date][] = $this->bookSlot(null, $beforeBufferTime, $booking->start_time);
332 }
333 if ($afterBufferTime > $booking->end_time) {
334 $books[$date][] = $this->bookSlot(null, $booking->end_time, $afterBufferTime);
335 }
336 } else {
337 $booking->start_time = $beforeBufferTime;
338 $booking->end_time = $afterBufferTime;
339 }
340 }
341
342 $rangedItems = $this->createDateRangeArrayFromSlotConfig([
343 'event_id' => $booking->event_id,
344 'start' => $booking->start_time,
345 'end' => $booking->end_time,
346 'remaining' => $remaining
347 ]);
348
349 $eventIdAdded = false;
350 foreach ($rangedItems as $date => $slot) {
351 if ($isGroupBooking && $remaining && $this->calendarSlot->id == $booking->event_id) {
352 if ($eventIdAdded) {
353 $slot['event_id'] = null;
354 }
355 $eventIdAdded = true;
356 }
357 $books[$date] = $books[$date] ?? [];
358 $books[$date][] = $slot;
359 }
360 }
361
362 return $books;
363 }
364
365 protected function processRemoteBookings($books, $remoteBookings)
366 {
367 if (!$remoteBookings) {
368 return $books;
369 }
370
371 foreach ($remoteBookings as $slot) {
372 $rangedItems = $this->createDateRangeArrayFromSlotConfig([
373 'start' => $slot['start'],
374 'end' => $slot['end'],
375 'source' => $slot['source']
376 ]);
377
378 foreach ($rangedItems as $rangedDate => $rangedSlot) {
379 $books[$rangedDate] = $books[$rangedDate] ?? [];
380
381 if (!$this->isLocalBooking($books[$rangedDate], $rangedSlot)) {
382 $books[$rangedDate][] = $rangedSlot;
383 }
384 }
385 }
386 return $books;
387 }
388
389 protected function getWeekDaySlots($duration, $hostId = null)
390 {
391 $period = $duration * 60;
392
393 $hostId = $hostId ?: $this->hostId;
394
395 $interval = $this->calendarSlot->getSlotInterval($duration) * 60;
396
397 $weeklySlots = $this->calendarSlot->getWeeklySlots($hostId);
398
399 $items = $this->getEnabledSlots($weeklySlots);
400
401 // create range of each day slots from $items array above with $period minutes interval
402 $formattedSlots = [];
403 $days = array_keys($items);
404 foreach ($items as $day => &$slots) {
405 $daySlots = [];
406 foreach ($slots as $slot) {
407 $slot['end'] = ($slot['end'] == '00:00') ? '24:00' : $slot['end'];
408 $start = strtotime($slot['start']);
409 $end = strtotime($slot['end']);
410
411 while ($start + $period <= $end) {
412 $daySlots[] = gmdate('H:i', $start); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
413 $start += $interval;
414 }
415
416 if ($slot['end'] == '24:00' && $start < $end) {
417 $daySlots = $this->handleNextDaySlot($daySlots, $items, $start, $end, $interval, $period, $day, $days);
418 }
419 }
420 if ($daySlots) {
421 $formattedSlots[$day] = $daySlots;
422 }
423 }
424
425 return $formattedSlots;
426 }
427
428 private function getEnabledSlots($weeklySlots)
429 {
430 $items = [];
431
432 foreach ($weeklySlots as $weekDay => $weeklySlot) {
433 if ($weeklySlot['enabled'] || !empty($weeklySlot['slots'])) {
434 $items[$weekDay] = $weeklySlot['slots'];
435 }
436 }
437
438 return $items;
439 }
440
441 protected function handleNextDaySlot($daySlots, &$items, $start, $end, $interval, $period, $day, $days)
442 {
443 $nextDayIndex = (array_search($day, $days) + 1) % count($items);
444
445 if (isset($days[$nextDayIndex])) {
446 $nextDay = $items[$days[$nextDayIndex]];
447
448 if ($nextDay && $nextDay[0]['start'] == '00:00') {
449 $nextDayEndTime = strtotime($nextDay[0]['end']) - strtotime($nextDay[0]['start']);
450 $reserveTime = $end - $start;
451
452 while ($period - $reserveTime <= $nextDayEndTime) {
453 $startTime = gmdate('H:i', $start); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
454 $nextDayStart = gmdate('H:i', $interval - $reserveTime); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
455 $daySlots[] = $startTime;
456
457 if ($nextDayStart < $startTime) {
458 $items[$days[$nextDayIndex]][0]['start'] = $nextDayStart;
459 break;
460 }
461
462 $start += $interval;
463 $reserveTime = $end - $start;
464 }
465 }
466 }
467
468 return $daySlots;
469 }
470
471 protected function maybeDateOverrides($dateOverrides, $availableSlots, $date, $duration)
472 {
473 if (!$dateOverrides) {
474 return $availableSlots;
475 }
476
477 list($overrideSlots, $overrideDays) = $dateOverrides;
478
479 if ($overrideDays && isset($overrideDays[$date])) {
480 $availableSlots = $this->removeOverrideSlots($availableSlots, $overrideDays[$date]);
481 }
482
483 if ($overrideSlots && isset($overrideSlots[$date])) {
484 $flatOverrideSlots = $this->convertSlotSetsToFlat($overrideSlots, $date, $duration);
485 $availableSlots = array_merge($availableSlots, $flatOverrideSlots);
486 $availableSlots = $this->sortDaySlots($availableSlots);
487 }
488
489 return $availableSlots;
490 }
491
492 protected function convertSlotSetsToFlat(&$overrideSlots, $date, $duration = null)
493 {
494 $period = $this->calendarSlot->getDuration($duration) * 60;
495
496 $interval = $this->calendarSlot->getSlotInterval($duration) * 60;
497
498 $formattedSlots = [];
499
500 $slotSets = $overrideSlots[$date];
501
502 foreach ($slotSets as $slot) {
503 $slot['end'] = ($slot['end'] == '00:00') ? '24:00' : $slot['end'];
504 $start = strtotime($slot['start']);
505 $end = strtotime($slot['end']);
506
507 while ($start + $period <= $end) {
508 $formattedSlots[] = gmdate('H:i', $start); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
509 $start += $interval;
510 }
511
512 if ($slot['end'] == '24:00' && $start < $end) {
513 $formattedSlots = $this->handleNextDayOverrideSlot($formattedSlots, $start, $end, $interval, $period, $overrideSlots, $date);
514 }
515 }
516
517 return $formattedSlots;
518 }
519
520 protected function handleNextDayOverrideSlot($formattedSlots, $start, $end, $interval, $period, &$overrideSlots, $date)
521 {
522 $nextDayIndex = gmdate('Y-m-d', strtotime($date) + 86400); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
523
524 if (isset($overrideSlots[$nextDayIndex])) {
525 $nextDay = $overrideSlots[$nextDayIndex];
526
527 if ($nextDay && $nextDay[0]['start'] == '00:00') {
528 $nextDayEndTime = strtotime($nextDay[0]['end']) - strtotime($nextDay[0]['start']);
529 $reserveTime = $end - $start;
530
531 while ($period - $reserveTime <= $nextDayEndTime) {
532 $startTime = gmdate('H:i', $start); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
533 $nextDayStart = gmdate('H:i', $interval - $reserveTime); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
534 $formattedSlots[] = $startTime;
535
536 if ($startTime > $nextDayStart) {
537 $overrideSlots[$nextDayIndex][0]['start'] = $nextDayStart;
538 break;
539 }
540
541 $start += $interval;
542 $reserveTime = $end - $start;
543 }
544 }
545 }
546
547 return $formattedSlots;
548 }
549
550 protected function removeOverrideSlots($availableSlots, $overrideDay)
551 {
552 if (!$availableSlots || !$overrideDay) {
553 return $availableSlots;
554 }
555
556 $startTime = strtotime($overrideDay['start']);
557 $endTime = strtotime($overrideDay['end']);
558
559 $filteredSlots = array_filter($availableSlots, function ($slot) use ($startTime, $endTime) {
560 return strtotime($slot) < $startTime || strtotime($slot) >= $endTime;
561 });
562
563 return $filteredSlots;
564 }
565
566 public function getAvailableSpots($startDate, $timeZone = 'UTC', $duration = null, $hostId = null)
567 {
568 $this->hostId = $hostId;
569
570 $event = $this->calendarSlot;
571 $duration = $event->getDuration($duration);
572
573 $adjustedDate = $this->adjustStartDate($startDate, $timeZone);
574
575 $isDisplaySpots = $event->is_display_spots;
576 $isMultiGuest = $event->isMultiGuestEvent();
577 $isMultiBooking = $event->isAdditionalGuestEnabled();
578 $endDate = $event->getMaxBookableDateTime($adjustedDate, $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(ksort($spots) ? $spots : $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)
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 return $ranges;
769 }
770
771 private function maybeBookingDurationLimitRanges($ranges, $duration)
772 {
773 if (!$ranges) {
774 return $ranges;
775 }
776
777 if (!Arr::get($this->calendarSlot->settings, 'booking_duration.enabled')) {
778 return $ranges;
779 }
780
781 $limits = Arr::get($this->calendarSlot->settings, 'booking_duration.limits', []);
782
783 $keyedLimits = [];
784 foreach ($limits as $limit) {
785 if (!empty($limit['value'])) {
786 $keyedLimits[$limit['unit']] = (int)$limit['value'];
787 }
788 }
789
790 // Per Month Booking Frequency Limit Hanlder
791 if (!empty($keyedLimits['per_month'])) {
792 $startDate = gmdate('Y-m-01 00:00:00', strtotime(min($ranges)));
793 $endDate = gmdate('Y-m-t 23:59:59', strtotime(min($ranges)));
794
795 $monthlyDuration = $this->getBookingDurationTotal($startDate, $endDate);
796
797 if ($monthlyDuration + $duration > $keyedLimits['per_month']) {
798 $ranges = [];
799 }
800 }
801
802 // Per Week Booking Frequency Limit Hanlder
803 if (!empty($keyedLimits['per_week'])) {
804 $weeklyLimit = (int)$keyedLimits['per_week'];
805 $filledWeeks = $this->getFilledWeeks(min($ranges), max($ranges));
806 foreach ($filledWeeks as $filledWeek) {
807 $weeklyDuration = $this->getBookingDurationTotal($filledWeek[0] . ' 00:00:00', $filledWeek[6] . ' 23:59:59');
808
809 if ($weeklyDuration + $duration > $weeklyLimit) {
810 $ranges = array_filter($ranges, function ($rangeDate) use ($filledWeek) {
811 return !in_array($rangeDate, $filledWeek);
812 });
813
814 if (!$ranges) {
815 return [];
816 }
817 }
818 }
819 }
820 return $ranges;
821 }
822
823 protected function maybeBookingPerDayLimitSlots($rangesSlots, $bookedSlots, $duration)
824 {
825 $isDurationEnabled = !!Arr::get($this->calendarSlot->settings, 'booking_duration.enabled');
826
827 $isFrequencyEnabled = !!Arr::get($this->calendarSlot->settings, 'booking_frequency.enabled');
828
829 if (!$isDurationEnabled && !$isFrequencyEnabled) {
830 return $rangesSlots;
831 }
832
833 $hostTimeZone = $this->calendarSlot->getScheduleTimezone($this->hostId);
834
835 $convertedRangesSlots = $this->convertSlotsByTimezone($rangesSlots, 'UTC', $hostTimeZone);
836
837 $convertedBookedSlots = $this->convertSlotsByTimezone($bookedSlots, 'UTC', $hostTimeZone);
838
839 $convertedRangesSlots = $this->maybeBookingDurationDayLimit($convertedRangesSlots, $convertedBookedSlots, $duration, $isDurationEnabled);
840
841 $convertedRangesSlots = $this->maybeBookingFrequencyDayLimit($convertedRangesSlots, $convertedBookedSlots, $isFrequencyEnabled);
842
843 $rangesSlots = $this->convertSlotsByTimezone($convertedRangesSlots, $hostTimeZone, 'UTC');
844
845 return $rangesSlots;
846 }
847
848 protected function maybeBookingDurationDayLimit($rangesSlots, $bookedSlots, $duration, $isEnabled)
849 {
850 if (!$isEnabled) {
851 return $rangesSlots;
852 }
853
854 $limits = Arr::get($this->calendarSlot->settings, 'booking_duration.limits', []);
855
856 $perDayLimit = null;
857 foreach ($limits as $limit) {
858 if (Arr::get($limit, 'unit') == 'per_day' && Arr::get($limit, 'value')) {
859 $perDayLimit = (int)Arr::get($limit, 'value');
860 break;
861 }
862 }
863
864 if (!$perDayLimit) {
865 return $rangesSlots;
866 }
867
868 $isMultiSlot = $this->calendarSlot->isMultiGuestEvent();
869
870 foreach ($rangesSlots as $rangeDate => &$slots) {
871 if (!isset($bookedSlots[$rangeDate])) {
872 continue;
873 }
874
875 $dayDuration = array_reduce($bookedSlots[$rangeDate], function ($carry, $slot) {
876 if (Arr::get($slot, 'event_id') == $this->calendarSlot->id) {
877 $carry += (int)((strtotime($slot['end']) - strtotime($slot['start'])) / 60);
878 }
879 return $carry;
880 }, 0);
881
882 if ($dayDuration + $duration > $perDayLimit) {
883 if ($isMultiSlot) {
884 $slots = array_values(array_filter($slots, function ($slot) {
885 return !empty(Arr::get($slot, 'remaining'));
886 }));
887 }
888 if (!$isMultiSlot || !count($slots)) {
889 unset($rangesSlots[$rangeDate]);
890 }
891 }
892 }
893
894 return $rangesSlots;
895 }
896
897 protected function maybeBookingFrequencyDayLimit($rangesSlots, $bookedSlots, $isEnabled)
898 {
899 if (!$isEnabled) {
900 return $rangesSlots;
901 }
902
903 $limits = Arr::get($this->calendarSlot->settings, 'booking_frequency.limits', []);
904
905 $perDayLimit = null;
906 foreach ($limits as $limit) {
907 if (Arr::get($limit, 'unit') == 'per_day' && Arr::get($limit, 'value')) {
908 $perDayLimit = (int)Arr::get($limit, 'value');
909 break;
910 }
911 }
912
913 if (!$perDayLimit) {
914 return $rangesSlots;
915 }
916
917 $isMultiSlot = $this->calendarSlot->isMultiGuestEvent();
918
919 foreach ($rangesSlots as $rangeDate => &$slots) {
920 if (!isset($bookedSlots[$rangeDate])) {
921 continue;
922 }
923
924 $dayBooked = array_filter($bookedSlots[$rangeDate], function ($slot) {
925 return Arr::get($slot, 'event_id') == $this->calendarSlot->id;
926 });
927
928 if (count($dayBooked) >= $perDayLimit) {
929 if ($isMultiSlot) {
930 $slots = array_values(array_filter($slots, function ($slot) {
931 return !empty(Arr::get($slot, 'remaining'));
932 }));
933 }
934 if (!$isMultiSlot || !count($slots)) {
935 unset($rangesSlots[$rangeDate]);
936 }
937 }
938 }
939
940 return $rangesSlots;
941 }
942
943 protected function convertSlotsByTimezone($slots, $fromTimeZone, $toTimeZone)
944 {
945 if ($fromTimeZone == $toTimeZone) {
946 return $slots;
947 }
948
949 $convertedSlots = [];
950
951 foreach ($slots as $spots) {
952 foreach ($spots as $spot) {
953 $spot['start'] = DateTimeHelper::convertToTimeZone($spot['start'], $fromTimeZone, $toTimeZone);
954 $spot['end'] = DateTimeHelper::convertToTimeZone($spot['end'], $fromTimeZone, $toTimeZone);
955
956 $spotDate = gmdate('Y-m-d', strtotime($spot['start']));
957
958 $convertedSlots[$spotDate] = $convertedSlots[$spotDate] ?? [];
959
960 $convertedSlots[$spotDate][] = $spot;
961 }
962 }
963
964 return $convertedSlots;
965 }
966
967 public function getFilledWeeks($from, $to, $weekStart = '')
968 {
969 $weekStart = $weekStart ? $weekStart : Arr::get(Helper::getGlobalSettings(), 'administration.start_day', 'sun');
970
971 $startDate = new DateTime($from);
972 $endDate = new DateTime($to);
973
974 if (strtolower($startDate->format('D')) != $weekStart) {
975 $startDate->modify('last ' . $weekStart);
976 }
977
978 $weeks = [];
979
980 while ($startDate <= $endDate) {
981 // get all days in this week
982 $week = [];
983 for ($i = 0; $i < 7; $i++) {
984 $week[] = $startDate->format('Y-m-d');
985 $startDate->modify('+1 day');
986 }
987 $weeks[] = $week;
988 }
989
990 return $weeks;
991 }
992
993 protected function getBookingsTotal($start, $end)
994 {
995 if ($this->calendarSlot->event_type == 'group') {
996 return Booking::query()
997 ->where('event_id', $this->calendarSlot->id)
998 ->whereBetween('start_time', [$start, $end])
999 ->whereIn('status', ['scheduled', 'completed'])
1000 ->groupBy('group_id')
1001 ->count();
1002 }
1003
1004 return Booking::query()
1005 ->where('event_id', $this->calendarSlot->id)
1006 ->whereBetween('start_time', [$start, $end])
1007 ->whereIn('status', ['scheduled', 'completed'])
1008 ->count();
1009 }
1010
1011 protected function getBookingDurationTotal($start, $end)
1012 {
1013 if ($this->calendarSlot->event_type == 'group') {
1014 return Booking::query()
1015 ->select(['group_id', 'slot_minutes'])
1016 ->where('event_id', $this->calendarSlot->id)
1017 ->whereBetween('start_time', [$start, $end])
1018 ->whereIn('status', ['scheduled', 'completed'])
1019 ->groupBy('group_id')
1020 ->get()
1021 ->sum('slot_minutes');
1022 }
1023
1024 return Booking::query()
1025 ->where('event_id', $this->calendarSlot->id)
1026 ->whereBetween('start_time', [$start, $end])
1027 ->whereIn('status', ['scheduled', 'completed'])
1028 ->sum('slot_minutes');
1029 }
1030
1031 protected function getMaxBookingTimestamp($fromDate, $toDate, $timeZone)
1032 {
1033 $maxBookingTime = $this->calendarSlot->getMaxBookableDateTime($toDate, $timeZone, 'Y-m-d H:i:s');
1034
1035 return strtotime($maxBookingTime);
1036 }
1037
1038 protected function getTimezoneInfo($hostId = null)
1039 {
1040 $hostId = $hostId ?: $this->hostId;
1041
1042 $scheduleTimezone = $this->calendarSlot->getScheduleTimezone($hostId);
1043
1044 $dstTime = DateTimeHelper::getDaylightSavingTime($scheduleTimezone);
1045
1046 return [$scheduleTimezone, $dstTime];
1047 }
1048
1049 protected function maybeDayLightSavingSlot($slot, $dstTime, $scheduleTimezone, $adjustSign = '-')
1050 {
1051 if (!$dstTime) {
1052 return $slot;
1053 }
1054
1055 $slot['start'] = $this->maybeDayLightSavingTime($slot['start'], $dstTime, $scheduleTimezone, $adjustSign);
1056 $slot['end'] = $this->maybeDayLightSavingTime($slot['end'], $dstTime, $scheduleTimezone, $adjustSign);
1057
1058 return $slot;
1059 }
1060
1061 protected function maybeDayLightSavingTime($time, $dstTime, $timezone, $adjustSign = '+')
1062 {
1063 $scheduleTime = DateTimeHelper::convertToTimeZone($time, 'UTC', $timezone);
1064 if (DateTimeHelper::isDaylightSavingActive($scheduleTime, $timezone)) {
1065 $time = gmdate('Y-m-d H:i:s', strtotime($time . " $adjustSign $dstTime minutes")); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
1066 }
1067
1068 return $time;
1069 }
1070
1071 protected function isLocalBooking($bookings, $slot)
1072 {
1073 if (empty($bookings)) {
1074 return false;
1075 }
1076
1077 foreach ($bookings as $book) {
1078 if ($book['start'] == $slot['start'] && $book['end'] == $slot['end']) {
1079 return true;
1080 }
1081 }
1082
1083 return false;
1084 }
1085
1086 protected function mergeAndSortSlots($currentSlots, $validSlots)
1087 {
1088 if (!$currentSlots) {
1089 return $validSlots;
1090 }
1091
1092 $mergedSlots = array_merge($currentSlots, $validSlots);
1093
1094 usort($mergedSlots, function ($a, $b) {
1095 return strtotime($a['start']) - strtotime($b['start']);
1096 });
1097
1098 return $mergedSlots;
1099 }
1100
1101 protected function sortDaySlots($daySlots)
1102 {
1103 usort($daySlots, function ($a, $b) {
1104 return strtotime($a) - strtotime($b);
1105 });
1106
1107 return $daySlots;
1108 }
1109 }
1110