offering = $offering; // Info $this->info = $info; // DateTimes $this->sessions = $sessions; } /** * @return mixed[] */ public function getSessions(): array { return $this->sessions; } /** * @param mixed[] $sessions * @return OfferingOption */ public function setSessions(array $sessions): OfferingOption { $this->sessions = $sessions; return $this; } public function setAvailabilityExcludedBookingId(int $booking_id): OfferingOption { $this->availability_excluded_booking_id = absint($booking_id); return $this; } public function availabilityExcludedBookingId(): int { return $this->availability_excluded_booking_id; } public function setAvailabilityEngine(AvailabilityEngine $engine): OfferingOption { $this->availability_engine = $engine; return $this; } private function availabilityEngine(): AvailabilityEngine { return $this->availability_engine ?? new AvailabilityEngine(); } /** * @param AdventInterface $offering * @param int $option_id * @return OfferingOption|null */ public static function getById(AdventInterface $offering, int $option_id): ?OfferingOption { return (new Adapter($offering))->get($option_id); } public function id(): int { return (int) apply_filters('DayTidy/F__DayTidy/Options/Option__id/id', $this->info['id'] ?? 0); } public function title(): string { return (string) apply_filters('DayTidy/F__DayTidy/Options/Option__title/title', $this->info['title'] ?? 'N/A'); } public function price(): float { return (float) apply_filters('DayTidy/F__DayTidy/Options/Option__price/price', $this->info['price'] ?? 0); } public function capacity(): int { return (int) apply_filters('DayTidy/F__DayTidy/Options/Option__capacity/capacity', $this->info['count'] ?? 0); } public function publicDescription(): string { return (string) apply_filters( 'DayTidy/F__DayTidy/Options/Option__publicDescription/description', $this->info['public_description'] ?? 1 ); } public function privateDescription(): string { return (string) apply_filters( 'DayTidy/F__DayTidy/Options/Option__privateDescription/description', $this->info['private_description'] ?? 1 ); } public function minToBuy(): int { return (int) apply_filters('DayTidy/F__DayTidy/Options/Option__minToBuy/min', $this->info['min_buy'] ?? 0); } public function maxToBuy(): int { $maximum = trim($this->info['max_buy']) !== '' ? $this->info['max_buy'] : $this->defaultMaximum(); return (int) apply_filters('DayTidy/F__DayTidy/Options/Option__maxToBuy/max', $maximum); } public function hasMaxToBuy(): bool { return isset($this->info['max_buy']) && trim((string) $this->info['max_buy']) !== ''; } public function seats(): int { return (int) apply_filters('DayTidy/F__DayTidy/Options/Option__seats/seats', $this->info['seats'] ?? 1); } /** * @return mixed[] */ public function sellWindow(): array { return (array) apply_filters( 'DayTidy/F__DayTidy/Options/Option__sellWindow/window', $this->info['sell_window'] ?? [] ); } /** * @return array */ public function scheduling(): array { $raw = is_array($this->info['scheduling'] ?? null) ? $this->info['scheduling'] : []; $duration = is_array($raw['duration'] ?? null) ? $raw['duration'] : []; $days = absint($duration['days'] ?? 0); $hours = absint($duration['hours'] ?? 0); $minutes = absint($duration['minutes'] ?? 0); $duration_minutes = intdiv( $days * DAY_IN_SECONDS + $hours * HOUR_IN_SECONDS + $minutes * MINUTE_IN_SECONDS, MINUTE_IN_SECONDS ); $scheduling = [ 'duration' => [ 'days' => $days, 'hours' => $hours, 'minutes' => $minutes, ], 'duration_minutes' => absint($raw['duration_minutes'] ?? $duration_minutes), 'buffer_before_minutes' => absint($raw['buffer_before_minutes'] ?? 0), 'buffer_after_minutes' => absint($raw['buffer_after_minutes'] ?? 0), 'resource_selection_mode' => ResourceSelectionModes::normalize((string) ($raw['resource_selection_mode'] ?? ResourceSelectionModes::NONE)), 'eligible_resource_ids' => $this->normalizeResourceIds($raw['eligible_resource_ids'] ?? []), 'capacity_behavior' => OptionCapacityBehaviors::normalize((string) ($raw['capacity_behavior'] ?? OptionCapacityBehaviors::OPTION_COUNT)), ]; return (array) apply_filters( 'DayTidy/F__DayTidy/Options/Option__scheduling/scheduling', $scheduling, $this ); } /** * @return array */ public function duration(): array { $duration = $this->scheduling()['duration'] ?? []; $duration = is_array($duration) ? $duration : []; return [ 'days' => absint($duration['days'] ?? 0), 'hours' => absint($duration['hours'] ?? 0), 'minutes' => absint($duration['minutes'] ?? 0), ]; } public function durationMinutes(): int { return absint($this->scheduling()['duration_minutes'] ?? 0); } public function bufferBeforeMinutes(): int { return absint($this->scheduling()['buffer_before_minutes'] ?? 0); } public function bufferAfterMinutes(): int { return absint($this->scheduling()['buffer_after_minutes'] ?? 0); } public function resourceSelectionMode(): string { return ResourceSelectionModes::normalize((string) ($this->scheduling()['resource_selection_mode'] ?? ResourceSelectionModes::NONE)); } public function requiresResource(): bool { return $this->resourceSelectionMode() !== ResourceSelectionModes::NONE; } /** * @return int[] */ public function eligibleResourceIds(): array { return $this->normalizeResourceIds($this->scheduling()['eligible_resource_ids'] ?? []); } public function capacityBehavior(): string { return OptionCapacityBehaviors::normalize((string) ($this->scheduling()['capacity_behavior'] ?? OptionCapacityBehaviors::OPTION_COUNT)); } public function usesResourceCapacity(): bool { return in_array($this->capacityBehavior(), [ OptionCapacityBehaviors::RESOURCE_CAPACITY, OptionCapacityBehaviors::OPTION_AND_RESOURCE, ], true); } public function resourceRemaining(): int { if (!$this->requiresResource() || !count($this->sessions)) return 0; return $this->availabilityEngine()->availableCapacity( $this->offering, $this, $this->sessions, $this->availabilityExcludedBookingId() ); } /** * @return mixed[] */ public function windowTime(): array { // Raw Window $raw = $this->sellWindow(); $start_time = (int) ($raw['start']['time'] ?? 0); $start_interval = $raw['start']['type'] ?? 'days'; $start_pointer = $raw['start']['pointer'] ?? 'before_start'; $end_time = (int) ($raw['end']['time'] ?? 0); $end_interval = $raw['end']['type'] ?? 'days'; $end_pointer = $raw['end']['pointer'] ?? 'before_start'; $window = []; foreach ($this->sessions as $datetime) { $timestamp = strtotime($datetime); if (!$timestamp) continue; try { $window[$timestamp] = [ 'datetime' => $datetime, 'start' => $start_time ? Utils::timeMachine($timestamp, $start_time, $start_interval, $start_pointer === 'before_start' ? 'sub' : 'add')->format('Y-m-d H:i:s') : '', 'end' => $end_time ? Utils::timeMachine($timestamp, $end_time, $end_interval, $end_pointer === 'before_start' ? 'sub' : 'add')->format('Y-m-d H:i:s') : '', ]; } catch (Exception $e) { continue; } } return (array) apply_filters( 'DayTidy/F__DayTidy/Options/Option__windowTime/window', $window ); } public function remaining(): int { if ($this->requiresResource() && count($this->sessions) && $this->capacityBehavior() === OptionCapacityBehaviors::RESOURCE_CAPACITY) { return $this->resourceRemaining(); } $option_remaining = $this->optionRemaining(); if (!$this->requiresResource() || !count($this->sessions) || !$this->usesResourceCapacity()) return $option_remaining; $resource_remaining = $this->resourceRemaining(); if ($this->capacityBehavior() === OptionCapacityBehaviors::OPTION_AND_RESOURCE) { return min($option_remaining, $resource_remaining); } return $option_remaining; } public function optionRemaining(): int { [$booked,] = $this->totalBooked(); $capacity = $this->capacity(); return max(0, $capacity - $booked); } public function isSoldOut(): bool { return $this->remaining() === 0; } public function hasCapacity(int $quantity): bool { return $this->remaining() >= $quantity; } /** * @return int[] */ public function totalBooked(): array { $booked = $seats = 0; foreach ($this->sessions as $datetime) { $records = $this->availabilityEngine()->optionRecords($this->offering, $this, $datetime); $b = $s = 0; foreach ($records as $record) { $quantity = absint($record->quantity ?? 0); $b += $quantity; $s += absint($record->capacity_units ?? ($quantity * $this->seats())); } $booked = max($booked, $b); $seats = max($seats, $s); } return [$booked, $seats]; } /** * @param int $quantity * @return bool */ public function isAvailable(int $quantity = 0): bool { $availability = $this->isWithinWindow(); if ($availability) { $availability = $quantity ? $this->remaining() >= $quantity : $this->remaining() > 0; } $availability = $availability && $this->hasResourceAvailability($quantity); return (bool) apply_filters( 'DayTidy/F__DayTidy/Options/Option__isAvailable/availability', $availability ); } /** * @param int $quantity * @return bool */ public function isUnavailable(int $quantity = 0): bool { return !$this->isAvailable($quantity); } public function isWithinWindow(): bool { return $this->isWindowOpened() && !$this->isWindowClosed(); } public function isWindowOpened(): bool { try { $window = $this->windowTime(); foreach ($this->sessions as $datetime) { $timestamp = strtotime($datetime); if (!$timestamp) return false; $current = (new DateTime('now', $this->offering->getTimeZone()))->format('Y-m-d H:i:s'); $start = $window[$timestamp]['start'] ?? ''; $opened = Utils::timeInPeriod($current, $start); if (!$opened) return false; } return true; } catch (Exception $e) { return false; } } public function isWindowClosed(): bool { try { $window = $this->windowTime(); foreach ($this->sessions as $datetime) { $timestamp = strtotime($datetime); if (!$timestamp) return true; $current = (new DateTime('now', $this->offering->getTimeZone()))->format('Y-m-d H:i:s'); $end = $window[$timestamp]['end'] ?? ''; $closed = !Utils::timeInPeriod($current, '', $end); if ($closed) return true; } return false; } catch (Exception $e) { return true; } } public function getWindowOpenDatetime(): string { $window = $this->windowTime(); $datetime = ''; foreach ($window as $time) { if ($time['start'] && (!$datetime || strtotime($datetime) < strtotime($time['start']))) { $datetime = $time['start']; } } return $datetime; } public function getWindowCloseDatetime(): string { $window = $this->windowTime(); $datetime = ''; foreach ($window as $time) { if ($time['end'] && (!$datetime || strtotime($datetime) < strtotime($time['end']))) { $datetime = $time['end']; } } return $datetime; } public function getUnavailableMessage(int $quantity = 0): string { $reason = $this->getUnavailableReason($quantity); // Short Capacity if ($quantity && $reason === self::SHORT_CAPACITY) { // translators: Placeholder values. return sprintf(esc_html__('Only %1$s %2$s options are available, so you can\'t book %3$s at this time.', 'daytidy'), $this->remaining(), '' . $this->title() . '', $quantity); } // Sold Out // translators: Placeholder values. if ($reason === self::SOLD_OUT) return sprintf(esc_html__('The %s option is sold out! Please choose another option or date.', 'daytidy'), '' . $this->title() . ''); // Window not opened if ($reason === self::WINDOW_NOT_OPENED) { // translators: Placeholder values. $message = sprintf(esc_html__('Sales for the %s option have not started yet!', 'daytidy'), '' . $this->title() . ''); try { $datetime = $this->getWindowOpenDatetime(); if ($datetime) { // translators: Placeholder values. $message .= '
' . sprintf(esc_html__('Sales will begin on %s', 'daytidy'), '' . $this->offering->date($datetime) . ''); } } catch (Exception $e){} return '

' . $message . '

' . esc_html__('Please consider booking another date, time, or option.', 'daytidy') . '

'; } // Window Closed if ($reason === self::WINDOW_CLOSED) { // translators: Placeholder values. $message = sprintf(esc_html__('Sales for the %s option have ended.', 'daytidy'), '' . $this->title() . ''); try { $datetime = $this->getWindowCloseDatetime(); if ($datetime) { // translators: Placeholder values. $message .= '
' . sprintf(esc_html__('Sales closed on %s', 'daytidy'), '' . $this->offering->date($datetime) . ''); } } catch (Exception $e) { } return '

' . $message . '

' . esc_html__('You may book a different date, time, or option.', 'daytidy') . '

'; } // Resource Unavailable if ($reason === self::RESOURCE_UNAVAILABLE) { return sprintf( // translators: Placeholder values. esc_html__('No eligible resource is available for the %s option at this time. Please choose another date, time, or option.', 'daytidy'), '' . $this->title() . '' ); } // Not Available // translators: Placeholder values. return sprintf(esc_html__('The %s option is currently unavailable! Please select a different option or booking option.', 'daytidy'), '' . $this->title() . ''); } public function getUnavailableReason(int $quantity = 0): int { // Resource Unavailable if (!$this->hasResourceAvailability($quantity)) return self::RESOURCE_UNAVAILABLE; // Short Capacity if ($quantity && !$this->hasCapacity($quantity)) return self::SHORT_CAPACITY; // Sold Out if ($this->isSoldOut()) return self::SOLD_OUT; // Window not opened if (!$this->isWindowOpened()) return self::WINDOW_NOT_OPENED; // Window Closed if ($this->isWindowClosed()) return self::WINDOW_CLOSED; // Not Available return self::UNAVAILABLE; } public function minAllowed(): int { return min($this->minToBuy(), $this->remaining()); } public function maxAllowed(): int { return min($this->maxToBuy(), $this->remaining()); } /** * @param mixed $raw * @return int[] */ private function normalizeResourceIds($raw): array { if (!is_array($raw)) $raw = explode(',', (string) $raw); $ids = []; foreach ($raw as $resource_id) { $resource_id = absint($resource_id); if ($resource_id > 0) $ids[] = $resource_id; } return array_values(array_unique($ids)); } public function hasResourceAvailability(int $quantity = 0): bool { if (!$this->requiresResource() || !count($this->sessions)) return true; return $this->availabilityEngine()->optionAvailable( $this->offering, $this, $this->sessions, $quantity, $this->availabilityExcludedBookingId() ); } private function defaultMaximum(): int { if ($this->capacityBehavior() === OptionCapacityBehaviors::RESOURCE_CAPACITY && count($this->sessions)) { return $this->resourceRemaining(); } return $this->capacity(); } }