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

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