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

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