$options['id'] ?? 0]) { // load full booking details $options = VikBooking::getBookingInfoFromID($options['id']); } if (empty($options['id'])) { throw new Exception('Missing booking ID.', 500); } // bind booking options to internal registry $this->bind($options); if (!$rooms) { // load booking rooms $rooms = VikBooking::loadOrdersRoomsData($this->getID()); } if (!$rooms) { throw new Exception('No booking rooms found.', 404); } // 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. * * @param bool $roomLevel True to calculate the nights at room-level. * * @return int */ public function getTotalNights(bool $roomLevel = false) { if ($roomLevel) { // get the stay timestamps at room-level $stayTimestamps = $this->getStayTimestamps(true); // return the room-level nights of stay return VikBooking::getAvailabilityInstance()->countNightsOfStay($stayTimestamps[0], $stayTimestamps[1]) ?: 1; } // return the booking nights of stay 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; } /** * Sets a value for the requested registry property name. * * @param string $name The registry property to set. * @param mixed $value The value to set. * * @return self */ public function setProperty(string $name, $value) { $this->registry[$name] = $value; return $this; } /** * 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 whole or provider-alias DAC data-registry. * * @param ?string $providerAlias Optional DAC provider alias identifier. * * @return array */ public function getDACData(?string $providerAlias = null) { if ($providerAlias) { return $this->dacData[$providerAlias] ?? []; } return $this->dacData; } /** * Returns the requested DAC data-registry property name. * * @param string $providerAlias The DAC provider alias identifier. * @param string $name The DAC registry property to fetch. * @param mixed $default The default value to return. * * @return mixed */ public function getDACProperty(string $providerAlias, string $name, $default = null) { return $this->dacData[$providerAlias][$name] ?? $default; } /** * Sets a value within the DAC data-registry for a given property name. * * @param string $providerAlias The DAC provider alias identifier. * @param string $name The DAC registry property to set. * @param mixed $value The property value to set. * * @return self */ public function setDACProperty(string $providerAlias, string $name, $value) { if (!isset($this->dacData[$providerAlias])) { // start a new container $this->dacData[$providerAlias] = []; } // set the value for the requested property name $this->dacData[$providerAlias][$name] = $value; return $this; } /** * Returns the booking rooms data. * * @return array */ public function getRooms() { return $this->bookingRooms; } /** * Returns the booked listing IDs. * * @param bool $unique True to only get a unique list. * * @return array List of booked room ID integers, unique or not. */ public function getBookedListingIds(bool $unique = true) { if (!$unique) { return array_map('intval', array_column($this->bookingRooms, 'idroom')); } return array_values(array_unique(array_map('intval', array_column($this->bookingRooms, 'idroom')))); } /** * Returns a list of booked listing IDs and subunits (0 if no subunit). * * @param bool $unique True to only get a unique list. * * @return array Linear array of strings as "roomid-subunit". * * @since 1.18.7 (J) - 1.8.7 (WP) */ public function getBookedListingSubunits(bool $unique = true) { $bookedListingSubunits = []; foreach ($this->bookingRooms as $bookingRoom) { // push booked listing ID and related subunit number $bookedListingSubunits[] = sprintf('%d-%d', $bookingRoom['idroom'], (int) ($bookingRoom['roomindex'] ?? 0)); } if ($unique) { // remove duplicate entries on listing-level with no subunits $bookedListingSubunits = array_values(array_unique($bookedListingSubunits)); } return $bookedListingSubunits; } /** * Counts the number of total adults, either at booking or room level. * * @param bool $roomLevel True to count the adults of the current room. * * @return int * * @since 1.18.7 (J) - 1.8.7 (WP) */ public function countTotalAdults(bool $roomLevel = false) { if ($roomLevel) { foreach ($this->bookingRooms as $index => $bookingRoom) { if ($index == $this->getCurrentRoomIndex()) { return (int) ($bookingRoom['adults'] ?? 0); } } } return array_sum(array_map('intval', array_column($this->bookingRooms, 'adults'))); } /** * Counts the number of total children, either at booking or room level. * * @param bool $roomLevel True to count the children of the current room. * * @return int * * @since 1.18.7 (J) - 1.8.7 (WP) */ public function countTotalChildren(bool $roomLevel = false) { if ($roomLevel) { foreach ($this->bookingRooms as $index => $bookingRoom) { if ($index == $this->getCurrentRoomIndex()) { return (int) ($bookingRoom['children'] ?? 0); } } } return array_sum(array_map('intval', array_column($this->bookingRooms, 'children'))); } /** * Counts the number of total pets, either at booking or room level. * * @param bool $roomLevel True to count the pets of the current room. * * @return int * * @since 1.18.7 (J) - 1.8.7 (WP) */ public function countTotalPets(bool $roomLevel = false) { if ($roomLevel) { foreach ($this->bookingRooms as $index => $bookingRoom) { if ($index == $this->getCurrentRoomIndex()) { return (int) ($bookingRoom['pets'] ?? 0); } } } return array_sum(array_map('intval', array_column($this->bookingRooms, 'pets'))); } /** * Counts the number of total guests, either at booking or room level. * * @param bool $roomLevel True to count the guests of the current room. * * @return int * * @since 1.18.7 (J) - 1.8.7 (WP) */ public function countTotalGuests(bool $roomLevel = false) { return $this->countTotalAdults($roomLevel) + $this->countTotalChildren($roomLevel); } /** * Returns the booking or customer country code, if any. * * @return ?string * * @since 1.18.8 (J) - 1.8.8 (WP) */ public function getCountry() { // give higher priority to booking-level country $countryCode = $this->getProperty('country'); if (!$countryCode) { // fetch booking customer details $customer = $this->getCustomer(); // check customer-level country $countryCode = ($customer['country'] ?? '') ?: $countryCode; } return $countryCode ?: null; } /** * Returns the booking or customer phone number, if any. * Note that the + prefix may not be included. * * @return ?string * * @since 1.18.8 (J) - 1.8.8 (WP) */ public function getPhoneNumber() { // give higher priority to booking-level phone number $phoneNumber = $this->getProperty('phone'); if (!$phoneNumber) { // fetch booking customer details $customer = $this->getCustomer(); // check customer-level phone number $phoneNumber = ($customer['phone'] ?? '') ?: $phoneNumber; } // trim phone number $phoneNumber = (string) $phoneNumber; // sanitize number by keeping the + char only if at the beginning of the string $phoneNumber = preg_replace('/(?!^)\+|[^0-9+]/', '', $phoneNumber); if (substr($phoneNumber, 0, 2) === '00') { // safely convert 00 to + $phoneNumber = substr_replace($phoneNumber, '+', 0, 2); } return $phoneNumber ?: null; } /** * Returns the booking or customer email address, if any. * * @return ?string * * @since 1.18.8 (J) - 1.8.8 (WP) */ public function getEmailAddress() { // give higher priority to booking-level email address $emailAddr = $this->getProperty('custmail'); if (!$emailAddr) { // fetch booking customer details $customer = $this->getCustomer(); // check customer-level email address $emailAddr = ($customer['email'] ?? '') ?: $emailAddr; } // trim email address $emailAddr = (string) $emailAddr; return $emailAddr ?: null; } /** * Returns the booking OTA type-data information, if any. * * @return array Empty or associative array. * * @since 1.18.8 (J) - 1.8.8 (WP) */ public function getOTATypeData() { $typeData = $this->getProperty('ota_type_data'); if ($typeData && is_scalar($typeData)) { $typeData = (array) json_encode($typeData, true); } return $typeData ?: []; } /** * Returns the previous booking data. * * @return array */ public function getPrevious() { return $this->previousBooking; } /** * Sets the previous booking data. * * @param array $previous Previous booking assoc data. * * @return self * * @since 1.18.11 (J) - 1.8.11 (WP) */ public function setPrevious(array $previous) { $this->previousBooking = $previous; return $this; } /** * 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; } /** * Gets the current room number (1-based index). * * @return int * * @since 1.18.7 (J) - 1.8.7 (WP) */ public function getCurrentRoomNumber() { return $this->currentRoomNumber; } /** * Sets the current room number (1-based index). * * @param int $number The current room number. * * @return void * * @since 1.18.7 (J) - 1.8.7 (WP) */ public function setCurrentRoomNumber(int $number) { $this->currentRoomNumber = $number; } /** * Gets the room ID from the current room index set. * * @return int * * @since 1.18.7 (J) - 1.8.7 (WP) */ public function getCurrentRoomID() { foreach ($this->getRooms() as $index => $bookingRoom) { if ($index == $this->currentRoomIndex) { return (int) $bookingRoom['idroom']; } } return (int) (($this->getRooms()[0]['idroom'] ?? 0) ?: 0); } /** * Returns the details for the given room ID or for all rooms set. * * @param ?int $listingId Optional listing ID to get. * * @return array * * @since 1.18.7 (J) - 1.8.7 (WP) */ public function getRoomDetails(?int $listingId = null) { if ($listingId && !($this->roomDetails[$listingId] ?? [])) { // obtain the information for the requested listing ID $listingDetails = VikBooking::getRoomInfo($listingId, ['name', 'img', 'units', 'params'], true); if ($listingDetails) { // cache value $this->setRoomDetails($listingId, $listingDetails); } } if (!$listingId) { return $this->roomDetails; } return $this->roomDetails[$listingId] ?? []; } /** * Sets the details for the given room ID. * * @param int $listingId The listing ID to update. * @param array $data The details data to set. * * @return self * * @since 1.18.7 (J) - 1.8.7 (WP) */ public function setRoomDetails(int $listingId, array $data) { $this->roomDetails[$listingId] = $data; return $this; } /** * Gets the booking customer details. * * @return array * * @since 1.18.7 (J) - 1.8.7 (WP) */ public function getCustomer() { if ($this->customer === null) { // load and cache customer details $this->customer = VikBooking::getCPinInstance()->getCustomerFromBooking($this->getID()); } return $this->customer; } /** * Sets the booking customer details. * * @param array $customer Raw customer details. * * @return self * * @since 1.18.7 (J) - 1.8.7 (WP) */ public function setCustomer(array $customer) { $this->customer = $customer; return $this; } /** * Gets the booking customer data: nominative, logo, provenience name. * * @return array Numeric list of booking-customer data value strings. * * @since 1.18.7 (J) - 1.8.7 (WP) */ public function getBookingCustomerData() { // starting values $customer_nominative = ''; $booking_avatar_src = ''; $booking_avatar_alt = ''; if ($this->customer === null) { // attempt to load customer details first $this->getCustomer(); } if (!empty($this->customer['first_name']) || !empty($this->customer['last_name'])) { // check if we need to display a profile picture or a channel logo if (!empty($this->customer['pic'])) { // customer profile picture $booking_avatar_src = strpos($this->customer['pic'], 'http') === 0 ? $this->customer['pic'] : VBO_SITE_URI . 'resources/uploads/' . $this->customer['pic']; $booking_avatar_alt = basename($booking_avatar_src); } elseif ($this->getProperty('idorderota') && $this->getProperty('channel')) { // channel logo $logo_helper = VikBooking::getVcmChannelsLogo($this->getProperty('channel'), $get_istance = true); if ($logo_helper !== false) { $booking_avatar_src = $logo_helper->getSmallLogoURL(); $booking_avatar_alt = $logo_helper->provenience; } } if (!empty($booking_avatar_src)) { // make sure the alt attribute is not too long in case of broken images $booking_avatar_alt = !empty($booking_avatar_alt) && strlen($booking_avatar_alt) > 15 ? '...' . substr($booking_avatar_alt, -12) : $booking_avatar_alt; } // customer name $customer_fullname = trim($this->customer['first_name'] . ' ' . $this->customer['last_name']); if (strlen($customer_fullname) > 26) { if (function_exists('mb_substr')) { $customer_fullname = trim(mb_substr($customer_fullname, 0, 26, 'UTF-8')) . '..'; } else { $customer_fullname = trim(substr($customer_fullname, 0, 26)) . '..'; } } $customer_nominative = $customer_fullname; } else { // parse the customer data string $custdata_parts = explode("\n", (string) $this->getProperty('custdata')); $enoughinfo = false; if (count($custdata_parts) > 2 && strpos($custdata_parts[0], ':') !== false && strpos($custdata_parts[1], ':') !== false) { // get the first two fields $custvalues = array(); foreach ($custdata_parts as $custdet) { if (strlen($custdet) < 1) { continue; } $custdet_parts = explode(':', $custdet); if (count($custdet_parts) >= 2) { unset($custdet_parts[0]); array_push($custvalues, trim(implode(':', $custdet_parts))); } if (count($custvalues) > 1) { break; } } if (count($custvalues) > 1) { $enoughinfo = true; $customer_nominative = trim(implode(' ', $custvalues)); if (strlen($customer_nominative) > 26) { if (function_exists('mb_substr')) { $customer_nominative = trim(mb_substr($customer_nominative, 0, 26, 'UTF-8')) . '..'; } else { $customer_nominative = trim(substr($customer_nominative, 0, 26)) . '..'; } } if ($this->getProperty('idorderota') && $this->getProperty('channel')) { // add support for the channel logo for the imported OTA reservations with no customer record $logo_helper = VikBooking::getVcmChannelsLogo($this->getProperty('channel'), $get_istance = true); if ($logo_helper !== false) { $booking_avatar_src = $logo_helper->getSmallLogoURL(); $booking_avatar_alt = $logo_helper->provenience; // make sure the alt attribute is not too long in case of broken images $booking_avatar_alt = !empty($booking_avatar_alt) && strlen($booking_avatar_alt) > 15 ? '...' . substr($booking_avatar_alt, -12) : $booking_avatar_alt; } } } } if (!$enoughinfo) { $customer_nominative = '#' . $this->getID(); } } return [ $customer_nominative, $booking_avatar_src, $booking_avatar_alt, ]; } /** * Returns the booking (or current room booking record) stay timestamps. * * @param bool $roomLevel True to return the stay timestamps at booking room level. * * @return array * * @since 1.18.7 (J) - 1.8.7 (WP) added argument $roomLevel. */ public function getStayTimestamps(bool $roomLevel = false) { if ($roomLevel) { // current room index signature for the stay timestamps $roomSignature = '_stay_timestamps' . $this->getCurrentRoomIndex(); if ($cachedTimestamps = $this->getProperty($roomSignature)) { // return the cached stay timestamps for the current room index return $cachedTimestamps; } // load room occupied records $room_stay_dates = VikBooking::getAvailabilityInstance(true)->loadSplitStayBusyRecords($this->getID()); // default room-level stay timestamps $roomLevelCheckin = $this->getProperty('checkin'); $roomLevelCheckout = $this->getProperty('checkout'); // split-stay booking or booked rooms for modified stay nights should rely on occupied records if ($room_stay_dates[$this->getCurrentRoomIndex()] ?? []) { // set booking-room-level stay timestamps $roomLevelCheckin = $room_stay_dates[$this->getCurrentRoomIndex()]['checkin']; $roomLevelCheckout = $room_stay_dates[$this->getCurrentRoomIndex()]['checkout']; } // build room-level stay timestamps list $roomLevelStayList = [ $roomLevelCheckin, $roomLevelCheckout, ]; // cache stay timestamps for the current room index $this->setProperty($roomSignature, $roomLevelStayList); // return booking-room-level stay timestamps, if different than global stay dates return $roomLevelStayList; } // global reservation stay timestamps 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(). * @param int $from_ts Optional from date period timestamp. * @param int $to_ts Optional to date period timestamp. * * @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. * * @param bool $roomLevel True to also detect alterations at room-level. * * @return bool False if no changes were actually proved, true otherwise. * * @since 1.18.7 (J) - 1.8.7 (WP) added argument $roomLevel. */ public function detectAlterations(bool $roomLevel = false) { 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; } // attempt to also detect changes at room-level if ($roomLevel) { // check subunits $current_room_indexes = array_map('intval', array_column($this->getRooms(), 'roomindex')); $previous_room_indexes = array_map('intval', array_column((array) $this->getPreviousProperty('rooms_info', []), 'roomindex')); sort($current_room_indexes); sort($previous_room_indexes); // ensure room subunits information is available for current and previous booking if ($current_room_indexes && $previous_room_indexes && $current_room_indexes != $previous_room_indexes) { // room subunits alteration detected return true; } } // no significant changes to stay dates or listings could be proved return false; } /** * Tells if the current date and time is between the stay dates. * * @return bool */ public function isStaying() { $now = time(); return $this->getProperty('checkin', 0) <= $now && $now <= $this->getProperty('checkout', 0); } /** * Tells if the booking arrival date is today. * * @return bool */ public function isArrivingToday() { return date('Y-m-d', $this->getProperty('checkin', 0)) === date('Y-m-d'); } /** * Tells if the booking departure date is today. * * @return bool */ public function isDepartingToday() { return date('Y-m-d', $this->getProperty('checkout', 0)) === date('Y-m-d'); } /** * Tells if the booking arrival date and time is in the future. * * @return bool */ public function isFuture() { return $this->getProperty('checkin', 0) > time(); } /** * Tells if the booking departure date and time is in the past. * * @return bool */ public function isPast() { return $this->getProperty('checkout', 0) < time(); } /** * Tells if the booking went through the pre-checkin process. * * @return bool * * @since 1.18.6 (J) - 1.8.6 (WP) */ public function hasPreCheckedIn() { // check registry protected value $pre_checkin = $this->getProperty('_precheckin'); if (!is_int($pre_checkin)) { // fetch the actual status and cache it internally $pre_checkin = (int) boolval(VikBooking::getBookingHistoryInstance($this->getID())->hasEvent('PC')); $this->registry['_precheckin'] = $pre_checkin; } return (bool) $pre_checkin; } /** * Tells if the booking comes from an OTA. * * @return bool * * @since 1.18.8 (J) - 1.8.8 (WP) */ public function isFromOTA() { $otaId = $this->getProperty('idorderota', null); $channel = $this->getProperty('channel', null); return !empty($otaId) && !empty($channel); } /** * Tells if the booking is fully paid as of now. * Note that OTA payouts may still need to be received, * or an outstanding payment may still be due in case of upselling events. * Do NOT rely on this method alone to determine if payments are needed. * * @return bool Even if true, there could be pending payments. * * @see getOutstandingBalance() * * @since 1.18.8 (J) - 1.8.8 (WP) */ public function isFullyPaid() { // get current totals $bookingTotal = (float) $this->getProperty('total', 0); $otaCompensation = (float) $this->getProperty('cmms', 0); $amountPaid = (float) $this->getProperty('totpaid', 0); if ($this->isFromOTA()) { return ($bookingTotal - $amountPaid - $otaCompensation) < 1; } return $amountPaid >= $bookingTotal; } /** * Calculates the outstanding balance as of now, if any. * * @return ?float * * @since 1.18.8 (J) - 1.8.8 (WP) */ public function getOutstandingBalance() { // get current totals $bookingTotal = (float) $this->getProperty('total', 0); $otaCompensation = (float) $this->getProperty('cmms', 0); $amountPaid = (float) $this->getProperty('totpaid', 0); $amountPayable = (float) $this->getProperty('payable', 0); $paymentCount = abs((int) $this->getProperty('paymcount', 0)); $bookingDamDep = (float) $this->getProperty('tot_damage_dep', 0); // check if the amounts look as fully paid $looksFullyPaid = $this->isFullyPaid(); if ($looksFullyPaid && !$amountPayable) { // no upselling events involved, nothing is due return null; } // obtain damage deposit details $damage_deposit_payment = VBORoomHelper::getInstance()->getDamageDepositSplitPayment($this->getData(), $this->getRooms()); // obtain previous damage deposit payments, if any $prev_dd_payments = []; if ($damage_deposit_payment['damagedep_gross'] ?? 0) { $prev_dd_payments = VikBooking::getBookingHistoryInstance($this->getID()) ->getEventsWithData('PN', function($data) { return (is_object($data) && !empty($data->damage_deposit)); }); if (!$prev_dd_payments) { // check also the first payment event in case of OTA bookings $prev_dd_payments = VikBooking::getBookingHistoryInstance($this->getID()) ->getEventsWithData('P0', function($data) { return (is_object($data) && !empty($data->damage_deposit)); }); } } // outstanding balance $outstanding = $bookingTotal - $amountPaid; if ($prev_dd_payments && $bookingDamDep) { // deduct previously paid damage deposit $outstanding -= $bookingDamDep; } // tell if the booking looks payable $isPayable = (($amountPaid > 0 && ($amountPaid + ($damage_deposit_payment['damagedep_gross'] ?? 0)) < $bookingTotal && $paymentCount) || $amountPayable > 0); // additional payment flags $otaWillPay = false; $payableLater = false; // determine booking pay-ability if ($isPayable && $this->isConfirmed() && $this->isFromOTA() && $amountPayable > 0 && !$prev_dd_payments) { if (($damage_deposit_payment['damagedep_gross'] ?? 0) == $amountPayable && ($damage_deposit_payment['payment_window']['pay_id'] ?? 0)) { // upselling event must have added the damage deposit to an OTA booking, and this is the only outstanding amount that will be paid separately $isPayable = false; // check if the damage deposit is payable later if (!($damage_deposit_payment['payment_window']['payable'] ?? 0) && ($damage_deposit_payment['payment_window']['payment_from_dt'] ?? null)) { // will be payable on a date in the future $payableLater = true; } } } if ($isPayable && $this->isFromOTA() && $otaCompensation && $looksFullyPaid) { // the difference of the amount paid is equal to the OTA commissions amount $isPayable = false; if ($amountPayable > 0 && $bookingTotal > $amountPaid && round(($bookingTotal - $amountPaid), 0) == round($amountPayable, 0)) { // there must have been an upselling event or a payment request with an amount equal to the OTA commissions $isPayable = true; } } if ($isPayable && $this->isFromOTA() && !$amountPayable) { // access OTA payout events $prev_ota_payments = VikBooking::getBookingHistoryInstance($this->getID()) ->getEventsWithData('PO', null, false); if (!$prev_ota_payments) { // the OTA will pay the remaining balance $isPayable = false; $otaWillPay = true; } } if ($outstanding && !$isPayable && $payableLater) { // deduct the damage deposit amount that will be payable in a future date $outstanding -= $bookingDamDep; } if ($outstanding && !$isPayable && $otaWillPay) { // deduct OTA commissions that has not been paid yet $outstanding -= $otaCompensation; } return $outstanding > 0 ? $outstanding : null; } /** * Returns a unique list of booked listing names. * * @return array Linear array of strings, if any. * * @since 1.18.8 (J) - 1.8.8 (WP) */ public function getListingNames() { $listingIds = $this->getBookedListingIds(); $roomNames = VikBooking::getAvailabilityInstance(true)->loadRooms($listingIds); return array_map(function($id) use ($roomNames) { return $roomNames[$id]['name'] ?? $id; }, $listingIds); } /** * Returns a unique list of booked listing addresses. * * @return array Linear array of strings, if any. * * @since 1.18.8 (J) - 1.8.8 (WP) */ public function getListingAddresses() { if (!class_exists('VCMOtaListing')) { // prevent errors when the CM is not installed return []; } $addresses = []; foreach ($this->getBookedListingIds() as $listingId) { $addresses[] = VCMOtaListing::getInstance()->getLocation($listingId)['address'] ?? null; } return array_values(array_unique($addresses)); } }