PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.5.0
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.5.0
2.5.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 All 34 releases
← All changes | app/Services/TimeSlotService.php +165 -61 1.6.0 → 2.5.0 View file →
@@ -33,10 +33,10 @@
33 33 $ranges = $this->getCurrentDateRange($fromDate, $toDate);
34 34
35 35 $bookedSlots = $this->getBookedSlots([$fromDate, $toDate], 'UTC', $isDoingBooking);
36 36
37 - $ranges = $this->maybeBookingFrequencyLimitRanges($ranges, $bookedSlots);
38 - $ranges = $this->maybeBookingDurationLimitRanges($ranges, $bookedSlots, $duration);
37 + $ranges = $this->maybeBookingFrequencyLimitRanges($ranges);
38 + $ranges = $this->maybeBookingDurationLimitRanges($ranges, $duration);
39 39
40 40 $cutOutTime = DateTimeHelper::getTimestamp() + $this->calendarSlot->getCutoutSeconds();
41 41
42 42 $maxBookingTime = $this->getMaxBookingTimestamp($fromDate, $toDate, $timeZone);
@@ -44,8 +44,10 @@
44 44 $timezoneInfo = $this->getTimezoneInfo();
45 45
46 46 $rangedSlots = $this->getRangedValidSlots($ranges, $duration, $bookedSlots, $cutOutTime, $maxBookingTime, $timezoneInfo);
47 47
48 + $rangedSlots = $this->maybeBookingPerDayLimitSlots($rangedSlots, $bookedSlots, $duration);
49 +
48 50 return $rangedSlots;
49 51 }
50 52
51 53 protected function getRangedValidSlots($ranges, $duration, $bookedSlots, $cutOutTime, $maxBookingTime, $timezoneInfo, $rangedSlots = [], $hostId = null)
@@ -104,9 +106,9 @@
104 106 continue;
105 107 }
106 108
107 109 if ($isLastDay && strtotime($slot['end']) > $maxBookingTime) {
108 - continue;
110 + break;
109 111 }
110 112
111 113 $isSlotAvailable = $this->isSlotAvailable($slot, $currentBookedSlots, $bufferTime, $hostId);
112 114
@@ -178,12 +180,12 @@
178 180 $duration = $this->calendarSlot->getDuration($duration);
179 181
180 182 list($scheduleTimezone, $dstTime) = $this->getTimezoneInfo();
181 183
182 - $fromStartTime = $this->maybeDayLightSavingTime($fromTime, $dstTime, $scheduleTimezone);
183 184 $toEndTime = $this->maybeDayLightSavingTime($toTime, $dstTime, $scheduleTimezone);
184 185
185 - $fromTime = gmdate('Y-m-d 00:00:00', strtotime($fromStartTime)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
186 + // Start from the requested time, not the DST-shifted one: the shift can cross into the next UTC day and drop that day's bookings.
187 + $fromTime = gmdate('Y-m-d 00:00:00', $fromTimeStamp); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
186 188 $toTime = gmdate('Y-m-d 23:59:59', strtotime($toEndTime)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
187 189
188 190 $slots = $this->getDates($fromTime, $toTime, $duration, true);
189 191
@@ -200,8 +202,15 @@
200 202 }
201 203
202 204 protected function isSlotExists($availableSlots, $fromTimeStamp, $toTimeStamp)
203 205 {
206 + // Slot generation only checks notice on today and the horizon on the last day.
207 + if ($fromTimeStamp < strtotime($this->calendarSlot->getMinBookableDateTime()) ||
208 + $toTimeStamp > $this->getMaxBookingTimestamp(null, gmdate('Y-m-d H:i:s', $toTimeStamp), 'UTC') // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
209 + ) {
210 + return false;
211 + }
212 +
204 213 $left = 0;
205 214 $right = count($availableSlots) - 1;
206 215
207 216 while ($left <= $right) {
@@ -233,9 +242,9 @@
233 242 $endDate = gmdate('Y-m-t 23:59:59', strtotime($startDate)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
234 243 }
235 244
236 245 $currentDate = strtotime($startDate);
237 - $endDate = strtotime($endDate);
246 + $endDate = strtotime($endDate) + 1; // add 1s in case end is 23:59:59
238 247 $oneDay = 24 * 60 * 60;
239 248
240 249 $dateArray = [];
241 250
@@ -267,16 +276,20 @@
267 276
268 277 $hostIds = $this->calendarSlot->getHostIds($this->hostId);
269 278 $status = ['pending', 'reserved', 'approved', 'scheduled', 'completed'];
270 279
280 + // Single indexed start_time range: widen the lower bound by max booking duration to catch overlaps.
281 + $maxDurationMinutes = (int) apply_filters('fluent_booking/max_booking_duration_minutes', DAY_IN_SECONDS / MINUTE_IN_SECONDS, $this->calendarSlot);
282 +
283 + $rangeLowerBound = gmdate('Y-m-d H:i:s', strtotime($dateRange[0]) - $maxDurationMinutes * MINUTE_IN_SECONDS); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
284 +
271 285 $bookings = Booking::with(['calendar_event'])
272 286 ->whereHas('hosts', function ($query) use ($hostIds) {
273 287 $query->whereIn('user_id', $hostIds);
274 288 })
275 - ->where(function ($query) use ($dateRange) {
276 - $query->whereBetween('start_time', $dateRange)
277 - ->orWhereBetween('end_time', $dateRange);
278 - })
289 + ->where('start_time', '>=', $rangeLowerBound)
290 + ->where('start_time', '<=', $dateRange[1])
291 + ->where('end_time', '>=', $dateRange[0])
279 292 ->orderBy('start_time', 'ASC')
280 293 ->whereIn('status', $status)
281 294 ->get()
282 295 ->groupBy('group_id');
@@ -567,14 +580,14 @@
567 580
568 581 $event = $this->calendarSlot;
569 582 $duration = $event->getDuration($duration);
570 583
571 - $startDate = $this->adjustStartDate($startDate, $timeZone);
584 + $adjustedDate = $this->adjustStartDate($startDate, $timeZone);
572 585
573 586 $isDisplaySpots = $event->is_display_spots;
574 587 $isMultiGuest = $event->isMultiGuestEvent();
575 588 $isMultiBooking = $event->isAdditionalGuestEnabled();
576 - $endDate = $event->getMaxBookableDateTime($startDate, $timeZone);
589 + $endDate = $event->getMaxBookableDateTime($adjustedDate, $timeZone);
577 590 $startDate = $event->getMinBookableDateTime($startDate, $timeZone);
578 591
579 592 $maxBooking = false;
580 593 if ($isMultiGuest && ($isDisplaySpots || $isMultiBooking)) {
@@ -645,9 +658,9 @@
645 658 }
646 659 }
647 660
648 661 $convertedSpots = array_map(function ($spots) {
649 - return array_values($spots);
662 + return array_values(ksort($spots) ? $spots : $spots);
650 663 }, $convertedSpots);
651 664
652 665 return $convertedSpots;
653 666 }
@@ -705,9 +718,9 @@
705 718
706 719 return $rangeArray;
707 720 }
708 721
709 - private function maybeBookingFrequencyLimitRanges($ranges, $bookedSlots)
722 + private function maybeBookingFrequencyLimitRanges($ranges)
710 723 {
711 724 if (!$ranges) {
712 725 return $ranges;
713 726 }
@@ -762,39 +775,12 @@
762 775 }
763 776 }
764 777 }
765 778 }
766 -
767 - // Per Day Booking Frequency Limit Hanlder
768 - if (!empty($keyedFrequenceyLimits['per_day'])) {
769 - $perDayLimit = $keyedFrequenceyLimits['per_day'];
770 - foreach ($ranges as $rangeIndex => $rangeDate) {
771 - if (!isset($bookedSlots[$rangeDate])) {
772 - continue;
773 - }
774 -
775 - $dayBooked = array_filter($bookedSlots[$rangeDate], function ($slot) {
776 - return Arr::get($slot, 'event_id') == $this->calendarSlot->id;
777 - });
778 -
779 - if (!$dayBooked) {
780 - continue;
781 - }
782 -
783 - if (count($dayBooked) >= $perDayLimit) {
784 - unset($ranges[$rangeIndex]);
785 - }
786 -
787 - if (!$ranges) {
788 - return [];
789 - }
790 - }
791 - }
792 -
793 779 return $ranges;
794 780 }
795 781
796 - private function maybeBookingDurationLimitRanges($ranges, $bookedSlots, $duration)
782 + private function maybeBookingDurationLimitRanges($ranges, $duration)
797 783 {
798 784 if (!$ranges) {
799 785 return $ranges;
800 786 }
@@ -841,35 +827,153 @@
841 827 }
842 828 }
843 829 }
844 830 }
831 + return $ranges;
832 + }
845 833
846 - // Per Day Booking Frequency Limit Hanlder
847 - if (!empty($keyedLimits['per_day'])) {
848 - $perDayLimit = $keyedLimits['per_day'];
849 - foreach ($ranges as $rangeIndex => $rangeDate) {
850 - if (!isset($bookedSlots[$rangeDate])) {
851 - continue;
834 + protected function maybeBookingPerDayLimitSlots($rangesSlots, $bookedSlots, $duration)
835 + {
836 + $isDurationEnabled = !!Arr::get($this->calendarSlot->settings, 'booking_duration.enabled');
837 +
838 + $isFrequencyEnabled = !!Arr::get($this->calendarSlot->settings, 'booking_frequency.enabled');
839 +
840 + if (!$isDurationEnabled && !$isFrequencyEnabled) {
841 + return $rangesSlots;
842 + }
843 +
844 + $hostTimeZone = $this->calendarSlot->getScheduleTimezone($this->hostId);
845 +
846 + $convertedRangesSlots = $this->convertSlotsByTimezone($rangesSlots, 'UTC', $hostTimeZone);
847 +
848 + $convertedBookedSlots = $this->convertSlotsByTimezone($bookedSlots, 'UTC', $hostTimeZone);
849 +
850 + $convertedRangesSlots = $this->maybeBookingDurationDayLimit($convertedRangesSlots, $convertedBookedSlots, $duration, $isDurationEnabled);
851 +
852 + $convertedRangesSlots = $this->maybeBookingFrequencyDayLimit($convertedRangesSlots, $convertedBookedSlots, $isFrequencyEnabled);
853 +
854 + $rangesSlots = $this->convertSlotsByTimezone($convertedRangesSlots, $hostTimeZone, 'UTC');
855 +
856 + return $rangesSlots;
857 + }
858 +
859 + protected function maybeBookingDurationDayLimit($rangesSlots, $bookedSlots, $duration, $isEnabled)
860 + {
861 + if (!$isEnabled) {
862 + return $rangesSlots;
863 + }
864 +
865 + $limits = Arr::get($this->calendarSlot->settings, 'booking_duration.limits', []);
866 +
867 + $perDayLimit = null;
868 + foreach ($limits as $limit) {
869 + if (Arr::get($limit, 'unit') == 'per_day' && Arr::get($limit, 'value')) {
870 + $perDayLimit = (int)Arr::get($limit, 'value');
871 + break;
872 + }
873 + }
874 +
875 + if (!$perDayLimit) {
876 + return $rangesSlots;
877 + }
878 +
879 + $isMultiSlot = $this->calendarSlot->isMultiGuestEvent();
880 +
881 + foreach ($rangesSlots as $rangeDate => &$slots) {
882 + if (!isset($bookedSlots[$rangeDate])) {
883 + continue;
884 + }
885 +
886 + $dayDuration = array_reduce($bookedSlots[$rangeDate], function ($carry, $slot) {
887 + if (Arr::get($slot, 'event_id') == $this->calendarSlot->id) {
888 + $carry += (int)((strtotime($slot['end']) - strtotime($slot['start'])) / 60);
852 889 }
890 + return $carry;
891 + }, 0);
853 892
854 - $dayDurarion = array_reduce($bookedSlots[$rangeDate], function ($carry, $slot) {
855 - if (Arr::get($slot, 'event_id') == $this->calendarSlot->id) {
856 - $carry += (int)((strtotime($slot['end']) - strtotime($slot['start'])) / 60);
857 - }
858 - return $carry;
859 - }, 0);
893 + if ($dayDuration + $duration > $perDayLimit) {
894 + if ($isMultiSlot) {
895 + $slots = array_values(array_filter($slots, function ($slot) {
896 + return !empty(Arr::get($slot, 'remaining'));
897 + }));
898 + }
899 + if (!$isMultiSlot || !count($slots)) {
900 + unset($rangesSlots[$rangeDate]);
901 + }
902 + }
903 + }
860 904
861 - if (!$dayDurarion) {
862 - continue;
905 + return $rangesSlots;
906 + }
907 +
908 + protected function maybeBookingFrequencyDayLimit($rangesSlots, $bookedSlots, $isEnabled)
909 + {
910 + if (!$isEnabled) {
911 + return $rangesSlots;
912 + }
913 +
914 + $limits = Arr::get($this->calendarSlot->settings, 'booking_frequency.limits', []);
915 +
916 + $perDayLimit = null;
917 + foreach ($limits as $limit) {
918 + if (Arr::get($limit, 'unit') == 'per_day' && Arr::get($limit, 'value')) {
919 + $perDayLimit = (int)Arr::get($limit, 'value');
920 + break;
921 + }
922 + }
923 +
924 + if (!$perDayLimit) {
925 + return $rangesSlots;
926 + }
927 +
928 + $isMultiSlot = $this->calendarSlot->isMultiGuestEvent();
929 +
930 + foreach ($rangesSlots as $rangeDate => &$slots) {
931 + if (!isset($bookedSlots[$rangeDate])) {
932 + continue;
933 + }
934 +
935 + $dayBooked = array_filter($bookedSlots[$rangeDate], function ($slot) {
936 + return Arr::get($slot, 'event_id') == $this->calendarSlot->id;
937 + });
938 +
939 + if (count($dayBooked) >= $perDayLimit) {
940 + if ($isMultiSlot) {
941 + $slots = array_values(array_filter($slots, function ($slot) {
942 + return !empty(Arr::get($slot, 'remaining'));
943 + }));
863 944 }
945 + if (!$isMultiSlot || !count($slots)) {
946 + unset($rangesSlots[$rangeDate]);
947 + }
948 + }
949 + }
864 950
865 - if ($dayDurarion + $duration > $perDayLimit) {
866 - unset($ranges[$rangeIndex]);
867 - }
951 + return $rangesSlots;
952 + }
953 +
954 + protected function convertSlotsByTimezone($slots, $fromTimeZone, $toTimeZone)
955 + {
956 + if ($fromTimeZone == $toTimeZone) {
957 + return $slots;
958 + }
959 +
960 + $convertedSlots = [];
961 +
962 + foreach ($slots as $spots) {
963 + foreach ($spots as $spot) {
964 + $spot['start'] = DateTimeHelper::convertToTimeZone($spot['start'], $fromTimeZone, $toTimeZone);
965 + $spot['end'] = DateTimeHelper::convertToTimeZone($spot['end'], $fromTimeZone, $toTimeZone);
966 +
967 + $spotDate = gmdate('Y-m-d', strtotime($spot['start']));
968 +
969 + $convertedSlots[$spotDate] = $convertedSlots[$spotDate] ?? [];
970 +
971 + $convertedSlots[$spotDate][] = $spot;
868 972 }
869 973 }
870 974
871 - return $ranges;
975 + return $convertedSlots;
872 976 }
873 977
874 978 public function getFilledWeeks($from, $to, $weekStart = '')
875 979 {
@@ -936,9 +1040,9 @@
936 1040 }
937 1041
938 1042 protected function getMaxBookingTimestamp($fromDate, $toDate, $timeZone)
939 1043 {
940 - $maxBookingTime = $this->calendarSlot->getMaxBookableDateTime($fromDate, $timeZone, 'Y-m-d H:i:s');
1044 + $maxBookingTime = $this->calendarSlot->getMaxBookableDateTime($toDate, $timeZone, 'Y-m-d H:i:s');
941 1045
942 1046 return strtotime($maxBookingTime);
943 1047 }
944 1048