| @@ -106,9 +106,9 @@ | ||
| 106 | 106 | continue; |
| 107 | 107 | } |
| 108 | 108 | |
| 109 | 109 | if ($isLastDay && strtotime($slot['end']) > $maxBookingTime) { |
| 110 | - continue; | |
| 110 | + break; | |
| 111 | 111 | } |
| 112 | 112 | |
| 113 | 113 | $isSlotAvailable = $this->isSlotAvailable($slot, $currentBookedSlots, $bufferTime, $hostId); |
| 114 | 114 | |
| @@ -180,12 +180,12 @@ | ||
| 180 | 180 | $duration = $this->calendarSlot->getDuration($duration); |
| 181 | 181 | |
| 182 | 182 | list($scheduleTimezone, $dstTime) = $this->getTimezoneInfo(); |
| 183 | 183 | |
| 184 | - $fromStartTime = $this->maybeDayLightSavingTime($fromTime, $dstTime, $scheduleTimezone); | |
| 185 | 184 | $toEndTime = $this->maybeDayLightSavingTime($toTime, $dstTime, $scheduleTimezone); |
| 186 | 185 | |
| 187 | - $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 | |
| 188 | 188 | $toTime = gmdate('Y-m-d 23:59:59', strtotime($toEndTime)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date |
| 189 | 189 | |
| 190 | 190 | $slots = $this->getDates($fromTime, $toTime, $duration, true); |
| 191 | 191 | |
| @@ -202,8 +202,15 @@ | ||
| 202 | 202 | } |
| 203 | 203 | |
| 204 | 204 | protected function isSlotExists($availableSlots, $fromTimeStamp, $toTimeStamp) |
| 205 | 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 | + | |
| 206 | 213 | $left = 0; |
| 207 | 214 | $right = count($availableSlots) - 1; |
| 208 | 215 | |
| 209 | 216 | while ($left <= $right) { |
| @@ -235,9 +242,9 @@ | ||
| 235 | 242 | $endDate = gmdate('Y-m-t 23:59:59', strtotime($startDate)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date |
| 236 | 243 | } |
| 237 | 244 | |
| 238 | 245 | $currentDate = strtotime($startDate); |
| 239 | - $endDate = strtotime($endDate); | |
| 246 | + $endDate = strtotime($endDate) + 1; // add 1s in case end is 23:59:59 | |
| 240 | 247 | $oneDay = 24 * 60 * 60; |
| 241 | 248 | |
| 242 | 249 | $dateArray = []; |
| 243 | 250 | |
| @@ -269,16 +276,20 @@ | ||
| 269 | 276 | |
| 270 | 277 | $hostIds = $this->calendarSlot->getHostIds($this->hostId); |
| 271 | 278 | $status = ['pending', 'reserved', 'approved', 'scheduled', 'completed']; |
| 272 | 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 | + | |
| 273 | 285 | $bookings = Booking::with(['calendar_event']) |
| 274 | 286 | ->whereHas('hosts', function ($query) use ($hostIds) { |
| 275 | 287 | $query->whereIn('user_id', $hostIds); |
| 276 | 288 | }) |
| 277 | - ->where(function ($query) use ($dateRange) { | |
| 278 | - $query->whereBetween('start_time', $dateRange) | |
| 279 | - ->orWhereBetween('end_time', $dateRange); | |
| 280 | - }) | |
| 289 | + ->where('start_time', '>=', $rangeLowerBound) | |
| 290 | + ->where('start_time', '<=', $dateRange[1]) | |
| 291 | + ->where('end_time', '>=', $dateRange[0]) | |
| 281 | 292 | ->orderBy('start_time', 'ASC') |
| 282 | 293 | ->whereIn('status', $status) |
| 283 | 294 | ->get() |
| 284 | 295 | ->groupBy('group_id'); |
| @@ -569,14 +580,14 @@ | ||
| 569 | 580 | |
| 570 | 581 | $event = $this->calendarSlot; |
| 571 | 582 | $duration = $event->getDuration($duration); |
| 572 | 583 | |
| 573 | - $startDate = $this->adjustStartDate($startDate, $timeZone); | |
| 584 | + $adjustedDate = $this->adjustStartDate($startDate, $timeZone); | |
| 574 | 585 | |
| 575 | 586 | $isDisplaySpots = $event->is_display_spots; |
| 576 | 587 | $isMultiGuest = $event->isMultiGuestEvent(); |
| 577 | 588 | $isMultiBooking = $event->isAdditionalGuestEnabled(); |
| 578 | - $endDate = $event->getMaxBookableDateTime($startDate, $timeZone); | |
| 589 | + $endDate = $event->getMaxBookableDateTime($adjustedDate, $timeZone); | |
| 579 | 590 | $startDate = $event->getMinBookableDateTime($startDate, $timeZone); |
| 580 | 591 | |
| 581 | 592 | $maxBooking = false; |
| 582 | 593 | if ($isMultiGuest && ($isDisplaySpots || $isMultiBooking)) { |
| @@ -647,9 +658,9 @@ | ||
| 647 | 658 | } |
| 648 | 659 | } |
| 649 | 660 | |
| 650 | 661 | $convertedSpots = array_map(function ($spots) { |
| 651 | - return array_values($spots); | |
| 662 | + return array_values(ksort($spots) ? $spots : $spots); | |
| 652 | 663 | }, $convertedSpots); |
| 653 | 664 | |
| 654 | 665 | return $convertedSpots; |
| 655 | 666 | } |
| @@ -1029,9 +1040,9 @@ | ||
| 1029 | 1040 | } |
| 1030 | 1041 | |
| 1031 | 1042 | protected function getMaxBookingTimestamp($fromDate, $toDate, $timeZone) |
| 1032 | 1043 | { |
| 1033 | - $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'); | |
| 1034 | 1045 | |
| 1035 | 1046 | return strtotime($maxBookingTime); |
| 1036 | 1047 | } |
| 1037 | 1048 | |