<?php /** * @package VikBooking * @subpackage core * @author E4J s.r.l. * @copyright Copyright (C) 2025 E4J s.r.l. All Rights Reserved. * @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL * @link https://vikwp.com */ // No direct access defined('ABSPATH') or die('No script kiddies please!'); /** * Task booking implementation. * * @since 1.18.0 (J) - 1.8.0 (WP) */ final class VBOTaskBooking { /** * @var array */ protected $registry = []; /** * @var array */ protected $bookingRooms = []; /** * @var array */ protected $previousBooking = []; /** * @var int */ protected $currentRoomIndex = 0; /** * Proxy to construct the object. * * @param array $options Associative list of booking information to bind. * @param array $rooms Associative list of booking rooms to bind. * @param array $previous Associative list of previous booking information to bind. * * @return VBOTaskBooking */ public static function getInstance(array $options, array $rooms = [], array $previous = []) { return new static($options, $rooms, $previous); } /** * Class constructor. * * @param array $options Associative list of booking information to bind. * @param array $rooms Associative list of booking rooms to bind. * @param array $previous Associative list of previous booking information to bind. * * @throws Exception */ public function __construct(array $options, array $rooms = [], array $previous = []) { if (empty($options['id'])) { throw new Exception('Missing booking ID.', 500); } // ensure we have enough booking details if ($options === ['id' => $options['id']]) { // load full booking details $options = VikBooking::getBookingInfoFromID($options['id']); } // bind booking options to internal registry $this->bind($options); if (!$rooms) { // load booking rooms $rooms = VikBooking::loadOrdersRoomsData($this->getID()); } // bind booking rooms $this->bookingRooms = $rooms; // bind previous booking information in case of alteration $this->previousBooking = $previous; } /** * Binds the given options onto the internal booking registry. * * @param array $options The booking options to bind. * * @return void */ public function bind(array $options) { $this->registry = array_merge($this->registry, $options); } /** * Returns the current booking ID. * * @return int */ public function getID() { return (int) $this->getProperty('id', 0); } /** * Returns the number of nights of stay for the current booking ID. * * @return int */ public function getTotalNights() { return (int) $this->getProperty('days', 1); } /** * Tells whether the booking is actually a closure reservation. * * @return bool */ public function isClosure() { return (bool) $this->getProperty('closure', 0); } /** * Tells whether the booking status is confirmed. * * @return bool */ public function isConfirmed() { return $this->getProperty('status', '') == 'confirmed'; } /** * Tells whether the booking status is pending (stand-by). * * @return bool */ public function isPending() { return $this->getProperty('status', '') == 'standby'; } /** * Tells whether the booking status is cancelled. * * @return bool */ public function isCancelled() { return $this->getProperty('status', '') == 'cancelled'; } /** * Tells whether the booking is flagged as overbooking. * * @return bool */ public function isOverbooking() { return $this->getProperty('type', '') == 'overbooking'; } /** * Returns the requested registry property name. * * @param string $name The registry property to fetch. * @param mixed $default The default value to return. * * @return mixed */ public function getProperty(string $name, $default = null) { return $this->registry[$name] ?? $default; } /** * Returns the requested previous booking property name. * * @param string $name The previous booking property to fetch. * @param mixed $default The default value to return. * * @return mixed */ public function getPreviousProperty(string $name, $default = null) { return $this->previousBooking[$name] ?? $default; } /** * Returns the booking data. * * @return array */ public function getData() { return $this->registry; } /** * Returns the booking rooms data. * * @return array */ public function getRooms() { return $this->bookingRooms; } /** * Returns the previous booking data. * * @return array */ public function getPrevious() { return $this->previousBooking; } /** * Gets the current room index. * * @return int */ public function getCurrentRoomIndex() { return $this->currentRoomIndex; } /** * Sets the current room index. * * @param int $index The current room index. * * @return void */ public function setCurrentRoomIndex(int $index) { $this->currentRoomIndex = $index; } /** * Returns the booking stay timestamps. * * @return array */ public function getStayTimestamps() { /** * @todo Do we need to do something different for split-stay bookings? * We are aware of the current room index when this method is called. */ return [ $this->getProperty('checkin'), $this->getProperty('checkout'), ]; } /** * Builds and returns the iterable date period interval for the nights of stay. * * @param string $duration The interval specification used for DateInterval::__construct(). * * @return DatePeriod */ public function buildStayPeriodInterval(string $duration = 'P1D', int $from_ts = 0, int $to_ts = 0) { if (empty($from_ts)) { $from_ts = $this->getProperty('checkin'); } if (empty($to_ts)) { $to_ts = $this->getProperty('checkout'); } // local timezone $tz = new DateTimezone(date_default_timezone_get()); // get date bounds $from_bound = new DateTime(date('Y-m-d H:i:s', $from_ts), $tz); $to_bound = new DateTime(date('Y-m-d H:i:s', $to_ts), $tz); // build iterable dates interval (period) $date_range = new DatePeriod( // start date included by default in the result set $from_bound, // interval between recurrences within the period new DateInterval($duration), // end date (check-out) excluded by default from the result set $to_bound ); return $date_range; } /** * Returns the iterable date period range of dates for the nights of stay. * * @return DatePeriod */ public function getStayPeriod() { if (($this->registry['stay_date_period'] ?? null) instanceof DatePeriod) { // return cached value return $this->registry['stay_date_period']; } // build iterable dates interval (period) $date_range = $this->buildStayPeriodInterval('P1D'); // cache value $this->bind(['stay_date_period' => $date_range]); return $date_range; } /** * Attempts to detect changes between the current and previous bookings. * * @return bool False if no changes were actually proved, true otherwise. */ public function detectAlterations() { if ($this->getProperty('checkin') != $this->getPreviousProperty('checkin')) { return true; } if ($this->getProperty('checkout') != $this->getPreviousProperty('checkout')) { return true; } if ($this->getProperty('days') != $this->getPreviousProperty('days')) { return true; } if ($this->getProperty('roomsnum') != $this->getPreviousProperty('roomsnum')) { return true; } // get the rooms booked with the current reservation $current_room_ids = array_column($this->getRooms(), 'idroom'); if (!$current_room_ids && is_array($this->getProperty('rooms_info'))) { $current_room_ids = array_column($this->getProperty('rooms_info'), 'idroom'); } // get the rooms booked with the previous reservation $previous_room_ids = array_column((array) $this->getPreviousProperty('rooms_info', []), 'idroom'); // map and sort both room lists $current_room_ids = array_map('intval', $current_room_ids); $previous_room_ids = array_map('intval', $previous_room_ids); sort($current_room_ids); sort($previous_room_ids); if (!$current_room_ids || !$previous_room_ids || $current_room_ids != $previous_room_ids) { return true; } // no significant changes to stay dates or listings could be proved return false; } }