hostId = null; $this->groupedSlots = []; $this->calendar = $calendar; $this->calendarSlot = $calendarSlot; } public function getDates($fromDate = false, $toDate = false, $duration = null, $isDoingBooking = false, $timeZone = 'UTC') { $duration = $this->calendarSlot->getDuration($duration); $fromDate = $fromDate ?: gmdate('Y-m-d'); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $toDate = $toDate ?: gmdate('Y-m-t 23:59:59', strtotime($fromDate)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $ranges = $this->getCurrentDateRange($fromDate, $toDate); $bookedSlots = $this->getBookedSlots([$fromDate, $toDate], 'UTC', $isDoingBooking); $ranges = $this->maybeBookingFrequencyLimitRanges($ranges, $bookedSlots); $ranges = $this->maybeBookingDurationLimitRanges($ranges, $bookedSlots, $duration); $cutOutTime = DateTimeHelper::getTimestamp() + $this->calendarSlot->getCutoutSeconds(); $maxBookingTime = strtotime($this->calendarSlot->getMaxBookableDateTime($fromDate, $timeZone, 'Y-m-d H:i:s')); $timezoneInfo = $this->getTimezoneInfo(); $rangedSlots = $this->getRangedValidSlots($ranges, $duration, $bookedSlots, $cutOutTime, $maxBookingTime, $timezoneInfo); if (!$this->groupedSlots) { return $rangedSlots; } return $this->adjustGroupedSlots($ranges, $rangedSlots, $cutOutTime, $maxBookingTime, $timezoneInfo); } protected function getRangedValidSlots($ranges, $duration, $bookedSlots, $cutOutTime, $maxBookingTime, $timezoneInfo, $rangedSlots = [], $hostId = null) { $period = $duration * 60; $hostId = $hostId ?: $this->hostId; $bufferTime = $this->calendarSlot->getTotalBufferTime() * 60; $daySlots = $this->getWeekDaySlots($duration, $hostId); $dateOverrides = $this->calendarSlot->getDateOverrides($hostId); list($scheduleTimezone, $dstTime) = $timezoneInfo; $todayDate = gmdate('Y-m-d'); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $lastDate = end($ranges); foreach ($ranges as $date) { $day = strtolower(gmdate('D', strtotime($date))); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $availableSlots = $daySlots[$day] ?? []; $availableSlots = $this->maybeDateOverrides($dateOverrides, $availableSlots, $date, $duration); if (!$availableSlots) { continue; } $currentBookedSlots = $bookedSlots[$date] ?? []; $isToday = $date === $todayDate; $isLastDay = $date === $lastDate; $validSlots = []; foreach ($availableSlots as $start) { $end = gmdate('H:i', strtotime($start) + $period); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $endDate = $start < $end ? $date : gmdate('Y-m-d', strtotime($date) + 86400); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $slot = [ 'start' => $date . ' ' . $start . ':00', 'end' => $endDate . ' ' . $end . ':00' ]; $slot = $this->maybeDayLightSavingSlot($slot, $dstTime, $scheduleTimezone); if ($isToday && strtotime($slot['start']) < $cutOutTime) { continue; } if ($isLastDay && strtotime($slot['end']) > $maxBookingTime) { continue; } $isSlotAvailable = $this->isSlotAvailable($slot, $currentBookedSlots, $bufferTime, $hostId); if ($isSlotAvailable) { $validSlots[] = $slot; } } if ($validSlots) { $currentSlots = $rangedSlots[$date] ?? []; $rangedSlots[$date] = $this->mergeAndSortSlots($currentSlots, $validSlots); } } return $rangedSlots; } protected function isSlotAvailable(&$slot, $currentBookedSlots, $bufferTime, $hostId) { if (!$currentBookedSlots) { return true; } $startTimeStamp = strtotime($slot['start']); $endTimeStamp = strtotime($slot['end']); foreach ($currentBookedSlots as $bookedSlot) { $bookedStart = strtotime($bookedSlot['start']); $bookedEnd = strtotime($bookedSlot['end']); if (Arr::get($bookedSlot, 'source')) { $bookedStart = $bookedStart - $bufferTime; $bookedEnd = $bookedEnd + $bufferTime; } if ( ($startTimeStamp >= $bookedStart && $startTimeStamp < $bookedEnd) || ($endTimeStamp > $bookedStart && $endTimeStamp <= $bookedEnd) || ($startTimeStamp <= $bookedStart && $endTimeStamp > $bookedStart) || ($startTimeStamp < $bookedEnd && $endTimeStamp >= $bookedEnd) ) { if (!Arr::get($bookedSlot, 'remaining')) { return false; } $slot['remaining'] = $bookedSlot['remaining']; } } return true; } protected function adjustGroupedSlots($ranges, $rangedSlots, $cutOutTime, $maxBookingTime, $timezoneInfo) { list($scheduleTimezone, $dstTime) = $timezoneInfo; $todayDate = gmdate('Y-m-d'); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $lastDate = end($ranges); $addedDates = []; foreach ($this->groupedSlots as $slot) { $slot = $this->maybeDayLightSavingSlot($slot, $dstTime, $scheduleTimezone); $date = gmdate('Y-m-d', strtotime($slot['start'])); if ($todayDate == $date && strtotime($slot['start']) < $cutOutTime) { continue; } if ($lastDate == $date && strtotime($slot['end']) > $maxBookingTime) { continue; } if (!isset($rangedSlots[$date])) { $rangedSlots[$date] = []; $rangedSlots[$date][] = $slot; continue; } $addedDates[$date] = $date; $rangedSlots[$date][] = $slot; } foreach ($addedDates as $date) { $dateSlots = $rangedSlots[$date]; usort($dateSlots, function ($a, $b) { return strtotime($a['start']) - strtotime($b['start']); }); $rangedSlots[$date] = $dateSlots; } return $rangedSlots; } public function isSpotAvailable($fromTime, $toTime, $duration = null, $hostId = null) { $this->hostId = $hostId; $fromTimeStamp = strtotime($fromTime); $toTimeStamp = strtotime($toTime); $fromTime = gmdate('Y-m-d 00:00:00', $fromTimeStamp); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $toTime = gmdate('Y-m-d 23:59:59', $toTimeStamp); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $duration = $this->calendarSlot->getDuration($duration); $slots = $this->getDates($fromTime, $toTime, $duration, true); $fromDate = gmdate('Y-m-d', $fromTimeStamp); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $toDate = gmdate('Y-m-d', $toTimeStamp); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $availableSlots = $slots[$fromDate] ?? []; if ($fromDate != $toDate) { $availableSlots = array_merge($availableSlots, $slots[$toDate] ?? []); } $left = 0; $right = count($availableSlots) - 1; while ($left <= $right) { $mid = $left + (($right - $left) >> 1); $midStartTime = strtotime($availableSlots[$mid]['start']); $midEndTime = strtotime($availableSlots[$mid]['end']); if ($fromTimeStamp == $midStartTime && $toTimeStamp == $midEndTime) { return true; } elseif ($fromTimeStamp > $midStartTime) { $left = $mid + 1; } else { $right = $mid - 1; } } return false; } protected function getCurrentDateRange($startDate = false, $endDate = false) { if (!$startDate) { $startDate = gmdate('Y-m-d'); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date } if (!$endDate) { $endDate = gmdate('Y-m-t 23:59:59', strtotime($startDate)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date } $currentDate = strtotime($startDate); $endDate = strtotime($endDate); $oneDay = 24 * 60 * 60; $dateArray = []; while ($currentDate <= $endDate) { $dateArray[] = gmdate('Y-m-d', $currentDate); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $currentDate += $oneDay; } return $dateArray; } protected function bookSlot($eventId, $start, $end, $remaining = 0, $source = null) { return [ 'event_id' => $eventId, 'start' => $start, 'end' => $end, 'remaining' => $remaining, 'source' => $source ]; } protected function getBookedSlots($dateRange, $toTimeZone = 'UTC', $isDoingBooking = false) { if ($toTimeZone != 'UTC') { $dateRange[0] = DateTimeHelper::convertToUtc($dateRange[0], $toTimeZone); $dateRange[1] = DateTimeHelper::convertToUtc($dateRange[1], $toTimeZone); } $hostIds = $this->calendarSlot->getHostIds($this->hostId); $status = ['pending', 'reserved', 'approved', 'scheduled', 'completed']; $bookings = Booking::with(['calendar_event']) ->whereHas('hosts', function ($query) use ($hostIds) { $query->whereIn('user_id', $hostIds); }) ->whereBetween('start_time', $dateRange) ->orderBy('start_time', 'ASC') ->whereIn('status', $status) ->get() ->groupBy('group_id'); $maxBooking = $this->calendarSlot->getMaxBookingPerSlot(); $isGroupBooking = $maxBooking > 1; $books = $this->processBookings($bookings, $toTimeZone, $maxBooking, $isGroupBooking); $books = apply_filters('fluent_booking/local_booked_events', $books, $this->calendarSlot, $toTimeZone, $dateRange, $isDoingBooking); $remoteBookings = apply_filters('fluent_booking/remote_booked_events', [], $this->calendarSlot, $toTimeZone, $dateRange, $this->hostId, $isDoingBooking); $books = $this->processRemoteBookings($books, $remoteBookings); return apply_filters('fluent_booking/booked_events', $books, $this->calendarSlot, $toTimeZone, $dateRange, $isDoingBooking); } protected function processBookings($bookings, $toTimeZone, $maxBooking, $isGroupBooking) { $books = []; foreach ($bookings as $booking) { $booked = $booking->count(); $booking = $booking[0]; $remaining = 0; if ($this->calendarSlot->id == $booking->event_id) { if ($booking->status == 'reserved') { continue; } $remaining = max(0, $maxBooking - $booked); } if ($toTimeZone != 'UTC') { $booking->start_time = DateTimeHelper::convertToTimeZone($booking->start_time, 'UTC', $toTimeZone); $booking->end_time = DateTimeHelper::convertToTimeZone($booking->end_time, 'UTC', $toTimeZone); } $date = gmdate('Y-m-d', strtotime($booking->start_time)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $books[$date] = $books[$date] ?? []; $bufferTime = $booking->calendar_event->getTotalBufferTime(); if ($bufferTime) { $beforeBufferTime = gmdate('Y-m-d H:i:s', strtotime($booking->start_time . " -$bufferTime minutes")); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $afterBufferTime = gmdate('Y-m-d H:i:s', strtotime($booking->end_time . " +$bufferTime minutes")); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date if ($remaining) { if ($beforeBufferTime < $booking->start_time) { $books[$date][] = $this->bookSlot(null, $beforeBufferTime, $booking->start_time); } if ($afterBufferTime > $booking->end_time) { $books[$date][] = $this->bookSlot(null, $booking->end_time, $afterBufferTime); } } else { $booking->start_time = $beforeBufferTime; $booking->end_time = $afterBufferTime; } } $rangedItems = $this->createDateRangeArrayFromSlotConfig([ 'event_id' => $booking->event_id, 'start' => $booking->start_time, 'end' => $booking->end_time, 'remaining' => $remaining ]); $eventIdAdded = false; foreach ($rangedItems as $date => $slot) { if ($isGroupBooking && $remaining && $this->calendarSlot->id == $booking->event_id) { $this->groupedSlots[] = $slot; if ($eventIdAdded) { $slot['event_id'] = null; } $eventIdAdded = true; } $books[$date] = $books[$date] ?? []; $books[$date][] = $slot; } } return $books; } protected function processRemoteBookings($books, $remoteBookings) { if (!$remoteBookings) { return $books; } foreach ($remoteBookings as $slot) { $rangedItems = $this->createDateRangeArrayFromSlotConfig([ 'start' => $slot['start'], 'end' => $slot['end'], 'source' => $slot['source'] ]); foreach ($rangedItems as $rangedDate => $rangedSlot) { $books[$rangedDate] = $books[$rangedDate] ?? []; if (!$this->isLocalBooking($books[$rangedDate], $rangedSlot)) { $books[$rangedDate][] = $rangedSlot; } } } return $books; } protected function getWeekDaySlots($duration, $hostId = null) { $period = $duration * 60; $hostId = $hostId ?: $this->hostId; $interval = $this->calendarSlot->getSlotInterval($duration) * 60; $weeklySlots = $this->calendarSlot->getWeeklySlots($hostId); $items = $this->getEnabledSlots($weeklySlots); // create range of each day slots from $items array above with $period minutes interval $formattedSlots = []; $days = array_keys($items); foreach ($items as $day => &$slots) { $daySlots = []; foreach ($slots as $slot) { $slot['end'] = ($slot['end'] == '00:00') ? '24:00' : $slot['end']; $start = strtotime($slot['start']); $end = strtotime($slot['end']); while ($start + $period <= $end) { $daySlots[] = gmdate('H:i', $start); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $start += $interval; } if ($slot['end'] == '24:00' && $start < $end) { $daySlots = $this->handleNextDaySlot($daySlots, $items, $end, $start, $interval, $period, $day, $days); } } if ($daySlots) { $formattedSlots[$day] = $daySlots; } } return $formattedSlots; } private function getEnabledSlots($weeklySlots) { $items = []; foreach ($weeklySlots as $weekDay => $weeklySlot) { if ($weeklySlot['enabled'] || !empty($weeklySlot['slots'])) { $items[$weekDay] = $weeklySlot['slots']; } } return $items; } protected function handleNextDaySlot($daySlots, &$items, $start, $end, $interval, $period, $day, $days) { $nextDayIndex = array_search($day, $days) + 1; if (isset($days[$nextDayIndex])) { $nextDay = $items[$days[$nextDayIndex]]; if ($nextDay && $nextDay[0]['start'] == '00:00') { $nextDayEnd = strtotime($nextDay[0]['end']); $reserveTime = $end - $start; while ($period - $reserveTime <= $nextDayEnd) { $startTime = gmdate('H:i', $start); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $nextDayStart = gmdate('H:i', $interval - $reserveTime); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $daySlots[] = $startTime; if ($nextDayStart < $startTime) { $items[$days[$nextDayIndex]][0]['start'] = $nextDayStart; break; } $start += $interval; $reserveTime = $end - $start; } } } return $daySlots; } protected function maybeDateOverrides($dateOverrides, $availableSlots, $date, $duration) { if (!$dateOverrides) { return $availableSlots; } list($overrideSlots, $overrideDays) = $dateOverrides; if ($overrideDays && isset($overrideDays[$date])) { $availableSlots = $this->removeOverrideSlots($availableSlots, $overrideDays[$date]); } if ($overrideSlots && isset($overrideSlots[$date])) { $flatOverrideSlots = $this->convertSlotSetsToFlat($overrideSlots, $date, $duration); $availableSlots = array_merge($availableSlots, $flatOverrideSlots); $availableSlots = $this->sortDaySlots($availableSlots); } return $availableSlots; } protected function convertSlotSetsToFlat(&$overrideSlots, $date, $duration = null) { $period = $this->calendarSlot->getDuration($duration) * 60; $interval = $this->calendarSlot->getSlotInterval($duration) * 60; $formattedSlots = []; $slotSets = $overrideSlots[$date]; foreach ($slotSets as $slot) { $slot['end'] = ($slot['end'] == '00:00') ? '24:00' : $slot['end']; $start = strtotime($slot['start']); $end = strtotime($slot['end']); while ($start + $period <= $end) { $formattedSlots[] = gmdate('H:i', $start); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $start += $interval; } if ($slot['end'] == '24:00' && $start < $end) { $formattedSlots = $this->handleNextDayOverrideSlot($formattedSlots, $start, $end, $interval, $period, $overrideSlots, $date); } } return $formattedSlots; } protected function handleNextDayOverrideSlot($formattedSlots, $start, $end, $interval, $period, &$overrideSlots, $date) { $nextDayIndex = gmdate('Y-m-d', strtotime($date) + 86400); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date if (isset($overrideSlots[$nextDayIndex])) { $nextDay = $overrideSlots[$nextDayIndex]; if ($nextDay && $nextDay[0]['start'] == '00:00') { $nextDayEnd = strtotime($nextDay[0]['end']); $reserveTime = $end - $start; while ($period - $reserveTime <= $nextDayEnd) { $startTime = gmdate('H:i', $start); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $nextDayStart = gmdate('H:i', $interval - $reserveTime); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $formattedSlots[] = $startTime; if ($startTime > $nextDayStart) { $overrideSlots[$nextDayIndex][0]['start'] = $nextDayStart; break; } $start += $interval; $reserveTime = $end - $start; } } } return $formattedSlots; } protected function removeOverrideSlots($availableSlots, $overrideDay) { if (!$availableSlots || !$overrideDay) { return $availableSlots; } $startTime = strtotime($overrideDay['start']); $endTime = strtotime($overrideDay['end']); $filteredSlots = array_filter($availableSlots, function ($slot) use ($startTime, $endTime) { return strtotime($slot) < $startTime || strtotime($slot) >= $endTime; }); return $filteredSlots; } public function getAvailableSpots($startDate, $timeZone = 'UTC', $duration = null, $hostId = null) { $this->hostId = $hostId; $event = $this->calendarSlot; $calendar = $this->calendar; $duration = $event->getDuration($duration); $startDate = $this->adjustStartDate($startDate, $timeZone); $isDisplaySpots = $event->is_display_spots; $isMultiGuest = $event->isMultiGuestEvent(); $endDate = $event->getMaxBookableDateTime($startDate, $timeZone); $startDate = $event->getMinBookableDateTime($startDate, $timeZone); $maxBooking = false; if ($isDisplaySpots && $isMultiGuest) { $maxBooking = $event->getMaxBookingPerSlot(); } if (strtotime($startDate) > strtotime($endDate)) { return new \WP_Error('invalid_date_range', __('Invalid date range', 'fluent-booking')); } $slots = $this->getDates($startDate, $endDate, $duration, false, $timeZone); return $this->convertSpots($slots, $startDate, 'UTC', $timeZone, $maxBooking); } protected function adjustStartDate($startDate, $timeZone) { $requestedDate = $startDate; $startDate = DateTimeHelper::convertToUtc($startDate, $timeZone); $currentDateTime = gmdate('Y-m-d H:i:s'); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date if (strtotime($startDate) < strtotime($currentDateTime)) { $startDate = $currentDateTime; } // Extract month and year from the timezone converted start date and requested date list($startDateMonth, $startDateYear) = $this->extractMonthAndYear($startDate); list($requestedDateMonth, $requestedDateYear) = $this->extractMonthAndYear($requestedDate); if ($startDateYear < $requestedDateYear || $startDateMonth < $requestedDateMonth) { $startDate = gmdate('Y-m-01 00:00:00', strtotime($requestedDate)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date } return $startDate; } protected function convertSpots($slots, $startDate, $fromTimeZone = 'UTC', $toTimeZone = 'UTC', $maxBooking = false) { $minBookableTimestamp = strtotime($startDate); $convertedSpots = []; foreach ($slots as $spots) { foreach ($spots as $spot) { $start = $spot['start']; $end = $spot['end']; if (strtotime($start) < $minBookableTimestamp) { continue; } if ($fromTimeZone != $toTimeZone) { $start = DateTimeHelper::convertToTimeZone($spot['start'], 'UTC', $toTimeZone); $end = DateTimeHelper::convertToTimeZone($spot['end'], 'UTC', $toTimeZone); } $spotStartDate = gmdate('Y-m-d', strtotime($start)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $convertedSpots[$spotStartDate] = $convertedSpots[$spotStartDate] ?? []; $remainingSlots = $maxBooking ? Arr::get($spot, 'remaining', $maxBooking) : false; $convertedSpots[$spotStartDate][$start] = [ 'start' => $start, 'end' => $end, 'remaining' => $remainingSlots, ]; } } $convertedSpots = array_map(function ($spots) { return array_values($spots); }, $convertedSpots); return $convertedSpots; } private function extractMonthAndYear($date) { $month = gmdate('m', strtotime($date)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $year = gmdate('Y', strtotime($date)); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date return [$month, $year]; } protected function createDateRangeArrayFromSlotConfig($slotConfig = []) { if (empty($slotConfig['start']) || empty($slotConfig['end'])) { return []; } $startTime = $slotConfig['start']; $endTime = $slotConfig['end']; if (gmdate('Ymd', strtotime($startTime)) == gmdate('Ymd', strtotime($endTime))) { return [ gmdate('Y-m-d', strtotime($startTime)) => $this->bookSlot(Arr::get($slotConfig, 'event_id'), $startTime, $endTime, Arr::get($slotConfig, 'remaining'), Arr::get($slotConfig, 'source')) ]; } $start = new \DateTime($startTime); $end = new \DateTime($endTime); // Set the end time to the end of the day if it's set to the beginning of a day if ($end->format('H:i:s') === '00:00:00') { $end->modify('-1 second'); // This will set the time to 23:59:59 of the previous day } $interval = new \DateInterval('P1D'); $dateRange = new \DatePeriod($start, $interval, $end); $rangeArray = []; foreach ($dateRange as $date) { $dateKey = $date->format('Y-m-d'); if ($date->format('Y-m-d') === $start->format('Y-m-d')) { $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')); } elseif ($date->format('Y-m-d') === $end->format('Y-m-d')) { $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')); } else { $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')); } } // Add the last day if it was not included in the loop if ($end->format('Y-m-d') !== $start->format('Y-m-d')) { $lastDayKey = $end->format('Y-m-d'); $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')); } return $rangeArray; } private function maybeBookingFrequencyLimitRanges($ranges, $bookedSlots) { if (!$ranges) { return $ranges; } $isBookingFrequencyEnabled = !!Arr::get($this->calendarSlot->settings, 'booking_frequency.enabled'); if (!$isBookingFrequencyEnabled) { return $ranges; } $keyedFrequenceyLimits = []; $frequenceyLimits = Arr::get($this->calendarSlot->settings, 'booking_frequency.limits', []); foreach ($frequenceyLimits as $limit) { if (!empty($limit['value'])) { $keyedFrequenceyLimits[$limit['unit']] = $limit['value']; } } // Per Month Booking Frequency Limit Hanlder if (!empty($keyedFrequenceyLimits['per_month'])) { $startDate = gmdate('Y-m-01 00:00:00', strtotime(min($ranges))); $endDate = gmdate('Y-m-t 23:59:59', strtotime(min($ranges))); $monthlyLimit = (int)$keyedFrequenceyLimits['per_month']; $monthlyCount = $this->getBookingsTotal($startDate, $endDate); if ($monthlyCount >= $monthlyLimit) { return []; } } // Per Week Booking Frequency Limit Hanlder if (!empty($keyedFrequenceyLimits['per_week'])) { if (!$ranges) { return []; } $weeklyLimit = (int)$keyedFrequenceyLimits['per_week']; $filledWeeks = $this->getFilledWeeks(min($ranges), max($ranges)); foreach ($filledWeeks as $filledWeek) { $weeklyCount = $this->getBookingsTotal($filledWeek[0] . ' 00:00:00', $filledWeek[6] . ' 23:59:59'); if ($weeklyCount >= $weeklyLimit) { $ranges = array_filter($ranges, function ($rangeDate) use ($filledWeek) { return !in_array($rangeDate, $filledWeek); }); if (!$ranges) { return []; } } } } // Per Day Booking Frequency Limit Hanlder if (!empty($keyedFrequenceyLimits['per_day'])) { $perDayLimit = $keyedFrequenceyLimits['per_day']; foreach ($ranges as $rangeIndex => $rangeDate) { if (!isset($bookedSlots[$rangeDate])) { continue; } $dayBooked = array_filter($bookedSlots[$rangeDate], function ($slot) { return Arr::get($slot, 'event_id') == $this->calendarSlot->id; }); if (!$dayBooked) { continue; } if (count($dayBooked) >= $perDayLimit) { unset($ranges[$rangeIndex]); } if (!$ranges) { return []; } } } return $ranges; } private function maybeBookingDurationLimitRanges($ranges, $bookedSlots, $duration) { if (!$ranges) { return $ranges; } if (!Arr::get($this->calendarSlot->settings, 'booking_duration.enabled')) { return $ranges; } $limits = Arr::get($this->calendarSlot->settings, 'booking_duration.limits', []); $keyedLimits = []; foreach ($limits as $limit) { if (!empty($limit['value'])) { $keyedLimits[$limit['unit']] = (int)$limit['value']; } } // Per Month Booking Frequency Limit Hanlder if (!empty($keyedLimits['per_month'])) { $startDate = gmdate('Y-m-01 00:00:00', strtotime(min($ranges))); $endDate = gmdate('Y-m-t 23:59:59', strtotime(min($ranges))); $monthlyDuration = $this->getBookingDurationTotal($startDate, $endDate); if ($monthlyDuration + $duration > $keyedLimits['per_month']) { $ranges = []; } } // Per Week Booking Frequency Limit Hanlder if (!empty($keyedLimits['per_week'])) { $weeklyLimit = (int)$keyedLimits['per_week']; $filledWeeks = $this->getFilledWeeks(min($ranges), max($ranges)); foreach ($filledWeeks as $filledWeek) { $weeklyDuration = $this->getBookingDurationTotal($filledWeek[0] . ' 00:00:00', $filledWeek[6] . ' 23:59:59'); if ($weeklyDuration + $duration > $weeklyLimit) { $ranges = array_filter($ranges, function ($rangeDate) use ($filledWeek) { return !in_array($rangeDate, $filledWeek); }); if (!$ranges) { return []; } } } } // Per Day Booking Frequency Limit Hanlder if (!empty($keyedLimits['per_day'])) { $perDayLimit = $keyedLimits['per_day']; foreach ($ranges as $rangeIndex => $rangeDate) { if (!isset($bookedSlots[$rangeDate])) { continue; } $dayDurarion = array_reduce($bookedSlots[$rangeDate], function ($carry, $slot) { if (Arr::get($slot, 'event_id') == $this->calendarSlot->id) { $carry += (int)((strtotime($slot['end']) - strtotime($slot['start'])) / 60); } return $carry; }, 0); if (!$dayDurarion) { continue; } if ($dayDurarion + $duration > $perDayLimit) { unset($ranges[$rangeIndex]); } } } return $ranges; } public function getFilledWeeks($from, $to, $weekStart = '') { $weekStart = $weekStart ? $weekStart : Arr::get(Helper::getGlobalSettings(), 'administration.start_day', 'sun'); $startDate = new DateTime($from); $endDate = new DateTime($to); if (strtolower($startDate->format('D')) != $weekStart) { $startDate->modify('last ' . $weekStart); } $weeks = []; while ($startDate <= $endDate) { // get all days in this week $week = []; for ($i = 0; $i < 7; $i++) { $week[] = $startDate->format('Y-m-d'); $startDate->modify('+1 day'); } $weeks[] = $week; } return $weeks; } protected function getBookingsTotal($start, $end) { if ($this->calendarSlot->event_type == 'group') { return Booking::query() ->where('event_id', $this->calendarSlot->id) ->whereBetween('start_time', [$start, $end]) ->whereIn('status', ['scheduled', 'completed']) ->groupBy('group_id') ->count(); } return Booking::query() ->where('event_id', $this->calendarSlot->id) ->whereBetween('start_time', [$start, $end]) ->whereIn('status', ['scheduled', 'completed']) ->count(); } protected function getBookingDurationTotal($start, $end) { if ($this->calendarSlot->event_type == 'group') { return Booking::query() ->select(['group_id', 'slot_minutes']) ->where('event_id', $this->calendarSlot->id) ->whereBetween('start_time', [$start, $end]) ->whereIn('status', ['scheduled', 'completed']) ->groupBy('group_id') ->get() ->sum('slot_minutes'); } return Booking::query() ->where('event_id', $this->calendarSlot->id) ->whereBetween('start_time', [$start, $end]) ->whereIn('status', ['scheduled', 'completed']) ->sum('slot_minutes'); } protected function getTimezoneInfo($hostId = null) { $hostId = $hostId ?: $this->hostId; $scheduleTimezone = $this->calendarSlot->getScheduleTimezone($hostId); $dstTime = DateTimeHelper::getDaylightSavingTime($scheduleTimezone); return [$scheduleTimezone, $dstTime]; } protected function maybeDayLightSavingSlot($slot, $dstTime, $scheduleTimezone, $adjustSign = '-') { if (!$dstTime) { return $slot; } $slot['start'] = $this->maybeDayLightSavingTime($slot['start'], $dstTime, $scheduleTimezone, $adjustSign); $slot['end'] = $this->maybeDayLightSavingTime($slot['end'], $dstTime, $scheduleTimezone, $adjustSign); return $slot; } protected function maybeDayLightSavingTime($time, $dstTime, $timezone, $adjustSign = '+') { $scheduleTime = DateTimeHelper::convertToTimeZone($time, 'UTC', $timezone); if (DateTimeHelper::isDaylightSavingActive($scheduleTime, $timezone)) { $time = gmdate('Y-m-d H:i:s', strtotime($time . " $adjustSign $dstTime minutes")); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date } return $time; } protected function isLocalBooking($bookings, $slot) { if (empty($bookings)) { return false; } foreach ($bookings as $book) { if ($book['start'] == $slot['start'] && $book['end'] == $slot['end']) { return true; } } return false; } protected function mergeAndSortSlots($currentSlots, $validSlots) { if (!$currentSlots) { return $validSlots; } $mergedSlots = array_merge($currentSlots, $validSlots); usort($mergedSlots, function ($a, $b) { return strtotime($a['start']) - strtotime($b['start']); }); return $mergedSlots; } protected function sortDaySlots($daySlots) { usort($daySlots, function ($a, $b) { return strtotime($a) - strtotime($b); }); return $daySlots; } }