| @@ -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 |
| @@ -16,14 +15,11 @@ | ||
| 16 | 15 | protected $calendar; |
| 17 | 16 | |
| 18 | 17 | protected $hostId = null; |
| 19 | 18 | |
| 20 | - protected $groupedSlots = []; | |
| 21 | - | |
| 22 | 19 | public function __construct(Calendar $calendar, CalendarSlot $calendarSlot) |
| 23 | 20 | { |
| 24 | 21 | $this->hostId = null; |
| 25 | - $this->groupedSlots = []; | |
| 26 | 22 | $this->calendar = $calendar; |
| 27 | 23 | $this->calendarSlot = $calendarSlot; |
| 28 | 24 | } |
| 29 | 25 | |
| @@ -37,24 +33,22 @@ | ||
| 37 | 33 | $ranges = $this->getCurrentDateRange($fromDate, $toDate); |
| 38 | 34 | |
| 39 | 35 | $bookedSlots = $this->getBookedSlots([$fromDate, $toDate], 'UTC', $isDoingBooking); |
| 40 | 36 | |
| 41 | - $ranges = $this->maybeBookingFrequencyLimitRanges($ranges, $bookedSlots); | |
| 42 | - $ranges = $this->maybeBookingDurationLimitRanges($ranges, $bookedSlots, $duration); | |
| 37 | + $ranges = $this->maybeBookingFrequencyLimitRanges($ranges); | |
| 38 | + $ranges = $this->maybeBookingDurationLimitRanges($ranges, $duration); | |
| 43 | 39 | |
| 44 | 40 | $cutOutTime = DateTimeHelper::getTimestamp() + $this->calendarSlot->getCutoutSeconds(); |
| 45 | 41 | |
| 46 | - $maxBookingTime = strtotime($this->calendarSlot->getMaxBookableDateTime($fromDate, $timeZone, 'Y-m-d H:i:s')); | |
| 42 | + $maxBookingTime = $this->getMaxBookingTimestamp($fromDate, $toDate, $timeZone); | |
| 47 | 43 | |
| 48 | 44 | $timezoneInfo = $this->getTimezoneInfo(); |
| 49 | 45 | |
| 50 | 46 | $rangedSlots = $this->getRangedValidSlots($ranges, $duration, $bookedSlots, $cutOutTime, $maxBookingTime, $timezoneInfo); |
| 51 | 47 | |
| 52 | - if (!$this->groupedSlots) { | |
| 53 | - return $rangedSlots; | |
| 54 | - } | |
| 48 | + $rangedSlots = $this->maybeBookingPerDayLimitSlots($rangedSlots, $bookedSlots, $duration); | |
| 55 | 49 | |
| 56 | - return $this->adjustGroupedSlots($ranges, $rangedSlots, $cutOutTime, $maxBookingTime, $timezoneInfo); | |
| 50 | + return $rangedSlots; | |
| 57 | 51 | } |
| 58 | 52 | |
| 59 | 53 | protected function getRangedValidSlots($ranges, $duration, $bookedSlots, $cutOutTime, $maxBookingTime, $timezoneInfo, $rangedSlots = [], $hostId = null) |
| 60 | 54 | { |
| @@ -84,10 +78,8 @@ | ||
| 84 | 78 | if (!$availableSlots) { |
| 85 | 79 | continue; |
| 86 | 80 | } |
| 87 | 81 | |
| 88 | - $currentBookedSlots = $bookedSlots[$date] ?? []; | |
| 89 | - | |
| 90 | 82 | $isToday = $date === $todayDate; |
| 91 | 83 | |
| 92 | 84 | $isLastDay = $date === $lastDate; |
| 93 | 85 | |
| @@ -92,8 +84,10 @@ | ||
| 92 | 84 | $isLastDay = $date === $lastDate; |
| 93 | 85 | |
| 94 | 86 | $validSlots = []; |
| 95 | 87 | |
| 88 | + $validDstDateSlots = []; | |
| 89 | + | |
| 96 | 90 | foreach ($availableSlots as $start) { |
| 97 | 91 | $end = gmdate('H:i', strtotime($start) + $period); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date |
| 98 | 92 | $endDate = $start < $end ? $date : gmdate('Y-m-d', strtotime($date) + 86400); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date |
| 99 | 93 | |
| @@ -103,20 +97,28 @@ | ||
| 103 | 97 | ]; |
| 104 | 98 | |
| 105 | 99 | $slot = $this->maybeDayLightSavingSlot($slot, $dstTime, $scheduleTimezone); |
| 106 | 100 | |
| 101 | + $slotDate = gmdate('Y-m-d', strtotime($slot['start'])); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date | |
| 102 | + | |
| 103 | + $currentBookedSlots = $bookedSlots[$slotDate] ?? []; | |
| 104 | + | |
| 107 | 105 | if ($isToday && strtotime($slot['start']) < $cutOutTime) { |
| 108 | 106 | continue; |
| 109 | 107 | } |
| 110 | 108 | |
| 111 | 109 | if ($isLastDay && strtotime($slot['end']) > $maxBookingTime) { |
| 112 | - continue; | |
| 110 | + break; | |
| 113 | 111 | } |
| 114 | 112 | |
| 115 | 113 | $isSlotAvailable = $this->isSlotAvailable($slot, $currentBookedSlots, $bufferTime, $hostId); |
| 116 | 114 | |
| 117 | 115 | if ($isSlotAvailable) { |
| 118 | - $validSlots[] = $slot; | |
| 116 | + if ($slotDate != $date) { | |
| 117 | + $validDstDateSlots[] = $slot; | |
| 118 | + } else { | |
| 119 | + $validSlots[] = $slot; | |
| 120 | + } | |
| 119 | 121 | } |
| 120 | 122 | } |
| 121 | 123 | |
| 122 | 124 | if ($validSlots) { |
| @@ -122,8 +124,14 @@ | ||
| 122 | 124 | if ($validSlots) { |
| 123 | 125 | $currentSlots = $rangedSlots[$date] ?? []; |
| 124 | 126 | $rangedSlots[$date] = $this->mergeAndSortSlots($currentSlots, $validSlots); |
| 125 | 127 | } |
| 128 | + | |
| 129 | + if ($validDstDateSlots) { | |
| 130 | + $slotDate = gmdate('Y-m-d', strtotime($validDstDateSlots[0]['start'])); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date | |
| 131 | + $currentSlots = $rangedSlots[$slotDate] ?? []; | |
| 132 | + $rangedSlots[$slotDate] = $this->mergeAndSortSlots($currentSlots, $validDstDateSlots); | |
| 133 | + } | |
| 126 | 134 | } |
| 127 | 135 | |
| 128 | 136 | return $rangedSlots; |
| 129 | 137 | } |
| @@ -161,52 +169,8 @@ | ||
| 161 | 169 | |
| 162 | 170 | return true; |
| 163 | 171 | } |
| 164 | 172 | |
| 165 | - protected function adjustGroupedSlots($ranges, $rangedSlots, $cutOutTime, $maxBookingTime, $timezoneInfo) | |
| 166 | - { | |
| 167 | - list($scheduleTimezone, $dstTime) = $timezoneInfo; | |
| 168 | - | |
| 169 | - $todayDate = gmdate('Y-m-d'); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date | |
| 170 | - | |
| 171 | - $lastDate = end($ranges); | |
| 172 | - | |
| 173 | - $addedDates = []; | |
| 174 | - | |
| 175 | - foreach ($this->groupedSlots as $slot) { | |
| 176 | - $date = gmdate('Y-m-d', strtotime($slot['start'])); | |
| 177 | - if ($todayDate == $date && strtotime($slot['start']) < $cutOutTime) { | |
| 178 | - continue; | |
| 179 | - } | |
| 180 | - | |
| 181 | - if ($lastDate == $date && strtotime($slot['end']) > $maxBookingTime) { | |
| 182 | - continue; | |
| 183 | - } | |
| 184 | - | |
| 185 | - if (!isset($rangedSlots[$date])) { | |
| 186 | - $rangedSlots[$date] = []; | |
| 187 | - $rangedSlots[$date][] = $slot; | |
| 188 | - continue; | |
| 189 | - } | |
| 190 | - | |
| 191 | - $addedDates[$date] = $date; | |
| 192 | - | |
| 193 | - $rangedSlots[$date][] = $slot; | |
| 194 | - } | |
| 195 | - | |
| 196 | - foreach ($addedDates as $date) { | |
| 197 | - $dateSlots = $rangedSlots[$date]; | |
| 198 | - | |
| 199 | - usort($dateSlots, function ($a, $b) { | |
| 200 | - return strtotime($a['start']) - strtotime($b['start']); | |
| 201 | - }); | |
| 202 | - | |
| 203 | - $rangedSlots[$date] = $dateSlots; | |
| 204 | - } | |
| 205 | - | |
| 206 | - return $rangedSlots; | |
| 207 | - } | |
| 208 | - | |
| 209 | 173 | public function isSpotAvailable($fromTime, $toTime, $duration = null, $hostId = null) |
| 210 | 174 | { |
| 211 | 175 | $this->hostId = $hostId; |
| 212 | 176 | |
| @@ -212,36 +176,54 @@ | ||
| 212 | 176 | |
| 213 | 177 | $fromTimeStamp = strtotime($fromTime); |
| 214 | 178 | $toTimeStamp = strtotime($toTime); |
| 215 | 179 | |
| 180 | + $duration = $this->calendarSlot->getDuration($duration); | |
| 181 | + | |
| 182 | + list($scheduleTimezone, $dstTime) = $this->getTimezoneInfo(); | |
| 183 | + | |
| 184 | + $toEndTime = $this->maybeDayLightSavingTime($toTime, $dstTime, $scheduleTimezone); | |
| 185 | + | |
| 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. | |
| 216 | 187 | $fromTime = gmdate('Y-m-d 00:00:00', $fromTimeStamp); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date |
| 217 | - $toTime = gmdate('Y-m-d 23:59:59', $toTimeStamp); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date | |
| 188 | + $toTime = gmdate('Y-m-d 23:59:59', strtotime($toEndTime)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date | |
| 218 | 189 | |
| 219 | - $duration = $this->calendarSlot->getDuration($duration); | |
| 220 | - | |
| 221 | 190 | $slots = $this->getDates($fromTime, $toTime, $duration, true); |
| 222 | 191 | |
| 223 | 192 | $fromDate = gmdate('Y-m-d', $fromTimeStamp); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date |
| 224 | 193 | $toDate = gmdate('Y-m-d', $toTimeStamp); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date |
| 225 | - | |
| 194 | + | |
| 226 | 195 | $availableSlots = $slots[$fromDate] ?? []; |
| 227 | - | |
| 196 | + | |
| 228 | 197 | if ($fromDate != $toDate) { |
| 229 | 198 | $availableSlots = array_merge($availableSlots, $slots[$toDate] ?? []); |
| 230 | 199 | } |
| 231 | 200 | |
| 201 | + return $this->isSlotExists($availableSlots, $fromTimeStamp, $toTimeStamp); | |
| 202 | + } | |
| 203 | + | |
| 204 | + protected function isSlotExists($availableSlots, $fromTimeStamp, $toTimeStamp) | |
| 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 | + | |
| 232 | 213 | $left = 0; |
| 233 | 214 | $right = count($availableSlots) - 1; |
| 234 | 215 | |
| 235 | 216 | while ($left <= $right) { |
| 236 | 217 | $mid = $left + (($right - $left) >> 1); |
| 218 | + $slot = $availableSlots[$mid]; | |
| 237 | 219 | |
| 238 | - $midStartTime = strtotime($availableSlots[$mid]['start']); | |
| 239 | - $midEndTime = strtotime($availableSlots[$mid]['end']); | |
| 220 | + $slotStartTime = strtotime($slot['start']); | |
| 221 | + $slotEndTime = strtotime($slot['end']); | |
| 240 | 222 | |
| 241 | - if ($fromTimeStamp == $midStartTime && $toTimeStamp == $midEndTime) { | |
| 242 | - return true; | |
| 243 | - } elseif ($fromTimeStamp > $midStartTime) { | |
| 223 | + if ($fromTimeStamp == $slotStartTime && $toTimeStamp == $slotEndTime) { | |
| 224 | + return $slot; | |
| 225 | + } elseif ($fromTimeStamp > $slotStartTime) { | |
| 244 | 226 | $left = $mid + 1; |
| 245 | 227 | } else { |
| 246 | 228 | $right = $mid - 1; |
| 247 | 229 | } |
| @@ -260,9 +242,9 @@ | ||
| 260 | 242 | $endDate = gmdate('Y-m-t 23:59:59', strtotime($startDate)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date |
| 261 | 243 | } |
| 262 | 244 | |
| 263 | 245 | $currentDate = strtotime($startDate); |
| 264 | - $endDate = strtotime($endDate); | |
| 246 | + $endDate = strtotime($endDate) + 1; // add 1s in case end is 23:59:59 | |
| 265 | 247 | $oneDay = 24 * 60 * 60; |
| 266 | 248 | |
| 267 | 249 | $dateArray = []; |
| 268 | 250 | |
| @@ -294,13 +276,20 @@ | ||
| 294 | 276 | |
| 295 | 277 | $hostIds = $this->calendarSlot->getHostIds($this->hostId); |
| 296 | 278 | $status = ['pending', 'reserved', 'approved', 'scheduled', 'completed']; |
| 297 | 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 | + | |
| 298 | 285 | $bookings = Booking::with(['calendar_event']) |
| 299 | 286 | ->whereHas('hosts', function ($query) use ($hostIds) { |
| 300 | 287 | $query->whereIn('user_id', $hostIds); |
| 301 | 288 | }) |
| 302 | - ->whereBetween('start_time', $dateRange) | |
| 289 | + ->where('start_time', '>=', $rangeLowerBound) | |
| 290 | + ->where('start_time', '<=', $dateRange[1]) | |
| 291 | + ->where('end_time', '>=', $dateRange[0]) | |
| 303 | 292 | ->orderBy('start_time', 'ASC') |
| 304 | 293 | ->whereIn('status', $status) |
| 305 | 294 | ->get() |
| 306 | 295 | ->groupBy('group_id'); |
| @@ -370,9 +359,8 @@ | ||
| 370 | 359 | |
| 371 | 360 | $eventIdAdded = false; |
| 372 | 361 | foreach ($rangedItems as $date => $slot) { |
| 373 | 362 | if ($isGroupBooking && $remaining && $this->calendarSlot->id == $booking->event_id) { |
| 374 | - $this->groupedSlots[] = $slot; | |
| 375 | 363 | if ($eventIdAdded) { |
| 376 | 364 | $slot['event_id'] = null; |
| 377 | 365 | } |
| 378 | 366 | $eventIdAdded = true; |
| @@ -436,9 +424,9 @@ | ||
| 436 | 424 | $start += $interval; |
| 437 | 425 | } |
| 438 | 426 | |
| 439 | 427 | if ($slot['end'] == '24:00' && $start < $end) { |
| 440 | - $daySlots = $this->handleNextDaySlot($daySlots, $items, $end, $start, $interval, $period, $day, $days); | |
| 428 | + $daySlots = $this->handleNextDaySlot($daySlots, $items, $start, $end, $interval, $period, $day, $days); | |
| 441 | 429 | } |
| 442 | 430 | } |
| 443 | 431 | if ($daySlots) { |
| 444 | 432 | $formattedSlots[$day] = $daySlots; |
| @@ -462,22 +450,22 @@ | ||
| 462 | 450 | } |
| 463 | 451 | |
| 464 | 452 | protected function handleNextDaySlot($daySlots, &$items, $start, $end, $interval, $period, $day, $days) |
| 465 | 453 | { |
| 466 | - $nextDayIndex = array_search($day, $days) + 1; | |
| 454 | + $nextDayIndex = (array_search($day, $days) + 1) % count($items); | |
| 467 | 455 | |
| 468 | 456 | if (isset($days[$nextDayIndex])) { |
| 469 | 457 | $nextDay = $items[$days[$nextDayIndex]]; |
| 470 | 458 | |
| 471 | 459 | if ($nextDay && $nextDay[0]['start'] == '00:00') { |
| 472 | - $nextDayEnd = strtotime($nextDay[0]['end']); | |
| 460 | + $nextDayEndTime = strtotime($nextDay[0]['end']) - strtotime($nextDay[0]['start']); | |
| 473 | 461 | $reserveTime = $end - $start; |
| 474 | 462 | |
| 475 | - while ($period - $reserveTime <= $nextDayEnd) { | |
| 463 | + while ($period - $reserveTime <= $nextDayEndTime) { | |
| 476 | 464 | $startTime = gmdate('H:i', $start); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date |
| 477 | 465 | $nextDayStart = gmdate('H:i', $interval - $reserveTime); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date |
| 478 | 466 | $daySlots[] = $startTime; |
| 479 | - | |
| 467 | + | |
| 480 | 468 | if ($nextDayStart < $startTime) { |
| 481 | 469 | $items[$days[$nextDayIndex]][0]['start'] = $nextDayStart; |
| 482 | 470 | break; |
| 483 | 471 | } |
| @@ -547,12 +535,12 @@ | ||
| 547 | 535 | if (isset($overrideSlots[$nextDayIndex])) { |
| 548 | 536 | $nextDay = $overrideSlots[$nextDayIndex]; |
| 549 | 537 | |
| 550 | 538 | if ($nextDay && $nextDay[0]['start'] == '00:00') { |
| 551 | - $nextDayEnd = strtotime($nextDay[0]['end']); | |
| 539 | + $nextDayEndTime = strtotime($nextDay[0]['end']) - strtotime($nextDay[0]['start']); | |
| 552 | 540 | $reserveTime = $end - $start; |
| 553 | 541 | |
| 554 | - while ($period - $reserveTime <= $nextDayEnd) { | |
| 542 | + while ($period - $reserveTime <= $nextDayEndTime) { | |
| 555 | 543 | $startTime = gmdate('H:i', $start); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date |
| 556 | 544 | $nextDayStart = gmdate('H:i', $interval - $reserveTime); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date |
| 557 | 545 | $formattedSlots[] = $startTime; |
| 558 | 546 | |
| @@ -590,20 +578,20 @@ | ||
| 590 | 578 | { |
| 591 | 579 | $this->hostId = $hostId; |
| 592 | 580 | |
| 593 | 581 | $event = $this->calendarSlot; |
| 594 | - $calendar = $this->calendar; | |
| 595 | 582 | $duration = $event->getDuration($duration); |
| 596 | 583 | |
| 597 | - $startDate = $this->adjustStartDate($startDate, $timeZone); | |
| 584 | + $adjustedDate = $this->adjustStartDate($startDate, $timeZone); | |
| 598 | 585 | |
| 599 | 586 | $isDisplaySpots = $event->is_display_spots; |
| 600 | 587 | $isMultiGuest = $event->isMultiGuestEvent(); |
| 601 | - $endDate = $event->getMaxBookableDateTime($startDate, $timeZone); | |
| 588 | + $isMultiBooking = $event->isAdditionalGuestEnabled(); | |
| 589 | + $endDate = $event->getMaxBookableDateTime($adjustedDate, $timeZone); | |
| 602 | 590 | $startDate = $event->getMinBookableDateTime($startDate, $timeZone); |
| 603 | 591 | |
| 604 | 592 | $maxBooking = false; |
| 605 | - if ($isDisplaySpots && $isMultiGuest) { | |
| 593 | + if ($isMultiGuest && ($isDisplaySpots || $isMultiBooking)) { | |
| 606 | 594 | $maxBooking = $event->getMaxBookingPerSlot(); |
| 607 | 595 | } |
| 608 | 596 | |
| 609 | 597 | if (strtotime($startDate) > strtotime($endDate)) { |
| @@ -670,9 +658,9 @@ | ||
| 670 | 658 | } |
| 671 | 659 | } |
| 672 | 660 | |
| 673 | 661 | $convertedSpots = array_map(function ($spots) { |
| 674 | - return array_values($spots); | |
| 662 | + return array_values(ksort($spots) ? $spots : $spots); | |
| 675 | 663 | }, $convertedSpots); |
| 676 | 664 | |
| 677 | 665 | return $convertedSpots; |
| 678 | 666 | } |
| @@ -730,9 +718,9 @@ | ||
| 730 | 718 | |
| 731 | 719 | return $rangeArray; |
| 732 | 720 | } |
| 733 | 721 | |
| 734 | - private function maybeBookingFrequencyLimitRanges($ranges, $bookedSlots) | |
| 722 | + private function maybeBookingFrequencyLimitRanges($ranges) | |
| 735 | 723 | { |
| 736 | 724 | if (!$ranges) { |
| 737 | 725 | return $ranges; |
| 738 | 726 | } |
| @@ -787,39 +775,12 @@ | ||
| 787 | 775 | } |
| 788 | 776 | } |
| 789 | 777 | } |
| 790 | 778 | } |
| 791 | - | |
| 792 | - // Per Day Booking Frequency Limit Hanlder | |
| 793 | - if (!empty($keyedFrequenceyLimits['per_day'])) { | |
| 794 | - $perDayLimit = $keyedFrequenceyLimits['per_day']; | |
| 795 | - foreach ($ranges as $rangeIndex => $rangeDate) { | |
| 796 | - if (!isset($bookedSlots[$rangeDate])) { | |
| 797 | - continue; | |
| 798 | - } | |
| 799 | - | |
| 800 | - $dayBooked = array_filter($bookedSlots[$rangeDate], function ($slot) { | |
| 801 | - return Arr::get($slot, 'event_id') == $this->calendarSlot->id; | |
| 802 | - }); | |
| 803 | - | |
| 804 | - if (!$dayBooked) { | |
| 805 | - continue; | |
| 806 | - } | |
| 807 | - | |
| 808 | - if (count($dayBooked) >= $perDayLimit) { | |
| 809 | - unset($ranges[$rangeIndex]); | |
| 810 | - } | |
| 811 | - | |
| 812 | - if (!$ranges) { | |
| 813 | - return []; | |
| 814 | - } | |
| 815 | - } | |
| 816 | - } | |
| 817 | - | |
| 818 | 779 | return $ranges; |
| 819 | 780 | } |
| 820 | 781 | |
| 821 | - private function maybeBookingDurationLimitRanges($ranges, $bookedSlots, $duration) | |
| 782 | + private function maybeBookingDurationLimitRanges($ranges, $duration) | |
| 822 | 783 | { |
| 823 | 784 | if (!$ranges) { |
| 824 | 785 | return $ranges; |
| 825 | 786 | } |
| @@ -866,35 +827,153 @@ | ||
| 866 | 827 | } |
| 867 | 828 | } |
| 868 | 829 | } |
| 869 | 830 | } |
| 831 | + return $ranges; | |
| 832 | + } | |
| 870 | 833 | |
| 871 | - // Per Day Booking Frequency Limit Hanlder | |
| 872 | - if (!empty($keyedLimits['per_day'])) { | |
| 873 | - $perDayLimit = $keyedLimits['per_day']; | |
| 874 | - foreach ($ranges as $rangeIndex => $rangeDate) { | |
| 875 | - if (!isset($bookedSlots[$rangeDate])) { | |
| 876 | - 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); | |
| 877 | 889 | } |
| 890 | + return $carry; | |
| 891 | + }, 0); | |
| 878 | 892 | |
| 879 | - $dayDurarion = 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); | |
| 882 | - } | |
| 883 | - return $carry; | |
| 884 | - }, 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 | + } | |
| 885 | 904 | |
| 886 | - if (!$dayDurarion) { | |
| 887 | - 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 | + })); | |
| 888 | 944 | } |
| 945 | + if (!$isMultiSlot || !count($slots)) { | |
| 946 | + unset($rangesSlots[$rangeDate]); | |
| 947 | + } | |
| 948 | + } | |
| 949 | + } | |
| 889 | 950 | |
| 890 | - if ($dayDurarion + $duration > $perDayLimit) { | |
| 891 | - unset($ranges[$rangeIndex]); | |
| 892 | - } | |
| 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; | |
| 893 | 972 | } |
| 894 | 973 | } |
| 895 | 974 | |
| 896 | - return $ranges; | |
| 975 | + return $convertedSlots; | |
| 897 | 976 | } |
| 898 | 977 | |
| 899 | 978 | public function getFilledWeeks($from, $to, $weekStart = '') |
| 900 | 979 | { |
| @@ -957,8 +1036,15 @@ | ||
| 957 | 1036 | ->where('event_id', $this->calendarSlot->id) |
| 958 | 1037 | ->whereBetween('start_time', [$start, $end]) |
| 959 | 1038 | ->whereIn('status', ['scheduled', 'completed']) |
| 960 | 1039 | ->sum('slot_minutes'); |
| 1040 | + } | |
| 1041 | + | |
| 1042 | + protected function getMaxBookingTimestamp($fromDate, $toDate, $timeZone) | |
| 1043 | + { | |
| 1044 | + $maxBookingTime = $this->calendarSlot->getMaxBookableDateTime($toDate, $timeZone, 'Y-m-d H:i:s'); | |
| 1045 | + | |
| 1046 | + return strtotime($maxBookingTime); | |
| 961 | 1047 | } |
| 962 | 1048 | |
| 963 | 1049 | protected function getTimezoneInfo($hostId = null) |
| 964 | 1050 | { |