PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / trunk
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution vtrunk
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
← All changes | app/Services/TimeSlotService.php +161 -66 1.5.21trunk View file →
@@ -4,9 +4,8 @@
4 4
5 5 use FluentBooking\App\Models\Booking;
6 6 use FluentBooking\App\Models\Calendar;
7 7 use FluentBooking\App\Models\CalendarSlot;
8 -use FluentBooking\App\Models\Availability;
9 8 use FluentBooking\Framework\Support\Arr;
10 9 use FluentBooking\Framework\Support\DateTime;
11 10
12 11 class TimeSlotService
@@ -34,10 +33,10 @@
34 33 $ranges = $this->getCurrentDateRange($fromDate, $toDate);
35 34
36 35 $bookedSlots = $this->getBookedSlots([$fromDate, $toDate], 'UTC', $isDoingBooking);
37 36
38 - $ranges = $this->maybeBookingFrequencyLimitRanges($ranges, $bookedSlots);
39 - $ranges = $this->maybeBookingDurationLimitRanges($ranges, $bookedSlots, $duration);
37 + $ranges = $this->maybeBookingFrequencyLimitRanges($ranges);
38 + $ranges = $this->maybeBookingDurationLimitRanges($ranges, $duration);
40 39
41 40 $cutOutTime = DateTimeHelper::getTimestamp() + $this->calendarSlot->getCutoutSeconds();
42 41
43 42 $maxBookingTime = $this->getMaxBookingTimestamp($fromDate, $toDate, $timeZone);
@@ -45,8 +44,10 @@
45 44 $timezoneInfo = $this->getTimezoneInfo();
46 45
47 46 $rangedSlots = $this->getRangedValidSlots($ranges, $duration, $bookedSlots, $cutOutTime, $maxBookingTime, $timezoneInfo);
48 47
48 + $rangedSlots = $this->maybeBookingPerDayLimitSlots($rangedSlots, $bookedSlots, $duration);
49 +
49 50 return $rangedSlots;
50 51 }
51 52
52 53 protected function getRangedValidSlots($ranges, $duration, $bookedSlots, $cutOutTime, $maxBookingTime, $timezoneInfo, $rangedSlots = [], $hostId = null)
@@ -105,9 +106,9 @@
105 106 continue;
106 107 }
107 108
108 109 if ($isLastDay && strtotime($slot['end']) > $maxBookingTime) {
109 - continue;
110 + break;
110 111 }
111 112
112 113 $isSlotAvailable = $this->isSlotAvailable($slot, $currentBookedSlots, $bufferTime, $hostId);
113 114
@@ -234,9 +235,9 @@
234 235 $endDate = gmdate('Y-m-t 23:59:59', strtotime($startDate)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
235 236 }
236 237
237 238 $currentDate = strtotime($startDate);
238 - $endDate = strtotime($endDate);
239 + $endDate = strtotime($endDate) + 1; // add 1s in case end is 23:59:59
239 240 $oneDay = 24 * 60 * 60;
240 241
241 242 $dateArray = [];
242 243
@@ -268,16 +269,20 @@
268 269
269 270 $hostIds = $this->calendarSlot->getHostIds($this->hostId);
270 271 $status = ['pending', 'reserved', 'approved', 'scheduled', 'completed'];
271 272
273 + // Single indexed start_time range: widen the lower bound by max booking duration to catch overlaps.
274 + $maxDurationMinutes = (int) apply_filters('fluent_booking/max_booking_duration_minutes', DAY_IN_SECONDS / MINUTE_IN_SECONDS, $this->calendarSlot);
275 +
276 + $rangeLowerBound = gmdate('Y-m-d H:i:s', strtotime($dateRange[0]) - $maxDurationMinutes * MINUTE_IN_SECONDS); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
277 +
272 278 $bookings = Booking::with(['calendar_event'])
273 279 ->whereHas('hosts', function ($query) use ($hostIds) {
274 280 $query->whereIn('user_id', $hostIds);
275 281 })
276 - ->where(function ($query) use ($dateRange) {
277 - $query->whereBetween('start_time', $dateRange)
278 - ->orWhereBetween('end_time', $dateRange);
279 - })
282 + ->where('start_time', '>=', $rangeLowerBound)
283 + ->where('start_time', '<=', $dateRange[1])
284 + ->where('end_time', '>=', $dateRange[0])
280 285 ->orderBy('start_time', 'ASC')
281 286 ->whereIn('status', $status)
282 287 ->get()
283 288 ->groupBy('group_id');
@@ -393,9 +398,9 @@
393 398
394 399 $interval = $this->calendarSlot->getSlotInterval($duration) * 60;
395 400
396 401 $weeklySlots = $this->calendarSlot->getWeeklySlots($hostId);
397 -
402 +
398 403 $items = $this->getEnabledSlots($weeklySlots);
399 404
400 405 // create range of each day slots from $items array above with $period minutes interval
401 406 $formattedSlots = [];
@@ -444,12 +449,12 @@
444 449 if (isset($days[$nextDayIndex])) {
445 450 $nextDay = $items[$days[$nextDayIndex]];
446 451
447 452 if ($nextDay && $nextDay[0]['start'] == '00:00') {
448 - $nextDayEnd = strtotime($nextDay[0]['end']);
453 + $nextDayEndTime = strtotime($nextDay[0]['end']) - strtotime($nextDay[0]['start']);
449 454 $reserveTime = $end - $start;
450 455
451 - while ($period - $reserveTime <= $nextDayEnd) {
456 + while ($period - $reserveTime <= $nextDayEndTime) {
452 457 $startTime = gmdate('H:i', $start); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
453 458 $nextDayStart = gmdate('H:i', $interval - $reserveTime); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
454 459 $daySlots[] = $startTime;
455 460
@@ -523,12 +528,12 @@
523 528 if (isset($overrideSlots[$nextDayIndex])) {
524 529 $nextDay = $overrideSlots[$nextDayIndex];
525 530
526 531 if ($nextDay && $nextDay[0]['start'] == '00:00') {
527 - $nextDayEnd = strtotime($nextDay[0]['end']);
532 + $nextDayEndTime = strtotime($nextDay[0]['end']) - strtotime($nextDay[0]['start']);
528 533 $reserveTime = $end - $start;
529 534
530 - while ($period - $reserveTime <= $nextDayEnd) {
535 + while ($period - $reserveTime <= $nextDayEndTime) {
531 536 $startTime = gmdate('H:i', $start); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
532 537 $nextDayStart = gmdate('H:i', $interval - $reserveTime); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
533 538 $formattedSlots[] = $startTime;
534 539
@@ -566,17 +571,16 @@
566 571 {
567 572 $this->hostId = $hostId;
568 573
569 574 $event = $this->calendarSlot;
570 - $calendar = $this->calendar;
571 575 $duration = $event->getDuration($duration);
572 576
573 - $startDate = $this->adjustStartDate($startDate, $timeZone);
577 + $adjustedDate = $this->adjustStartDate($startDate, $timeZone);
574 578
575 579 $isDisplaySpots = $event->is_display_spots;
576 580 $isMultiGuest = $event->isMultiGuestEvent();
577 581 $isMultiBooking = $event->isAdditionalGuestEnabled();
578 - $endDate = $event->getMaxBookableDateTime($startDate, $timeZone);
582 + $endDate = $event->getMaxBookableDateTime($adjustedDate, $timeZone);
579 583 $startDate = $event->getMinBookableDateTime($startDate, $timeZone);
580 584
581 585 $maxBooking = false;
582 586 if ($isMultiGuest && ($isDisplaySpots || $isMultiBooking)) {
@@ -647,9 +651,9 @@
647 651 }
648 652 }
649 653
650 654 $convertedSpots = array_map(function ($spots) {
651 - return array_values($spots);
655 + return array_values(ksort($spots) ? $spots : $spots);
652 656 }, $convertedSpots);
653 657
654 658 return $convertedSpots;
655 659 }
@@ -707,9 +711,9 @@
707 711
708 712 return $rangeArray;
709 713 }
710 714
711 - private function maybeBookingFrequencyLimitRanges($ranges, $bookedSlots)
715 + private function maybeBookingFrequencyLimitRanges($ranges)
712 716 {
713 717 if (!$ranges) {
714 718 return $ranges;
715 719 }
@@ -764,39 +768,12 @@
764 768 }
765 769 }
766 770 }
767 771 }
768 -
769 - // Per Day Booking Frequency Limit Hanlder
770 - if (!empty($keyedFrequenceyLimits['per_day'])) {
771 - $perDayLimit = $keyedFrequenceyLimits['per_day'];
772 - foreach ($ranges as $rangeIndex => $rangeDate) {
773 - if (!isset($bookedSlots[$rangeDate])) {
774 - continue;
775 - }
776 -
777 - $dayBooked = array_filter($bookedSlots[$rangeDate], function ($slot) {
778 - return Arr::get($slot, 'event_id') == $this->calendarSlot->id;
779 - });
780 -
781 - if (!$dayBooked) {
782 - continue;
783 - }
784 -
785 - if (count($dayBooked) >= $perDayLimit) {
786 - unset($ranges[$rangeIndex]);
787 - }
788 -
789 - if (!$ranges) {
790 - return [];
791 - }
792 - }
793 - }
794 -
795 772 return $ranges;
796 773 }
797 774
798 - private function maybeBookingDurationLimitRanges($ranges, $bookedSlots, $duration)
775 + private function maybeBookingDurationLimitRanges($ranges, $duration)
799 776 {
800 777 if (!$ranges) {
801 778 return $ranges;
802 779 }
@@ -843,35 +820,153 @@
843 820 }
844 821 }
845 822 }
846 823 }
824 + return $ranges;
825 + }
847 826
848 - // Per Day Booking Frequency Limit Hanlder
849 - if (!empty($keyedLimits['per_day'])) {
850 - $perDayLimit = $keyedLimits['per_day'];
851 - foreach ($ranges as $rangeIndex => $rangeDate) {
852 - if (!isset($bookedSlots[$rangeDate])) {
853 - continue;
827 + protected function maybeBookingPerDayLimitSlots($rangesSlots, $bookedSlots, $duration)
828 + {
829 + $isDurationEnabled = !!Arr::get($this->calendarSlot->settings, 'booking_duration.enabled');
830 +
831 + $isFrequencyEnabled = !!Arr::get($this->calendarSlot->settings, 'booking_frequency.enabled');
832 +
833 + if (!$isDurationEnabled && !$isFrequencyEnabled) {
834 + return $rangesSlots;
835 + }
836 +
837 + $hostTimeZone = $this->calendarSlot->getScheduleTimezone($this->hostId);
838 +
839 + $convertedRangesSlots = $this->convertSlotsByTimezone($rangesSlots, 'UTC', $hostTimeZone);
840 +
841 + $convertedBookedSlots = $this->convertSlotsByTimezone($bookedSlots, 'UTC', $hostTimeZone);
842 +
843 + $convertedRangesSlots = $this->maybeBookingDurationDayLimit($convertedRangesSlots, $convertedBookedSlots, $duration, $isDurationEnabled);
844 +
845 + $convertedRangesSlots = $this->maybeBookingFrequencyDayLimit($convertedRangesSlots, $convertedBookedSlots, $isFrequencyEnabled);
846 +
847 + $rangesSlots = $this->convertSlotsByTimezone($convertedRangesSlots, $hostTimeZone, 'UTC');
848 +
849 + return $rangesSlots;
850 + }
851 +
852 + protected function maybeBookingDurationDayLimit($rangesSlots, $bookedSlots, $duration, $isEnabled)
853 + {
854 + if (!$isEnabled) {
855 + return $rangesSlots;
856 + }
857 +
858 + $limits = Arr::get($this->calendarSlot->settings, 'booking_duration.limits', []);
859 +
860 + $perDayLimit = null;
861 + foreach ($limits as $limit) {
862 + if (Arr::get($limit, 'unit') == 'per_day' && Arr::get($limit, 'value')) {
863 + $perDayLimit = (int)Arr::get($limit, 'value');
864 + break;
865 + }
866 + }
867 +
868 + if (!$perDayLimit) {
869 + return $rangesSlots;
870 + }
871 +
872 + $isMultiSlot = $this->calendarSlot->isMultiGuestEvent();
873 +
874 + foreach ($rangesSlots as $rangeDate => &$slots) {
875 + if (!isset($bookedSlots[$rangeDate])) {
876 + continue;
877 + }
878 +
879 + $dayDuration = array_reduce($bookedSlots[$rangeDate], function ($carry, $slot) {
880 + if (Arr::get($slot, 'event_id') == $this->calendarSlot->id) {
881 + $carry += (int)((strtotime($slot['end']) - strtotime($slot['start'])) / 60);
854 882 }
883 + return $carry;
884 + }, 0);
855 885
856 - $dayDurarion = array_reduce($bookedSlots[$rangeDate], function ($carry, $slot) {
857 - if (Arr::get($slot, 'event_id') == $this->calendarSlot->id) {
858 - $carry += (int)((strtotime($slot['end']) - strtotime($slot['start'])) / 60);
859 - }
860 - return $carry;
861 - }, 0);
886 + if ($dayDuration + $duration > $perDayLimit) {
887 + if ($isMultiSlot) {
888 + $slots = array_values(array_filter($slots, function ($slot) {
889 + return !empty(Arr::get($slot, 'remaining'));
890 + }));
891 + }
892 + if (!$isMultiSlot || !count($slots)) {
893 + unset($rangesSlots[$rangeDate]);
894 + }
895 + }
896 + }
862 897
863 - if (!$dayDurarion) {
864 - continue;
898 + return $rangesSlots;
899 + }
900 +
901 + protected function maybeBookingFrequencyDayLimit($rangesSlots, $bookedSlots, $isEnabled)
902 + {
903 + if (!$isEnabled) {
904 + return $rangesSlots;
905 + }
906 +
907 + $limits = Arr::get($this->calendarSlot->settings, 'booking_frequency.limits', []);
908 +
909 + $perDayLimit = null;
910 + foreach ($limits as $limit) {
911 + if (Arr::get($limit, 'unit') == 'per_day' && Arr::get($limit, 'value')) {
912 + $perDayLimit = (int)Arr::get($limit, 'value');
913 + break;
914 + }
915 + }
916 +
917 + if (!$perDayLimit) {
918 + return $rangesSlots;
919 + }
920 +
921 + $isMultiSlot = $this->calendarSlot->isMultiGuestEvent();
922 +
923 + foreach ($rangesSlots as $rangeDate => &$slots) {
924 + if (!isset($bookedSlots[$rangeDate])) {
925 + continue;
926 + }
927 +
928 + $dayBooked = array_filter($bookedSlots[$rangeDate], function ($slot) {
929 + return Arr::get($slot, 'event_id') == $this->calendarSlot->id;
930 + });
931 +
932 + if (count($dayBooked) >= $perDayLimit) {
933 + if ($isMultiSlot) {
934 + $slots = array_values(array_filter($slots, function ($slot) {
935 + return !empty(Arr::get($slot, 'remaining'));
936 + }));
865 937 }
938 + if (!$isMultiSlot || !count($slots)) {
939 + unset($rangesSlots[$rangeDate]);
940 + }
941 + }
942 + }
866 943
867 - if ($dayDurarion + $duration > $perDayLimit) {
868 - unset($ranges[$rangeIndex]);
869 - }
944 + return $rangesSlots;
945 + }
946 +
947 + protected function convertSlotsByTimezone($slots, $fromTimeZone, $toTimeZone)
948 + {
949 + if ($fromTimeZone == $toTimeZone) {
950 + return $slots;
951 + }
952 +
953 + $convertedSlots = [];
954 +
955 + foreach ($slots as $spots) {
956 + foreach ($spots as $spot) {
957 + $spot['start'] = DateTimeHelper::convertToTimeZone($spot['start'], $fromTimeZone, $toTimeZone);
958 + $spot['end'] = DateTimeHelper::convertToTimeZone($spot['end'], $fromTimeZone, $toTimeZone);
959 +
960 + $spotDate = gmdate('Y-m-d', strtotime($spot['start']));
961 +
962 + $convertedSlots[$spotDate] = $convertedSlots[$spotDate] ?? [];
963 +
964 + $convertedSlots[$spotDate][] = $spot;
870 965 }
871 966 }
872 967
873 - return $ranges;
968 + return $convertedSlots;
874 969 }
875 970
876 971 public function getFilledWeeks($from, $to, $weekStart = '')
877 972 {
@@ -938,9 +1033,9 @@
938 1033 }
939 1034
940 1035 protected function getMaxBookingTimestamp($fromDate, $toDate, $timeZone)
941 1036 {
942 - $maxBookingTime = $this->calendarSlot->getMaxBookableDateTime($fromDate, $timeZone, 'Y-m-d H:i:s');
1037 + $maxBookingTime = $this->calendarSlot->getMaxBookableDateTime($toDate, $timeZone, 'Y-m-d H:i:s');
943 1038
944 1039 return strtotime($maxBookingTime);
945 1040 }
946 1041