Create New Item
Item Type
File
Folder
Item Name
Search file in folder and subfolders...
Are you sure want to rename?
File Manager
/
wp-content
/
plugins
/
vikbooking
/
admin
/
helpers
/
src
/
model
:
reservation.php
Advanced Search
Upload
New Item
Settings
Back
Back Up
Advanced Editor
Save
<?php /** * @package VikBooking * @subpackage core * @author E4J s.r.l. * @copyright Copyright (C) 2023 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!'); /** * VikBooking reservation model. * * @since 1.16.0 (J) - 1.6.0 (WP) */ class VBOModelReservation extends JObject { /** * The singleton instance of the class. * * @var VBOModelReservation */ private static $instance = null; /** * The total number of bookings found through the last search. * * @var int */ protected $totalBookings = 0; /** * Proxy for immediately getting the object and bind data. * * @param array|object $data optional data to bind. * @param boolean $anew true for forcing a new instance. * * @return self */ public static function getInstance($data = [], $anew = false) { if (is_null(static::$instance) || $anew) { static::$instance = new static($data); } return static::$instance; } /** * Sets the caller information used to save history records. * * @param string $caller The caller identifier. * * @return self * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function setCaller($caller = '') { $this->set('_caller', (string) $caller); return $this; } /** * Returns the caller information. * * @return string * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function getCaller() { return (string) $this->get('_caller', ''); } /** * Sets the history extra data value. * * @param array $data The history extra data array. * * @return self * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function setHistoryData(array $data = []) { $this->set('_historyData', $data); return $this; } /** * Returns the customer information. * * @return array * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function getHistoryData() { return (array) $this->get('_historyData', []); } /** * Sets the search filters. * * @param array $data The search filters associative array. * * @return self * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function setFilters(array $data = []) { $this->set('_filters', $data); return $this; } /** * Returns the search filters. * * @return array * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function getFilters() { return (array) $this->get('_filters', []); } /** * Sets the booking information record. * * @param array $booking The booking record. * * @return self * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function setBooking(array $booking = []) { $this->set('_booking', $booking); return $this; } /** * Returns the booking information record. * * @return array * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function getBooking() { return (array) $this->get('_booking', []); } /** * Sets the room booking records. * * @param array $room_booking The room booking records. * * @return self * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function setRoomBooking(array $room_booking = []) { $this->set('_roomBooking', $room_booking); return $this; } /** * Returns the room booking records. * * @return array * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function getRoomBooking() { return (array) $this->get('_roomBooking', []); } /** * Sets the customer information. * * @param array $customer the customer array. * * @return self */ public function setCustomer(array $customer = []) { $this->set('_customer', $customer); return $this; } /** * Returns the customer information. * * @return array */ public function getCustomer() { return (array) $this->get('_customer', []); } /** * Sets the room information. * * @param array $room the room array. * * @return self */ public function setRoom(array $room = []) { $this->set('_room', $room); return $this; } /** * Returns the room information. * * @return array */ public function getRoom() { return (array) $this->get('_room', []); } /** * Sets the new booking ID created. * * @param int $bid the newly added record ID. * * @return self */ protected function setNewBookingID($bid = 0) { $this->set('_newBookingID', $bid); return $this; } /** * Returns the new booking ID created, or 0. * * @return int */ public function getNewBookingID() { return (int) $this->get('_newBookingID', 0); } /** * Sets the VCM action to be performed in order to sync the availability. * * @param string $action the VCM action, usually an HTML link. * * @return self */ protected function setChannelManagerAction($action = '') { $this->set('_vcmAction', $action); return $this; } /** * Returns the VCM action (if any) to sync the availability. * * @return string */ public function getChannelManagerAction() { return $this->get('_vcmAction', ''); } /** * Sets the check-in and check-out times with hours and minutes. * * @return array list of check-in and check-out hours and minutes. */ public function loadCheckinOutTimes() { static $times_loaded = null; if ($times_loaded) { return [ $this->get('checkin_h'), $this->get('checkin_m'), $this->get('checkout_h'), $this->get('checkout_m'), ]; } $timeopst = VikBooking::getTimeOpenStore(); if (is_array($timeopst) && $timeopst) { $opent = VikBooking::getHoursMinutes($timeopst[0]); $closet = VikBooking::getHoursMinutes($timeopst[1]); $hcheckin = $opent[0]; $mcheckin = $opent[1]; $hcheckout = $closet[0]; $mcheckout = $closet[1]; } else { $hcheckin = 0; $mcheckin = 0; $hcheckout = 0; $mcheckout = 0; } $this->set('checkin_h', $hcheckin); $this->set('checkin_m', $mcheckin); $this->set('checkout_h', $hcheckout); $this->set('checkout_m', $mcheckout); $times_loaded = 1; return [ $hcheckin, $mcheckin, $hcheckout, $mcheckout, ]; } /** * Attempts to extract the Special Requests from the customer raw data. * * @return string * * @since 1.16.5 (J) - 1.6.5 (WP) */ public function extractSpecialRequests() { $raw_cust_data = $this->get('custdata', ''); if (empty($raw_cust_data)) { return ''; } $special_requests = ''; if (preg_match("/(?:special_?requests:\s*)(.*?)$/is", $raw_cust_data, $match)) { $special_requests = $match[1]; } elseif (preg_match("/(?:special_?request:\s*)(.*?)$/is", $raw_cust_data, $match)) { $special_requests = $match[1]; } elseif (preg_match("/(?:special_?request\s*)(.*?)$/is", $raw_cust_data, $match)) { $special_requests = $match[1]; } elseif (preg_match("/(?:" . JText::translate('ORDER_SPREQUESTS') . ":\s*)(.*?)$/is", $raw_cust_data, $match)) { $special_requests = $match[1]; } return $special_requests; } /** * Creates a new reservation record after having constructed the * object by properly injecting all the necessary booking information. * * @return bool */ public function create() { if (!$this->canCreate()) { $this->setError('Forbidden'); return false; } // availability helper $av_helper = VikBooking::getAvailabilityInstance(true); // validate mandatory fields $room = $this->getRoom(); if (!$this->get('checkin') || !$this->get('checkout') || empty($room['id'])) { $this->setError('Missing mandatory fields'); return false; } if ($this->get('checkin') >= $this->get('checkout')) { $this->setError('Invalid dates'); return false; } // make sure we have the time for check-in and check-out if (!$this->get('checkin_h') || !$this->get('checkout_h')) { // make sure to set the times (hours/minutes) for check-in and check-out $this->loadCheckinOutTimes(); // make sure check-in and check-out timestamps have been set to a proper time $from_info = getdate($this->get('checkin')); $to_info = getdate($this->get('checkout')); if ((int) $from_info['hour'] != (int) $this->get('checkin_h')) { $this->set('checkin', mktime((int) $this->get('checkin_h'), (int) $this->get('checkin_m'), 0, $from_info['mon'], $from_info['mday'], $from_info['year'])); } if ((int) $to_info['hour'] != (int) $this->get('checkout_h')) { $this->set('checkout', mktime((int) $this->get('checkout_h'), (int) $this->get('checkout_m'), 0, $to_info['mon'], $to_info['mday'], $to_info['year'])); } } // number of nights of stay if (!$this->get('nights')) { $this->set('nights', $av_helper->countNightsOfStay($this->get('checkin'), $this->get('checkout'))); } // fetch and apply turnover time before doing anything else $this->applyTurnover(); // if rate plan selected, get the tariff ID $this->loadTariffID(); // get pool of rooms involved $rooms_pool = $this->getRoomsPool(); if (!$rooms_pool) { if ($this->getError() === false) { // set generic error if not set already $this->setError('No rooms involved in the reservation'); } return false; } // check if the room is available $room_available = $this->isRoomAvailable(); if (!$this->get('force_booking', 0) && !$this->get('set_closed', 0) && !$room_available) { // no forcing, no closure and room fully booked results into an error message $this->setError(JText::translate('VBBOOKNOTMADE')); return false; } // detect if we are forcing the reservation $this->detectForcedReason($room_available); // store the customer information $this->storeCustomer(); // calculate total amount and total tax $this->calculateTotal(); // store booking and room-booking records if (!$this->storeReservationRecords($rooms_pool)) { if ($this->getError() === false) { // set generic error if not set already $this->setError('Could not create the reservation'); } return false; } return true; } /** * Searches for bookings according to specified filters. * * @return array * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function search() { $dbo = JFactory::getDbo(); $this->totalBookings = 0; $filters = $this->getFilters(); if (!$filters) { $this->setError('Missing filters to search for a booking.'); return []; } $q = $dbo->getQuery(true) ->select($dbo->qn('o') . '.*') ->select([ $dbo->qn('c.first_name', 'customer_first_name'), $dbo->qn('c.last_name', 'customer_last_name'), ]) ->from($dbo->qn('#__vikbooking_orders', 'o')) ->leftJoin($dbo->qn('#__vikbooking_customers_orders', 'co') . ' ON ' . $dbo->qn('co.idorder') . ' = ' . $dbo->qn('o.id')) ->leftJoin($dbo->qn('#__vikbooking_customers', 'c') . ' ON ' . $dbo->qn('c.id') . ' = ' . $dbo->qn('co.idcustomer')) ->where(1); if (($filters['booking_id'] ?? null)) { $q->andWhere([ $dbo->qn('o.id') . ' = ' . $dbo->q($filters['booking_id']), $dbo->qn('o.idorderota') . ' = ' . $dbo->q($filters['booking_id']), ], $glue = 'OR'); } if (($filters['status'] ?? null)) { $q->where($dbo->qn('o.status') . ' = ' . $dbo->q($filters['status'])); } if (($filters['exclude_closures'] ?? false)) { $q->where($dbo->qn('o.closure') . ' = 0'); } if (($filters['exclude_expired'] ?? false)) { // take only active reservations with a check-out date in the future $today_dt = JFactory::getDate('today', new DateTimeZone(date_default_timezone_get())); $q->where($dbo->qn('o.checkout') . ' >= ' . $dbo->q($today_dt->format('U', true))); } if (($filters['email'] ?? null)) { $q->where($dbo->qn('o.custmail') . ' = ' . $dbo->q($filters['email'])); } if (($filters['phone'] ?? null)) { $q->where(sprintf('REPLACE(%s, \' \', \'\') LIKE REPLACE(%s, \' \', \'\')', $dbo->qn('o.phone'), $dbo->q('%' . $filters['phone']) )); } if (($filters['date_range']['type'] ?? null) && (($filters['date_range']['start'] ?? null) || ($filters['date_range']['end'] ?? null))) { // search by date range $from_dt = JFactory::getDate(($filters['date_range']['start'] ?? $filters['date_range']['end'])); $from_dt->modify('00:00:00'); $to_dt = JFactory::getDate(($filters['date_range']['end'] ?? $filters['date_range']['start'])); $to_dt->modify('23:59:59'); // check the type of date if ($filters['date_range']['type'] == 'stay') { // find intersections of stay dates $q->andWhere([ '(' . $dbo->qn('o.checkin') . ' <= ' . $dbo->q($from_dt->format('U')) . ' AND ' . $dbo->qn('o.checkout') . ' >= ' . $dbo->q($to_dt->format('U')) . ')', '(' . $dbo->qn('o.checkin') . ' >= ' . $dbo->q($from_dt->format('U')) . ' AND ' . $dbo->qn('o.checkout') . ' <= ' . $dbo->q($to_dt->format('U')) . ')', '(' . $dbo->qn('o.checkin') . ' >= ' . $dbo->q($from_dt->format('U')) . ' AND ' . $dbo->qn('o.checkin') . ' < ' . $dbo->q($to_dt->format('U')) . ' AND ' . $dbo->qn('o.checkout') . ' >= ' . $dbo->q($to_dt->format('U')) . ')', '(' . $dbo->qn('o.checkin') . ' <= ' . $dbo->q($from_dt->format('U')) . ' AND ' . $dbo->qn('o.checkout') . ' > ' . $dbo->q($from_dt->format('U')) . ' AND ' . $dbo->qn('o.checkout') . ' <= ' . $dbo->q($to_dt->format('U')) . ')', ], $glue = 'OR'); } else { $column = $dbo->qn('o.checkin'); if ($filters['date_range']['type'] == 'checkout') { $column = $dbo->qn('o.checkout'); } elseif ($filters['date_range']['type'] == 'creation') { $column = $dbo->qn('o.ts'); } $q->where($column . ' >= ' . $dbo->q($from_dt->format('U'))); $q->where($column . ' <= ' . $dbo->q($to_dt->format('U'))); } } else { // check for single date filters if (($filters['creation_date'] ?? null)) { // dates are expected to be in military format $creation = JFactory::getDate($filters['creation_date']); $creation->modify('00:00:00'); $q->where($dbo->qn('o.ts') . ' >= ' . $dbo->q($creation->format('U'))); $creation->modify('23:59:59'); $q->where($dbo->qn('o.ts') . ' <= ' . $dbo->q($creation->format('U'))); } if (($filters['checkin_date'] ?? null) && !($filters['checkout_date'] ?? null)) { // dates are expected to be in military format $checkin = JFactory::getDate($filters['checkin_date']); $checkin->modify('00:00:00'); $q->where($dbo->qn('o.checkin') . ' >= ' . $dbo->q($checkin->format('U'))); $checkin->modify('23:59:59'); $q->where($dbo->qn('o.checkin') . ' <= ' . $dbo->q($checkin->format('U'))); } if (($filters['checkout_date'] ?? null) && !($filters['checkin_date'] ?? null)) { // dates are expected to be in military format $checkout = JFactory::getDate($filters['checkout_date']); $checkout->modify('00:00:00'); $q->where($dbo->qn('o.checkout') . ' >= ' . $dbo->q($checkout->format('U'))); $checkout->modify('23:59:59'); $q->where($dbo->qn('o.checkout') . ' <= ' . $dbo->q($checkout->format('U'))); } if (($filters['checkin_date'] ?? null) && ($filters['checkout_date'] ?? null)) { // range of dates (dates are expected to be in military format) $checkin = JFactory::getDate($filters['checkin_date']); $checkin->modify('00:00:00'); $checkout = JFactory::getDate($filters['checkout_date']); $checkout->modify('23:59:59'); $q->andWhere([ $dbo->qn('o.checkin') . ' BETWEEN ' . $dbo->q($checkin->format('U')) . ' AND ' . $dbo->q($checkout->format('U')), $dbo->qn('o.checkout') . ' BETWEEN ' . $dbo->q($checkin->format('U')) . ' AND ' . $dbo->q($checkout->format('U')), ], $glue = 'OR'); } if (($filters['stay_date'] ?? null)) { // dates are expected to be in military format $staydt = JFactory::getDate($filters['stay_date']); $staydt->modify('23:59:59'); $q->where($dbo->qn('o.checkin') . ' < ' . $dbo->q($staydt->format('U'))); $q->where($dbo->qn('o.checkout') . ' > ' . $dbo->q($staydt->format('U'))); } } if (($filters['customer_name'] ?? null)) { $q->where('CONCAT_WS(\' \', ' . $dbo->qn('c.first_name') . ', ' . $dbo->qn('c.last_name') . ') LIKE ' . $dbo->q('%' . $filters['customer_name'] . '%')); } if (($filters['confirmation_number'] ?? null)) { $q->where($dbo->qn('o.confirmnumber') . ' = ' . $dbo->q($filters['confirmation_number'])); } if (($filters['room_name'] ?? null)) { // find the room involved from the given name $room_record = VikBooking::getAvailabilityInstance()->getRoomByName($filters['room_name']); if ($room_record) { $q->leftJoin($dbo->qn('#__vikbooking_ordersrooms', 'or') . ' ON ' . $dbo->qn('or.idorder') . ' = ' . $dbo->qn('o.id')); $q->where($dbo->qn('or.idroom') . ' = ' . (int) $room_record['id']); } } /** * It is now possible to use a custom ordering. * * @since 1.17.1 (J) - 1.7.1 (WP) */ switch ($filters['ordering'] ?? 'id') { case 'creation': $ordering = 'o.ts'; break; case 'checkin': $ordering = 'o.checkin'; break; case 'checkout': $ordering = 'o.checkout'; break; default: $ordering = 'o.id'; } $q->order($dbo->qn($ordering) . ' ' . (strcasecmp($filters['direction'] ?? 'desc', 'desc') ? 'ASC' : 'DESC')); $dbo->setQuery($q, 0, ($filters['max_bookings'] ?? 0)); $rows = $dbo->loadAssocList(); $this->totalBookings = count($rows); /** * Calculate the total number of matching records. * * @since 1.17.1 (J) - 1.7.1 (WP) */ if ($this->totalBookings && $this->totalBookings == ($filters['max_bookings'] ?? 0)) { // set up the query used to count the matching records $dbo->setQuery($q->clear('select')->clear('offset')->clear('limit')->select('COUNT(1)')); $this->totalBookings = (int) $dbo->loadResult(); } return $rows; } /** * Returns the total number of bookings matching the last search query made. * * @return int * * @since 1.17.1 (J) - 1.7.1 (WP) */ public function getTotBookingsFound() { return $this->totalBookings; } /** * Modifies the requested booking ID according to the provided options. * This method does not support all rate plan options like for the creation of a * new booking. This is a method for making quick updates concerning a room switch, * a change of stay dates, new booking total amount, guests, add extra services etc.. * * @param array $options List of details to perform the modification. * * @return bool * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function modify(array $options) { $dbo = JFactory::getDbo(); // access the previous booking details $prev_booking = $this->getBooking(); // access the current rooms booked $roomBooking = $this->getRoomBooking(); // gather modification options $booking_id = $options['booking_id'] ?? $prev_booking['id'] ?? 0; if (!$booking_id) { $this->setError('Missing booking ID.'); return false; } if (!$prev_booking) { // load current booking record if not injected $prev_booking = VikBooking::getBookingInfoFromID($booking_id); if (!$prev_booking) { $this->setError('Booking not found.'); return false; } } if (!$roomBooking) { // load current rooms booked $roomBooking = VikBooking::loadOrdersRoomsData($booking_id); } // do not touch this array property because it's used by VCM $prev_booking['rooms_info'] = $roomBooking; // list of operations to trigger/perform $trigger_operations = []; // list of history description rows $history_descr_rows = []; // access availability helper $av_helper = VikBooking::getAvailabilityInstance(true); // calculate the new stay dates, if different $diff_stay_dates = false; $set_checkin = date('Y-m-d', $prev_booking['checkin']); $set_checkout = date('Y-m-d', $prev_booking['checkout']); if (($options['checkin'] ?? null)) { // date is expected in military format $diff_stay_dates = $diff_stay_dates || ($options['checkin'] != $set_checkin); $set_checkin = $options['checkin']; } if (($options['checkout'] ?? null)) { // date is expected in military format $diff_stay_dates = $diff_stay_dates || ($options['checkout'] != $set_checkout); $set_checkout = $options['checkout']; } // ensure the stay dates are valid if (JFactory::getDate($set_checkin) >= JFactory::getDate($set_checkout)) { $this->setError('Invalid stay dates provided.'); return false; } // ensure we are not changing dates for a split-stay reservation if ($diff_stay_dates && !empty($prev_booking['split_stay'])) { // we receive the stay dates at booking record, so we cannot proceed with the update $this->setError('Cannot modify the stay dates for a split-stay reservation. Please do it manually.'); return false; } // set dates involved $av_helper->setStayDates($set_checkin, $set_checkout); // count new nights of stay $set_nights = $av_helper->countNightsOfStay(); // gather stay timestamps list($set_checkin_ts, $set_checkout_ts) = $av_helper->getStayDates(true); // load the current busy record IDs before any modification, if any $dbo->setQuery( $dbo->getQuery(true) ->select('*') ->from($dbo->qn('#__vikbooking_ordersbusy')) ->where($dbo->qn('idorder') . ' = ' . (int) $booking_id) ); $busy_ids = array_column($dbo->loadAssocList(), 'idbusy'); // first off, check if any room switch was requested (recommended one switch at most) $switching_details = []; if ($options['switch_rooms'] ?? []) { // get all room IDs for the switch that were not booked already $booked_rooms = array_column($roomBooking, 'idroom'); $new_missing_rooms = array_values(array_diff((array) $options['switch_rooms'], $booked_rooms)); // scan all rooms requested for the switch that were not booked already foreach ($new_missing_rooms as $index => $switch_room_id) { if (!isset($roomBooking[$index])) { // adding more rooms is not supported break; } // ensure the room switch is allowed (room should be available on the new dates) $switched_room_info = VikBooking::getRoomInfo($switch_room_id, ['id', 'name', 'units']); if (!$switched_room_info) { $this->setError('The requested room could not be found for the switch.'); return false; } if (!VikBooking::roomBookable($switch_room_id, 1, $set_checkin_ts, $set_checkout_ts, $busy_ids)) { // abort by setting a descriptive error message $this->setError(sprintf( 'The room %s is not available from %s to %s, and so the room switch cannot be made.', $switched_room_info['name'] ?? '', $set_checkin, $set_checkout )); return false; } } // scan again all rooms to be switched once we know they are available foreach ($new_missing_rooms as $index => $switch_room_id) { if (!isset($roomBooking[$index])) { // adding more rooms is not supported break; } // update room-booking record by switching room ID $q = $dbo->getQuery(true) ->update($dbo->qn('#__vikbooking_ordersrooms')) ->set($dbo->qn('idroom') . ' = ' . (int) $switch_room_id) ->where($dbo->qn('idorder') . ' = ' . (int) $booking_id) ->where($dbo->qn('idroom') . ' = ' . (int) ($roomBooking[$index]['idroom'] ?? 0)); $dbo->setQuery($q, 0, 1); $dbo->execute(); if ($busy_ids) { // update busy records with new stay dates just for the switched room $q = $dbo->getQuery(true) ->update($dbo->qn('#__vikbooking_busy')) ->set($dbo->qn('idroom') . ' = ' . (int) $switch_room_id) ->set($dbo->qn('checkin') . ' = ' . $dbo->q($set_checkin_ts)) ->set($dbo->qn('checkout') . ' = ' . $dbo->q($set_checkout_ts)) ->set($dbo->qn('realback') . ' = ' . $dbo->q($set_checkout_ts + (VikBooking::getHoursRoomAvail() * 3600))) ->where($dbo->qn('id') . ' IN (' . implode(', ', array_map('intval', $busy_ids)) . ')') ->where($dbo->qn('idroom') . ' = ' . (int) ($roomBooking[$index]['idroom'] ?? 0)); $dbo->setQuery($q, 0, 1); $dbo->execute(); // register room switching details $switching_details[$index] = $switch_room_id; // register CM sync operation $trigger_operations[] = 'vcm_sync'; } // register history description row $switched_room_info = VikBooking::getRoomInfo($switch_room_id, ['id', 'name', 'units']); $prev_room_info = VikBooking::getRoomInfo($roomBooking[$index]['idroom'] ?? 0, ['id', 'name', 'units']); $history_descr_rows[] = sprintf('%s switched with %s.', $prev_room_info['name'] ?? '', $switched_room_info['name'] ?? ''); } } // start query builder for booking record $bookingQ = $dbo->getQuery(true) ->update($dbo->qn('#__vikbooking_orders')) ->where($dbo->qn('id') . ' = ' . (int) $booking_id); // modify stay dates, if requested if ($diff_stay_dates) { // ensure all rooms are bookable on the new stay dates if ($prev_booking['status'] == 'confirmed') { foreach ($roomBooking as $kor => $or) { if ($switching_details[$kor] ?? null) { // this room index was switched with another room, hence we know it was available continue; } if (!VikBooking::roomBookable($or['idroom'], 1, $set_checkin_ts, $set_checkout_ts, $busy_ids)) { // abort $abort_room_info = VikBooking::getRoomInfo($or['idroom'], ['id', 'name', 'units']); $this->setError(sprintf( 'The room %s is not available from %s to %s, and so the stay dates cannot be modified.', $abort_room_info['name'] ?? '', $set_checkin, $set_checkout )); return false; } } } // set booking values to update $bookingQ->set($dbo->qn('checkin') . ' = ' . $dbo->q($set_checkin_ts)); $bookingQ->set($dbo->qn('checkout') . ' = ' . $dbo->q($set_checkout_ts)); $bookingQ->set($dbo->qn('days') . ' = ' . $dbo->q($set_nights)); // update busy records, if any (reservation status could be confirmed) if ($busy_ids) { // update busy records with new stay dates for all rooms $dbo->setQuery( $dbo->getQuery(true) ->update($dbo->qn('#__vikbooking_busy')) ->set($dbo->qn('checkin') . ' = ' . $dbo->q($set_checkin_ts)) ->set($dbo->qn('checkout') . ' = ' . $dbo->q($set_checkout_ts)) ->set($dbo->qn('realback') . ' = ' . $dbo->q($set_checkout_ts + (VikBooking::getHoursRoomAvail() * 3600))) ->where($dbo->qn('id') . ' IN (' . implode(', ', array_map('intval', $busy_ids)) . ')') ); $dbo->execute(); // register CM sync operation $trigger_operations[] = 'vcm_sync'; } // register operation to trigger the shared calendars $trigger_operations[] = 'shared_calendars'; } // update number of guests, if requested if (($options['guests']['adults'] ?? null) || ($options['guests']['children'] ?? null)) { // update the requested number of guests ONLY on the first room booked $q = $dbo->getQuery(true) ->update($dbo->qn('#__vikbooking_ordersrooms')) ->set($dbo->qn('adults') . ' = ' . (int) ($options['guests']['adults'] ?? $roomBooking[0]['adults'])) ->set($dbo->qn('children') . ' = ' . (int) ($options['guests']['children'] ?? $roomBooking[0]['children'])) ->where($dbo->qn('idorder') . ' = ' . (int) $booking_id); $dbo->setQuery($q, 0, 1); $dbo->execute(); // register history description row $history_descr_rows[] = sprintf( 'New adults %d, new children %d.', (int) ($options['guests']['adults'] ?? $roomBooking[0]['adults']), (int) ($options['guests']['children'] ?? $roomBooking[0]['children']) ); } // check if extra services should be added and calculate the booking cost difference $new_extras_cost = 0; if (is_array(($options['add_extra_services'] ?? null))) { $current_extras = !empty($roomBooking[0]['extracosts']) ? json_decode($roomBooking[0]['extracosts'], true) : []; $current_extras = is_array($current_extras) ? $current_extras : []; $new_extras = []; foreach ($options['add_extra_services'] as $extras) { if (!is_array($extras) || (!isset($extras['name']) && !isset($extras['cost']))) { // invalid extra service structure continue; } // build new extra service $new_extra = [ 'name' => (string) ($extras['name'] ?? 'Custom Extra'), 'cost' => (float) ($extras['cost'] ?? 0), 'idtax' => null, ]; // push custom extra service $current_extras[] = $new_extra; // push the custom extra service in the new list $new_extras[] = $new_extra; } if ($new_extras) { // update the extra services ONLY on the first room booked $q = $dbo->getQuery(true) ->update($dbo->qn('#__vikbooking_ordersrooms')) ->set($dbo->qn('extracosts') . ' = ' . $dbo->q(json_encode($current_extras))) ->where($dbo->qn('idorder') . ' = ' . (int) $booking_id); $dbo->setQuery($q, 0, 1); $dbo->execute(); // register history description row $history_descr_rows[] = sprintf( 'New extras: %s.', implode(', ', array_column($new_extras, 'name')) ); // check if we need to increase the booking total amount $new_extras_cost = array_sum(array_column($new_extras, 'cost')); if ($new_extras_cost > 0 && (float) ($options['cost_difference'] ?? 0) < $new_extras_cost) { // increase the "cost difference" due to the newly added extra services $options['cost_difference'] = ($options['cost_difference'] ?? 0) + $new_extras_cost; } } } // check if the booking total amount should change if ($options['cost_difference'] ?? null) { // this difference should be summed to (or deducted from) the current booking total value $bookingQ->set($dbo->qn('total') . ' = ' . ($prev_booking['total'] + (float) $options['cost_difference'])); // register history description row $history_descr_rows[] = sprintf( 'Booking total cost difference calculated: %d.', (float) $options['cost_difference'] ); // calculate the cost difference just for the rooms $rooms_cost_difference = (float) $options['cost_difference'] - $new_extras_cost; if ($rooms_cost_difference) { // this value should be summed to (or deducted from) the current room rate to have a proper calculation $new_room_cost = null; $room_cost_prop = null; if (!empty($roomBooking[0]['cust_cost'])) { $new_room_cost = $roomBooking[0]['cust_cost'] + $rooms_cost_difference; $room_cost_prop = 'cust_cost'; } elseif (!empty($roomBooking[0]['room_cost'])) { $new_room_cost = $roomBooking[0]['room_cost'] + $rooms_cost_difference; $room_cost_prop = 'room_cost'; } if ($room_cost_prop) { // we can update the room cost for the difference calculated ONLY on the first room booked // if no room cost was found, maybe because of a tariff, we would keep just the total changed $q = $dbo->getQuery(true) ->update($dbo->qn('#__vikbooking_ordersrooms')) ->set($dbo->qn($room_cost_prop) . ' = ' . $new_room_cost) ->where($dbo->qn('idorder') . ' = ' . (int) $booking_id); $dbo->setQuery($q, 0, 1); $dbo->execute(); } } } if ($options['extra_notes'] ?? '') { // update administrator notes $bookingQ->set($dbo->qn('adminnotes') . ' = ' . $dbo->q(trim($prev_booking['adminnotes'] . "\n" . $options['extra_notes']))); } if (($options['custmail'] ?? '') || ($options['customer_email'] ?? '')) { // update guest email address at booking level $set_cust_mail = $options['custmail'] ?? $options['customer_email'] ?? ''; if (preg_match("/^[^@]+@[^@]+\.[^@]+$/", $set_cust_mail)) { // email pattern is safe $bookingQ->set($dbo->qn('custmail') . ' = ' . $dbo->q(trim($set_cust_mail))); } } // finally, update the booking record try { // make sure something to update was set by using the apposite getter magic method if ($bookingQ->set) { // some booking record values should be updated $dbo->setQuery($bookingQ); $dbo->execute(); } } catch (Throwable $e) { $this->setError($e->getMessage()); return false; } // update booking history $history_obj = VikBooking::getBookingHistoryInstance($booking_id); $now_user = JFactory::getUser(); $caller_id = $now_user->name ? "({$now_user->name})" : ''; if ($this->getCaller()) { $caller_id = '(' . $this->getCaller() . ')'; if ($this->getHistoryData()) { $history_obj->setExtraData($this->getHistoryData()); } } // update Booking History $history_obj->store('MB', $caller_id . ($history_descr_rows ? "\n" . implode("\n", $history_descr_rows) : '')); // check for the operations to perform if (in_array('shared_calendars', $trigger_operations)) { // unset any previously booked room due to calendar sharing VikBooking::cleanSharedCalendarsBusy($booking_id); // check if some of the rooms booked have shared calendars VikBooking::updateSharedCalendars($booking_id); } if (in_array('vcm_sync', $trigger_operations)) { // invoke Channel Manager $vcm_autosync = VikBooking::vcmAutoUpdate(); if ($vcm_autosync > 0) { $vcm_obj = VikBooking::getVcmInvoker(); $vcm_obj->setOids([$booking_id])->setSyncType('modify')->setOriginalBooking($prev_booking); $sync_result = $vcm_obj->doSync(); if ($sync_result === false) { // set error message $vcm_err = $vcm_obj->getError(); $this->setError(JText::translate('VBCHANNELMANAGERRESULTKO') . (!empty($vcm_err) ? ' - ' . $vcm_err : '')); } } elseif (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'synch.vikbooking.php')) { // set the necessary action to invoke VCM manually $this->setChannelManagerAction( JText::translate('VBCHANNELMANAGERINVOKEASK') . ' ' . '<form action="index.php?option=com_vikbooking" method="post">' . '<input type="hidden" name="option" value="com_vikbooking"/>' . '<input type="hidden" name="task" value="invoke_vcm"/>' . '<input type="hidden" name="stype" value="modify"/>' . '<input type="hidden" name="cid[]" value="' . $booking_id . '"/>' . '<input type="hidden" name="origb" value="' . urlencode(json_encode($prev_booking)) . '"/>' . '<button type="submit" class="btn btn-primary">' . JText::translate('VBCHANNELMANAGERSENDRQ') . '</button>' . '</form>' ); } } if (($options['ota_reporting'] ?? null) && $diff_stay_dates) { // perform the OTA reporting action, if allowed if (class_exists('VCMOtaReporting') && VCMOtaReporting::getInstance($ord)->stayChangeAllowed()) { // check if an OTA reporting action is needed $ota_stay_change_data = []; foreach ($roomBooking as $kor => $or) { // set room data for stay change $ota_stay_change_room = [ 'idroom' => $or['idroom'], 'checkin' => $set_checkin, 'checkout' => $set_checkout, ]; if (isset($or['modified_price'])) { $ota_stay_change_room['price'] = $or['modified_price']; } // push room data for stay change $ota_stay_change_data[] = $ota_stay_change_room; } // notify the OTA through Vik Channel Manager $ota_reporting = VCMOtaReporting::getInstance(); $ota_result = $ota_reporting->notifyStayChange($ota_stay_change_data); if (!$ota_result) { // register error message $this->setError($ota_reporting->getError()); } } } return true; } /** * Deletes the requested booking ID. * * @param array $options List of details to perform the cancellation. * * @return bool * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function delete(array $options) { $dbo = JFactory::getDbo(); $booking_id = $options['booking_id'] ?? 0; $canc_reason = $options['cancellation_reason'] ?? ''; $purge_remove = $options['purge_remove'] ?? false; $booking = VikBooking::getBookingInfoFromID($booking_id); if (!$booking) { $this->setError('Booking not found.'); return false; } if ($booking['status'] === 'cancelled' && !$purge_remove) { $this->setError(sprintf('Booking ID %d is already cancelled.', $booking['id'])); return false; } if (class_exists('VCMFeesCancellation')) { // let VCM detect if there are any constraints for the cancellation $canc_denied = VCMFeesCancellation::getInstance($booking, $anew = true)->isBookingConstrained(); if ($canc_denied) { // set error message $canc_deny_error = VCMFeesCancellation::getInstance()->getError(); $this->setError($canc_deny_error ?: 'Booking cannot be cancelled due to OTA contraints.'); return false; } } // access the current user $now_user = JFactory::getUser(); // whether OTAs should be notified $notify_otas = false; if ($booking['status'] != 'cancelled') { // update status to cancelled $q = $dbo->getQuery(true) ->update($dbo->qn('#__vikbooking_orders')) ->set($dbo->qn('status') . ' = ' . $dbo->q('cancelled')) ->where($dbo->qn('id') . ' = ' . (int) $booking['id']); if (!empty($canc_reason)) { $set_canc_reason = (!empty($booking['adminnotes']) ? $booking['adminnotes'] . "\n" : '') . $canc_reason; $q->set($dbo->qn('adminnotes') . ' = ' . $dbo->q($set_canc_reason)); } $dbo->setQuery($q); $dbo->execute(); // delete temporarily locked records, if any $dbo->setQuery( $dbo->getQuery(true) ->delete($dbo->qn('#__vikbooking_tmplock')) ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id']) ); $dbo->execute(); if ($booking['status'] == 'confirmed') { // turn flag on $notify_otas = true; } // access history object $history_obj = VikBooking::getBookingHistoryInstance($booking['id']); $caller_id = $now_user->name ? "({$now_user->name})" : ''; if ($this->getCaller()) { $caller_id = '(' . $this->getCaller() . ')'; if ($this->getHistoryData()) { $history_obj->setExtraData($this->getHistoryData()); } } // update Booking History $history_obj->store('CB', $caller_id); } // always attempt to free records up $dbo->setQuery( $dbo->getQuery(true) ->select('*') ->from($dbo->qn('#__vikbooking_ordersbusy')) ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id']) ); foreach ($dbo->loadAssocList() as $ob) { // delete busy record $dbo->setQuery( $dbo->getQuery(true) ->delete($dbo->qn('#__vikbooking_busy')) ->where($dbo->qn('id') . ' = ' . (int) $ob['idbusy']) ); $dbo->execute(); } // delete booking-busy-record relations $dbo->setQuery( $dbo->getQuery(true) ->delete($dbo->qn('#__vikbooking_ordersbusy')) ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id']) ); $dbo->execute(); // check for purge removal if ($booking['status'] === 'cancelled' && $purge_remove) { // delete booking-customer relation $dbo->setQuery( $dbo->getQuery(true) ->delete($dbo->qn('#__vikbooking_customers_orders')) ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id']) ); $dbo->execute(); // delete booking-room relations $dbo->setQuery( $dbo->getQuery(true) ->delete($dbo->qn('#__vikbooking_ordersrooms')) ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id']) ); $dbo->execute(); // delete booking-history relations $dbo->setQuery( $dbo->getQuery(true) ->delete($dbo->qn('#__vikbooking_orderhistory')) ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id']) ); $dbo->execute(); // delete the booking record $dbo->setQuery( $dbo->getQuery(true) ->delete($dbo->qn('#__vikbooking_orders')) ->where($dbo->qn('id') . ' = ' . (int) $booking['id']) ); $dbo->execute(); // in case of split stay booking, remove the transient if ($booking['split_stay']) { VBOFactory::getConfig()->remove('split_stay_' . $booking['id']); } } if ($notify_otas) { $vcm_autosync = VikBooking::vcmAutoUpdate(); if ($vcm_autosync > 0) { $vcm_obj = VikBooking::getVcmInvoker(); $vcm_obj->setOids([$booking['id']])->setSyncType('cancel'); $sync_result = $vcm_obj->doSync(); if ($sync_result === false) { // set error message $vcm_err = $vcm_obj->getError(); $this->setError(JText::translate('VBCHANNELMANAGERRESULTKO') . (!empty($vcm_err) ? ' - ' . $vcm_err : '')); } } } return true; } /** * Sets a booking ID to confirmed according to the provided options. * * @param array $options List of details to perform the update. * * @return bool * * @since 1.17.3 (J) - 1.7.3 (WP) */ public function setConfirmed(array $options) { $dbo = JFactory::getDbo(); // access the current booking details, if any $booking = $this->getBooking(); // access the current rooms booked, if any $roomBooking = $this->getRoomBooking(); // gather modification options $booking_id = $options['booking_id'] ?? $booking['id'] ?? 0; if (!$booking_id) { $this->setError('Missing booking ID.'); return false; } if (!$booking) { // load current booking record if not injected $booking = VikBooking::getBookingInfoFromID($booking_id); if (!$booking) { $this->setError('Booking not found.'); return false; } } if (!$roomBooking) { // load current rooms booked $roomBooking = VikBooking::loadOrdersRoomsData($booking_id); } // make sure the booking status is not already confirmed if (!strcasecmp($booking['status'], 'confirmed')) { $this->setError('Booking is already confirmed.'); return false; } // memorize the original booking status for VCM in case of OTA booking $original_book_status = null; if (!empty($booking['idorderota']) && !empty($booking['channel'])) { $original_book_status = $booking['status']; } // availability helper $av_helper = VikBooking::getAvailabilityInstance(true); // room stay dates in case of split stay $room_stay_dates = []; if ($booking['split_stay']) { $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $booking['id'], []); } // make sure all rooms are available for confirmation $turnover_secs = VikBooking::getHoursRoomAvail() * 3600; $realback = $turnover_secs + $booking['checkout']; $allbook = true; $notavail = []; /** * We need to calculate a minus operator for each room that was booked more than once. * In case we are confirming a booking for more than one unit of the same room, we need to * make sure the calculation is made properly, as only one unit of that room could be free. */ $units_minus_oper = []; foreach ($roomBooking as $ind => $or) { if (!isset($units_minus_oper[$or['idroom']])) { $units_minus_oper[$or['idroom']] = -1; } // increase counter $units_minus_oper[$or['idroom']]++; if (!empty($room_stay_dates)) { // split stay rooms never have the same stay dates, but they should also be different rooms $units_minus_oper[$or['idroom']] = 0; } } // check availability for each room involved foreach ($roomBooking as $ind => $or) { // determine proper values for this room $room_stay_checkin = $booking['checkin']; $room_stay_checkout = $booking['checkout']; $room_stay_nights = $booking['days']; if ($booking['split_stay'] && $room_stay_dates && isset($room_stay_dates[$ind]) && $room_stay_dates[$ind]['idroom'] == $or['idroom']) { $room_stay_checkin = $room_stay_dates[$ind]['checkin_ts'] ?: $room_stay_dates[$ind]['checkin']; $room_stay_checkout = $room_stay_dates[$ind]['checkout_ts'] ?: $room_stay_dates[$ind]['checkout']; $room_stay_nights = $av_helper->countNightsOfStay($room_stay_checkin, $room_stay_checkout); // inject nights calculated for this room $room_stay_dates[$ind]['nights'] = $room_stay_nights; } // get room record $room_record = VikBooking::getRoomInfo($or['idroom']); // check if the room is available if (!VikBooking::roomBookable($or['idroom'], (($room_record['units'] ?? 0) - $units_minus_oper[$or['idroom']]), $room_stay_checkin, $room_stay_checkout)) { $allbook = false; $notavail[] = $room_record['name'] ?? '?'; } } // ensure all rooms involved were available or forced to be if (!$allbook && !($options['force_availability'] ?? false)) { $this->setError(sprintf('Some rooms are no longer available: %s', implode(', ', $notavail))); return false; } // occupy the involved rooms on the db foreach ($roomBooking as $ind => $or) { // determine proper values for this room $room_stay_checkin = $booking['checkin']; $room_stay_checkout = $booking['checkout']; $room_stay_realback = $realback; if ($booking['split_stay'] && $room_stay_dates && isset($room_stay_dates[$ind]) && $room_stay_dates[$ind]['idroom'] == $or['idroom']) { $room_stay_checkin = $room_stay_dates[$ind]['checkin_ts'] ?: $room_stay_dates[$ind]['checkin']; $room_stay_checkout = $room_stay_dates[$ind]['checkout_ts'] ?: $room_stay_dates[$ind]['checkout']; $room_stay_realback = $turnover_secs + $room_stay_checkout; } // build busy record $busy_record = new stdClass; $busy_record->idroom = (int) $or['idroom']; $busy_record->checkin = (int) $room_stay_checkin; $busy_record->checkout = (int) $room_stay_checkout; $busy_record->realback = (int) $room_stay_realback; // store busy record and obtain the newly created ID $dbo->insertObject('#__vikbooking_busy', $busy_record, 'id'); $lid = $busy_record->id ?? 0; // build busy relation record $obusy_record = new stdClass; $obusy_record->idorder = (int) $booking['id']; $obusy_record->idbusy = (int) $lid; // store busy relation record $dbo->insertObject('#__vikbooking_ordersbusy', $obusy_record, 'id'); } // delete temporarily locked records, if any $dbo->setQuery( $dbo->getQuery(true) ->delete($dbo->qn('#__vikbooking_tmplock')) ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id']) ); $dbo->execute(); // update booking status (and notes, if any) $q = $dbo->getQuery(true) ->update($dbo->qn('#__vikbooking_orders')) ->set($dbo->qn('status') . ' = ' . $dbo->q('confirmed')) ->where($dbo->qn('id') . ' = ' . (int) $booking['id']); if ($options['extra_notes'] ?? '') { // update administrator notes $q->set($dbo->qn('adminnotes') . ' = ' . $dbo->q(trim($booking['adminnotes'] . "\n" . $options['extra_notes']))); } $dbo->setQuery($q); $dbo->execute(); // set booking confirmation number $confirmnumber = VikBooking::generateConfirmNumber($booking['id'], true); // update booking history $history_obj = VikBooking::getBookingHistoryInstance($booking['id']); $now_user = JFactory::getUser(); $caller_id = $now_user->name ? "({$now_user->name})" : ''; if ($this->getCaller()) { $caller_id = '(' . $this->getCaller() . ')'; if ($this->getHistoryData()) { $history_obj->setExtraData($this->getHistoryData()); } } // update Booking History $history_obj->store('TC', $caller_id); // check if some of the rooms booked have shared calendars VikBooking::updateSharedCalendars($booking['id'], array_column($roomBooking, 'idroom'), $booking['checkin'], $booking['checkout']); // assign room specific unit(s) $set_room_indexes = VikBooking::autoRoomUnit(); $room_indexes_usemap = []; foreach ($roomBooking as $kor => $or) { // determine proper values for this room $room_stay_checkin = $booking['checkin']; $room_stay_checkout = $booking['checkout']; $room_stay_nights = $booking['days']; if ($booking['split_stay'] && $room_stay_dates && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) { $room_stay_checkin = $room_stay_dates[$kor]['checkin_ts'] ?: $room_stay_dates[$kor]['checkin']; $room_stay_checkout = $room_stay_dates[$kor]['checkout_ts'] ?: $room_stay_dates[$kor]['checkout']; $room_stay_nights = $room_stay_dates[$kor]['nights']; } // assign room specific unit if ($set_room_indexes === true) { $room_indexes = VikBooking::getRoomUnitNumsAvailable($booking, $or['idroom']); $use_ind_key = 0; if ($room_indexes) { if (!isset($room_indexes_usemap[$or['idroom']])) { $room_indexes_usemap[$or['idroom']] = $use_ind_key; } else { $use_ind_key = $room_indexes_usemap[$or['idroom']]; } // update room-reservation record by assigning the room index (unit) $dbo->setQuery( $dbo->getQuery(true) ->update($dbo->qn('#__vikbooking_ordersrooms')) ->set($dbo->qn('roomindex') . ' = ' . (int) $room_indexes[$use_ind_key]) ->where($dbo->qn('id') . ' = ' . (int) $or['id']) ); $dbo->execute(); // increase index counter $room_indexes_usemap[$or['idroom']]++; } } } // Invoke Channel Manager $vcm_autosync = VikBooking::vcmAutoUpdate(); if ($vcm_autosync > 0) { $vcm_obj = VikBooking::getVcmInvoker(); $vcm_obj->setOids([$booking['id']])->setSyncType('new')->setOriginalStatuses([$original_book_status]); $sync_result = $vcm_obj->doSync(); if ($sync_result === false) { // set error message $vcm_err = $vcm_obj->getError(); $this->setError(JText::translate('VBCHANNELMANAGERRESULTKO') . (!empty($vcm_err) ? ' - ' . $vcm_err : '')); // return true because the booking was actually confirmed return true; } } elseif (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'synch.vikbooking.php')) { // set the necessary action to invoke VCM $vcm_sync_url = 'index.php?option=com_vikbooking&task=invoke_vcm&stype=new&cid[]=' . $booking['id'] . '&returl=' . urlencode('index.php?option=com_vikbooking&task=editorder&cid[]=' . $booking['id']); $this->setChannelManagerAction(JText::translate('VBCHANNELMANAGERINVOKEASK') . ' <button type="button" class="btn btn-primary" onclick="document.location.href=\'' . $vcm_sync_url . '\';">' . JText::translate('VBCHANNELMANAGERSENDRQ') . '</button>'); } // check if the guest should be notified via email if ($options['notify'] ?? null) { // send email notification to guest VikBooking::sendBookingEmail($booking['id'], ['guest']); // SMS skipping the administrator VikBooking::sendBookingSMS($booking['id'], ['admin']); } return true; } /** * Tells if the booking record set can be modified and/or cancelled. * * @return array * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function getAlterationDetails() { $dbo = JFactory::getDbo(); $booking = $this->getBooking(); $roomBooking = $this->getRoomBooking(); if (!$booking || !$roomBooking) { $this->setError('Missing booking or room booking record details.'); return []; } // gather room booking tariffs $tars = []; foreach ($roomBooking as $kor => $or) { $num = $kor + 1; if (!empty($order['pkg']) || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) { // package or custom cost set from the back-end continue; } // get room tariff details $dbo->setQuery( $dbo->getQuery(true) ->select($dbo->qn('t') . '.*') ->select([ $dbo->qn('p.name'), $dbo->qn('p.free_cancellation'), $dbo->qn('p.canc_deadline'), $dbo->qn('p.canc_policy'), ]) ->from($dbo->qn('#__vikbooking_dispcost', 't')) ->leftJoin($dbo->qn('#__vikbooking_prices', 'p') . ' ON ' . $dbo->qn('t.idprice') . ' = ' . $dbo->qn('p.id')) ->where($dbo->qn('t.id') . ' = ' . (int) ($or['idtar'] ?? 0)) ); $tar = $dbo->loadAssoc(); if ($tar) { // push room booking tariff $tars[$num] = $tar; } } // count days to arrival $days_to_arrival = 0; $now_info = getdate(); $checkin_info = getdate($booking['checkin'] ?? 0); if ($now_info[0] < $checkin_info[0]) { while ($now_info[0] < $checkin_info[0]) { if (!($now_info['mday'] != $checkin_info['mday'] || $now_info['mon'] != $checkin_info['mon'] || $now_info['year'] != $checkin_info['year'])) { break; } $days_to_arrival++; $now_info = getdate(mktime(0, 0, 0, $now_info['mon'], ($now_info['mday'] + 1), $now_info['year'])); } } // check if the rate plan(s) are refundable $is_refundable = 0; $daysadv_refund_arr = []; $daysadv_refund = 0; $canc_policy = ''; foreach ($tars as $num => $tar) { if (!$tar['free_cancellation']) { // if at least one rate plan is non-refundable, the whole reservation cannot be cancelled $is_refundable = 0; $daysadv_refund_arr = []; break; } $is_refundable = 1; $daysadv_refund_arr[] = $tar['canc_deadline']; } // get the rate plan with the lowest cancellation deadline $daysadv_refund = $daysadv_refund_arr ? min($daysadv_refund_arr) : $daysadv_refund; if ($daysadv_refund > 0) { foreach ($tars as $num => $tar) { if ($tar['free_cancellation'] && $tar['canc_deadline'] == $daysadv_refund) { // get the cancellation policy from the first rate plan with free cancellation and same cancellation deadline $canc_policy = $tar['canc_policy']; break; } } } // access global settings to determine the alterations available $resmodcanc = VikBooking::getReservationModCanc(); $resmodcanc = !$days_to_arrival ? 0 : $resmodcanc; $resmodcancmin = VikBooking::getReservationModCancMin(); // build alteration deadline date $checkin_dt = JFactory::getDate(date('Y-m-d', ($booking['checkin'] ?? 0))); $checkin_dt->modify("-{$resmodcancmin} days"); $alteration_deadline = $checkin_dt->format('Y-m-d'); return [ 'refundable' => ($resmodcanc > 1 && $resmodcanc != 2 && $is_refundable > 0 && $daysadv_refund <= $days_to_arrival && $days_to_arrival >= $resmodcancmin), 'modifiable' => ($resmodcanc > 1 && $resmodcanc != 3 && $days_to_arrival >= $resmodcancmin), 'alteration_disabled' => $resmodcanc === 0, 'request_alteration' => $resmodcanc === 1, 'cancellation_policy' => $canc_policy ?: null, 'alteration_deadline' => $alteration_deadline, ]; } /** * Attempts to invoke the payment processor assigned to the current booking. * * @param array $card Optional credit card details to bind. * * @return object The payment processor dispatcher instance. * * @throws Exception * * @since 1.16.10 (J) - 1.6.10 (WP) * @since 1.17.6 (J) - 1.7.6 (WP) added "tn_metadata" details. */ public function getPaymentProcessor(array $card = []) { $booking = $this->getProperties(); $processor = null; $payment = []; if (!$booking) { throw new Exception('Missing booking details', 500); } if (!empty($booking['idpayment'])) { $payment = VikBooking::getPayment($booking['idpayment']); } if (!$payment) { throw new Exception('Missing payment method details', 500); } // set payment details internally $this->set('_payment_info', $payment); if ($card) { // inject CC details for the payment processor $booking['card'] = $card; } // get the booking customer record, if any $customer = $this->getCustomer(); if (!$customer) { $customer = VikBooking::getCPinInstance()->getCustomerFromBooking($booking['id']); } // build and inject transaction metadata $booking['tn_metadata'] = [ 'booking_id' => $booking['id'], 'source' => (($booking['channel'] ?? '') ?: 'Website'), 'ota_booking_id' => (($booking['idorderota'] ?? '') ?: ''), 'guest_name' => implode(' ', array_filter([($customer['first_name'] ?? ''), ($customer['last_name'] ?? '')])), ]; if (VBOPlatformDetection::isWordPress()) { /** * @wponly The payment gateway is loaded * through the apposite dispatcher. */ JLoader::import('adapter.payment.dispatcher'); $processor = JPaymentDispatcher::getInstance('vikbooking', $payment['file'], $booking, $payment['params']); } elseif (VBOPlatformDetection::isJoomla()) { /** * @joomlaonly The Payment Factory library will invoke the gateway. */ require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'payments' . DIRECTORY_SEPARATOR . 'libraries' . DIRECTORY_SEPARATOR . 'factory.php'; $processor = VBOPaymentFactory::getPaymentInstance($payment['file'], $booking, $payment['params']); } if (!$processor) { throw new Exception('Could not invoke the payment processor', 500); } // return the valid payment processor instance return $processor; } /** * Gets the reservation's payment method name. * * @return string * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function getPaymentName() { // access the reserved property $payment = (array) $this->get('_payment_info', []); if (!$payment) { return ''; } return $payment['name'] ?? ''; } /** * Attempts to get the most recent transaction data for an off-session capturing. * * @param string $tn_driver Optional payment processor driver name. * * @return object[] Eligible transaction data list or empty array. * * @since 1.18.0 (J) - 1.8.0 (WP) */ public function getOffSessionTransactionData(string $tn_driver = '') { // transaction data validation callback $tn_data_callback = function($data) use ($tn_driver) { return (is_object($data) && isset($data->driver) && (!$tn_driver || basename($data->driver, '.php') == basename($tn_driver, '.php')) && ($data->future_usage ?? null)); }; // get previous transactions (in date ascending order) $prev_tn_data = (array) VikBooking::getBookingHistoryInstance($this->get('id', 0))->getEventsWithData(['P0', 'PN'], $tn_data_callback); if (!$prev_tn_data) { return []; } // return the eligible transaction data list in reverse order return array_reverse(array_values(array_filter(array_map(function($data) { if (is_array($data)) { // cast to object $data = (object) $data; } return is_object($data) ? $data : null; }, $prev_tn_data)))); } /** * Attempts to get the credit card value pairs from the current booking. * * @return array Associative list of CC value-pairs, if any. * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function getCardValuePairs() { $booking_info = $this->getProperties(); if (empty($booking_info['paymentlog'])) { return []; } // build complete credit card payload, if available $cc_payload_str = ''; // extract CC data from payment logs by ensuring they're not null $booking_info['paymentlog'] = (string) $booking_info['paymentlog']; if (stripos($booking_info['paymentlog'], 'card number') !== false && strpos($booking_info['paymentlog'], '*') !== false) { // matched a log for an OTA CC $cc_payload_str = $booking_info['paymentlog']; } elseif (preg_match("/(([\d\*]{4,4}\s*){4,4})|(([\d\*]{4,6}\s*){3,3})/", $booking_info['paymentlog'])) { // matched a credit card $cc_payload_str = $booking_info['paymentlog']; } // check if this is an OTA reservation with remotely decoded CC details required $remote_cc_data = []; if (!empty($booking_info['idorderota']) && !empty($booking_info['channel'])) { // channel source $channel_source = (string)$booking_info['channel']; if (strpos($booking_info['channel'], '_') !== false) { $channelparts = explode('_', $booking_info['channel']); $channel_source = $channelparts[0]; } // only updated versions of VCM will support remote CC decoding for OTA reservations if (class_exists('VCMOtaBooking')) { // invoke the OTA Booking helper class from VCM $cc_helper = VCMOtaBooking::getInstance([ 'channel_source' => $channel_source, 'ota_id' => $booking_info['idorderota'], ], $anew = true); if (method_exists($cc_helper, 'decodeCreditCardDetails')) { $remote_cc_data = $cc_helper->decodeCreditCardDetails(); // make sure the response was valid if (!$remote_cc_data || !empty($remote_cc_data['error'])) { // we ignore the error by simply resetting the array $remote_cc_data = []; } } } } // check if we have already a full VCC if (($remote_cc_data['card_number'] ?? '') && strlen(preg_replace('/[^0-9]/', '', (string) $remote_cc_data['card_number'])) >= 15) { // do not merge any local data and return the full VCC details return $remote_cc_data; } // merge remotely decoded CC details with parsed payment log (if any) return array_merge($remote_cc_data, $this->parseCreditCardValuePairs($cc_payload_str, $remote_cc_data)); } /** * Given a raw string of credit card key-value pairs from payments log, * parse the corresponding keys and values into an associative array. * In case of conflicting keys with the remotely decoded CC details, * attempts to replace the masked numbers with asterisks. * * @param string $cc_payload the raw CC details from payment logs. * @param array $remote_cc_data associative array of decoded CC data. * * @return array associative or empty array. * * @since 1.16.10 (J) - 1.6.10 (WP) moved from widget Virtual Terminal. */ protected function parseCreditCardValuePairs($cc_payload, array $remote_cc_data = []) { $cc_value_pairs = []; if (empty($cc_payload)) { return $cc_value_pairs; } $cc_lines = preg_split("/(\r\n|\n|\r)/", $cc_payload); foreach ($cc_lines as $cc_line) { if (strpos($cc_line, ':') === false) { continue; } $cc_line_parts = explode(':', $cc_line); if (empty($cc_line_parts[0]) || !strlen(trim($cc_line_parts[1]))) { continue; } $key = str_replace(' ', '_', strtolower($cc_line_parts[0])); $value = trim($cc_line_parts[1]); if (isset($cc_value_pairs[$key])) { /** * Do not overwrite existing keys because this probably means that the * credit card was updated by an OTA like Booking.com, hence the payment * logs string in VBO may contain the information of two different cards. * New credit card details are always pre-pended by VCM in the payment logs. */ continue; } if (!empty($remote_cc_data[$key]) && is_string($remote_cc_data[$key]) && strpos($value, '*') !== false) { // replace masked numbers with remote content $value = $this->replaceMaskedNumbers($value, $remote_cc_data[$key]); } $cc_value_pairs[$key] = $value; } return $cc_value_pairs; } /** * Given a local and a remote credit card number string with * masked symbols, replaces the values in the corresponding * positions with the unmasked numbers. * * @param string $local current string with masked values. * @param string $remote remote string with unmasked values. * * @return string the local string with unmasked values. * * @since 1.16.10 (J) - 1.6.10 (WP) moved from widget Virtual Terminal. */ protected function replaceMaskedNumbers($local, $remote) { // split anything but numbers $numbers = preg_split("/([^0-9]+)/", trim($remote)); if ($numbers) { // filter empty values $numbers = array_filter($numbers); } if (!$numbers) { // unable to proceed return $local; } // split anything but stars (asterisks) $stars = preg_split("/([^\*]+)/", trim($local)); if ($stars) { // filter empty values $stars = array_filter($stars); } if (!$stars) { // unable to proceed return $local; } // replace masked symbols with numbers at their first occurrence foreach ($numbers as $k => $unmasked) { if (!isset($stars[$k])) { continue; } $masked_pos = strpos($local, $stars[$k]); if ($masked_pos === false) { continue; } $local = substr_replace($local, $unmasked, $masked_pos, strlen($stars[$k])); } // return the string with possibly unmasked values return $local; } /** * Tells whether the booking can be created. By default this * is only allowed from the administrator section of the site. * * @return bool */ protected function canCreate() { return $this->get('_isAdministrator') || JFactory::getApplication()->isClient('administrator'); } /** * Gets and sets the tariff ID if a rate plan was set. * * @return int the tariff ID or 0. */ protected function loadTariffID() { $dbo = JFactory::getDbo(); $id_tariff = 0; $room = $this->getRoom(); $daysdiff = (int)$this->get('nights', 1); if (!empty($room['id']) && !empty($room['id_price']) && !empty($room['room_cost']) && $room['room_cost'] > 0 && !(int)$this->get('set_closed') && !$this->get('split_stay', [])) { $room['id_price'] = (int)$room['id_price']; $q = "SELECT `id` FROM `#__vikbooking_dispcost` WHERE `idroom`={$room['id']} AND `days`={$daysdiff} AND `idprice`={$room['id_price']};"; $dbo->setQuery($q); $id_tariff = $dbo->loadResult(); } $this->set('id_tariff', (int)$id_tariff); return (int)$id_tariff; } /** * Applies the turnover time to the checkout timestamp and sets its value. * * @return int the turnover seconds applied. */ protected function applyTurnover() { $turnover_secs = 0; $checkout = $this->get('checkout', 0); if ($checkout) { // turnover time $turnover_secs = VikBooking::getHoursRoomAvail() * 3600; $this->set('checkout_real', ($checkout + $turnover_secs)); } $this->set('turnover_secs', $turnover_secs); return $turnover_secs; } /** * Returns the details of a specific room ID. * * @param int $rid the room ID. * * @return array the record found or empty array. */ protected function getRoomDetails($rid = null) { $all_rooms = VikBooking::getAvailabilityInstance(true)->loadRooms(); if (!$rid) { $inj_room = $this->getRoom(); if (!empty($inj_room['id'])) { $rid = $inj_room['id']; } } if ($rid && isset($all_rooms[$rid])) { return $all_rooms[$rid]; } return []; } /** * Gets the list of rooms involved in the reservation in case of closures. * * @return array the list of rooms involved */ protected function getRoomsPool() { $room = $this->getRoom(); if (empty($room['id'])) { return []; } $room = $this->getRoomDetails($room['id']); // gather values $set_close_others = (array)$this->get('close_others', []); $split_stay_data = $this->get('split_stay', []); $set_closed = (int)$this->get('set_closed'); $turnover_secs = $this->get('turnover_secs', 0); $hcheckin = $this->get('checkin_h', 12); $mcheckin = $this->get('checkin_m', 0); $hcheckout = $this->get('checkout_h', 10); $mcheckout = $this->get('checkout_m', 0); $av_helper = VikBooking::getAvailabilityInstance(true); $all_rooms = $av_helper->loadRooms(); $rooms_pool = []; $closeothers = []; if ($set_close_others && $set_closed) { // prepend current room for closing array_unshift($set_close_others, $room['id']); } $set_close_others = array_unique($set_close_others); foreach ($set_close_others as $closeid) { if (empty($closeid)) { continue; } if ((int)$closeid === -1) { // close all rooms $closeothers = []; foreach ($all_rooms as $cr) { array_push($closeothers, $cr); } break; } foreach ($all_rooms as $cr) { if ((int)$cr['id'] == (int)$closeid) { // push the main room or one of the other rooms requested for closure array_push($closeothers, $cr); break; } } } if (!$closeothers || !$set_closed) { $rooms_pool = [$room]; } else { $rooms_pool = $closeothers; } // check split stay rooms booking if (!empty($split_stay_data)) { // reset pool and set it with the split stay rooms $rooms_pool = []; foreach ($split_stay_data as $sps_k => $split_stay) { if (!isset($all_rooms[$split_stay['idroom']])) { continue; } // calculate and set the exact check-in and check-out timestamps for this split-room $split_stay['checkin_ts'] = VikBooking::getDateTimestamp($split_stay['checkin'], $hcheckin, $mcheckin); $split_stay['checkout_ts'] = VikBooking::getDateTimestamp($split_stay['checkout'], $hcheckout, $mcheckout); $split_stay['realback_ts'] = $turnover_secs + $split_stay['checkout_ts']; $split_stay['nights'] = $av_helper->countNightsOfStay($split_stay['checkin_ts'], $split_stay['checkout_ts']); $split_stay_data[$sps_k] = $split_stay; // push room data to pool after storing additional information $room_data = $all_rooms[$split_stay['idroom']]; $room_data['checkin_ts'] = $split_stay['checkin_ts']; $room_data['checkout_ts'] = $split_stay['checkout_ts']; $rooms_pool[] = $room_data; } if (!$rooms_pool) { $this->setError('No valid rooms for the split stay booking'); return []; } // update split stay data manipulated $this->set('split_stay', $split_stay_data); } return $rooms_pool; } /** * Checks that the room is available on the requested dates. * Call this method only after getting the rooms pool. * * @return bool */ protected function isRoomAvailable() { $inj_room = $this->getRoom(); if (empty($inj_room['id'])) { return false; } $room = $this->getRoomDetails($inj_room['id']); if (!$room) { return false; } $split_stay_data = $this->get('split_stay', []); $set_closed = $this->get('set_closed', 0); $num_rooms = $this->get('num_rooms', 1); $room_available = true; if (empty($split_stay_data)) { // make sure the rooms are available $check_units = $room['units']; if ($num_rooms > 1 && $num_rooms <= $room['units'] && !$set_closed) { // only when non closing the room we check the availability for the units requested for booking $check_units = $room['units'] - $num_rooms + 1; } $room_available = VikBooking::roomBookable($room['id'], $check_units, $this->get('checkin', 0), $this->get('checkout', 0)); } else { $all_rooms = VikBooking::getAvailabilityInstance(true)->loadRooms(); // make sure the rooms for the split stay are available foreach ($split_stay_data as $split_stay) { if (!isset($all_rooms[$split_stay['idroom']])) { $room_available = false; break; } $room_available = $room_available && VikBooking::roomBookable($split_stay['idroom'], $all_rooms[$split_stay['idroom']]['units'], $split_stay['checkin_ts'], $split_stay['checkout_ts']); } } return $room_available; } /** * In case the reservation is forced or is a closure, we detect the * forced reason to eventually attach it to the booking history. * * @param bool $room_available whether the room is available. * * @return void */ protected function detectForcedReason($room_available = true) { $split_stay_data = $this->get('split_stay', []); $force_booking = $this->get('force_booking', 0); $set_closed = $this->get('set_closed', 0); $forced_reason = $this->get('forced_reason', ''); if (empty($split_stay_data)) { // eventually build string for the description of the history event if (($force_booking || $set_closed) && !$room_available) { $forced_reason = JText::translate('VBO_FORCED_BOOKDATES'); } } else { $all_rooms = VikBooking::getAvailabilityInstance(true)->loadRooms(); // set "split stay" as the description of the history event $forced_reason = JText::translate('VBO_SPLIT_STAY') . "\n"; foreach ($split_stay_data as $sps_k => $split_stay) { // describe the split stay for each room if (!isset($all_rooms[$split_stay['idroom']])) { continue; } $room_stay_nights = $split_stay['nights']; $forced_reason .= $all_rooms[$split_stay['idroom']]['name'] . ': ' . $room_stay_nights . ' ' . ($room_stay_nights > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY')) . ', '; $forced_reason .= $split_stay['checkin'] . ' - ' . $split_stay['checkout'] . "\n"; } $forced_reason = rtrim($forced_reason, "\n"); } $this->set('forced_reason', $forced_reason); } /** * Stores the customer information to a new or existing record. * In case of success, the customer ID property is updated. * The customer shall be stored before the reservation records. * * @return bool */ protected function storeCustomer() { $dbo = JFactory::getDbo(); $inj_customer = $this->getCustomer(); $first_name = !empty($inj_customer['first_name']) ? $inj_customer['first_name'] : ''; $last_name = !empty($inj_customer['last_name']) ? $inj_customer['last_name'] : ''; $custdata = !empty($inj_customer['data']) ? $inj_customer['data'] : ''; $email = !empty($inj_customer['email']) ? $inj_customer['email'] : ''; $country = !empty($inj_customer['country']) ? $inj_customer['country'] : ''; $phone = !empty($inj_customer['phone']) ? $inj_customer['phone'] : ''; // custom fields $q = "SELECT * FROM `#__vikbooking_custfields` ORDER BY `ordering` ASC;"; $dbo->setQuery($q); $all_cfields = $dbo->loadAssocList(); $customer_cfields = []; $customer_extrainfo = []; $custdata_parts = explode("\n", $custdata); foreach ($custdata_parts as $cdataline) { if (!strlen(trim($cdataline))) { continue; } $cdata_parts = explode(':', $cdataline); if (count($cdata_parts) < 2 || !strlen(trim($cdata_parts[0])) || !strlen(trim($cdata_parts[1]))) { continue; } foreach ($all_cfields as $cf) { $needle = JText::translate($cf['name']); if (!empty($needle) && strpos($cdata_parts[0], $needle) !== false && !array_key_exists($cf['id'], $customer_cfields) && $cf['type'] != 'country') { $user_input_val = trim($cdata_parts[1]); $customer_cfields[$cf['id']] = $user_input_val; if (!empty($cf['flag'])) { $customer_extrainfo[$cf['flag']] = $user_input_val; } elseif ($cf['type'] == 'state') { $customer_extrainfo['state'] = $user_input_val; } break; } } } $cpin = VikBooking::getCPinInstance(); $cpin->is_admin = true; $cpin->setCustomerExtraInfo($customer_extrainfo); $cpin->saveCustomerDetails($first_name, $last_name, $email, $phone, $country, $customer_cfields); $customer_id = $cpin->getNewCustomerId(); if (!$customer_id) { return false; } $inj_customer['id'] = $customer_id; $this->setCustomer($inj_customer); return true; } /** * Returns the calculated total booking amount and total taxes. * Sets the necessary properties with the calculated amounts. * * @return array list of booking total amount and total taxes. */ protected function calculateTotal() { // the values to calculate $set_total = 0; $set_taxes = 0; $dbo = JFactory::getDbo(); // get data $inj_room = $this->getRoom(); $set_closed = $this->get('set_closed', 0); $daysdiff = (int)$this->get('nights', 1); $num_rooms = (int)$this->get('num_rooms', 1); $totalpnight = !empty($inj_room['total_or_pnight']) ? $inj_room['total_or_pnight'] : 'total'; $cust_cost = !empty($inj_room['cust_cost']) ? (float)$inj_room['cust_cost'] : 0; $room_cost = !empty($inj_room['room_cost']) ? (float)$inj_room['room_cost'] : 0; $id_price = !empty($inj_room['id_price']) ? (int)$inj_room['id_price'] : 0; $id_tax = !empty($inj_room['id_tax']) ? (int)$inj_room['id_tax'] : 0; $split_stay_data = $this->get('split_stay', []); $room = $this->getRoomDetails(); if (!$room) { return [$set_total, $set_taxes]; } if ($cust_cost > 0 && !$set_closed) { // custom cost can be per night if ($totalpnight == 'pnight') { $cust_cost = $cust_cost * $daysdiff; } $set_total = $cust_cost; if (!$id_tax && ($inj_room['guess_tax'] ?? false)) { // try to guess the tax rate $dbo->setQuery( $dbo->getQuery(true) ->select($dbo->qn('id')) ->from($dbo->qn('#__vikbooking_iva')) ->order($dbo->qn('aliq') . ' ASC'), 0, 1); $guessed_id_tax = $dbo->loadResult(); if ($guessed_id_tax) { // update the id_tax values $id_tax = $guessed_id_tax; $inj_room['id_tax'] = $guessed_id_tax; $this->setRoom($inj_room); } } // apply taxes, if necessary if ($id_tax) { $dbo->setQuery( $dbo->getQuery(true) ->select([ $dbo->qn('i.aliq'), $dbo->qn('i.taxcap'), ]) ->from($dbo->qn('#__vikbooking_iva', 'i')) ->where($dbo->qn('i.id') . ' = ' . (int) $id_tax), 0, 1); $taxdata = $dbo->loadAssoc(); if ($taxdata) { $aliq = $taxdata['aliq']; if (floatval($aliq) > 0.00) { if (!VikBooking::ivaInclusa()) { // add tax to the total amount $subt = 100 + (float)$aliq; $set_total = ($set_total * $subt / 100); /** * Tax Cap implementation for prices tax excluded (most common). * * @since 1.12 (J) - 1.2 (WP) */ if ($taxdata['taxcap'] > 0 && ($set_total - $cust_cost) > $taxdata['taxcap']) { $set_total = ($cust_cost + $taxdata['taxcap']); } // calculate tax $set_taxes = $set_total - $cust_cost; } else { // calculate tax $cost_minus_tax = VikBooking::sayPackageMinusIva($cust_cost, $id_tax); $set_taxes += ($cust_cost - $cost_minus_tax); } } } } } elseif (!empty($id_price) && $room_cost > 0 && !$set_closed && empty($split_stay_data)) { // one website rate plan was selected, so we calculate total and taxes $set_total = $room_cost; // find tax rate assigned to this rate plan $q = "SELECT `p`.`id`,`p`.`idiva`,`i`.`aliq`,`i`.`taxcap` FROM `#__vikbooking_prices` AS `p` LEFT JOIN `#__vikbooking_iva` AS `i` ON `p`.`idiva`=`i`.`id` WHERE `p`.`id`=" . $id_price . ";"; $dbo->setQuery($q); $taxdata = $dbo->loadAssoc(); if ($taxdata) { $aliq = $taxdata['aliq']; if (floatval($aliq) > 0.00) { if (!VikBooking::ivaInclusa()) { // add tax to the total amount $subt = 100 + (float)$aliq; $set_total = ($set_total * $subt / 100); /** * Tax Cap implementation for prices tax excluded (most common). * * @since 1.12 (J) - 1.2 (WP) */ if ($taxdata['taxcap'] > 0 && ($set_total - $room_cost) > $taxdata['taxcap']) { $set_total = ($room_cost + $taxdata['taxcap']); } // calculate tax $set_taxes = $set_total - $room_cost; } else { // calculate tax $cost_minus_tax = VikBooking::sayPackageMinusIva($room_cost, $taxdata['idiva']); $set_taxes += ($room_cost - $cost_minus_tax); } } } // total and taxes should be multiplied by the number of rooms booked when using a website rate plan if ($set_closed) { $set_total *= $room['units']; $set_taxes *= $room['units']; } elseif ($num_rooms > 1 && $num_rooms <= $room['units']) { $set_total *= $num_rooms; $set_taxes *= $num_rooms; } } // set values $this->set('_total', $set_total); $this->set('_total_tax', $set_taxes); return [$set_total, $set_taxes]; } /** * Stores the booking and room-booking records. * If no errors, the newly generated booking id is set. * * @param array $rooms_pool list of rooms involved. * * @return bool */ protected function storeReservationRecords(array $rooms_pool) { $dbo = JFactory::getDbo(); $set_closed = $this->get('set_closed', 0); $units_closed = $this->get('units_closed', 0); $daysdiff = (int) $this->get('nights', 1); $num_rooms = (int) $this->get('num_rooms', 1); $adults = (int) $this->get('adults', 1); $children = (int) $this->get('children', 0); $children_age = (array) $this->get('children_age', []); $status = $this->get('status', 'confirmed'); $split_stay_data = $this->get('split_stay', []); $room = $this->getRoomDetails(); if (!$room || !$rooms_pool) { return false; } // get current Joomla/WordPress User ID $now_user = JFactory::getUser(); $store_ujid = property_exists($now_user, 'id') ? (int)$now_user->id : 0; // forced booking reason, status validation and additional data $forced_reason = $this->get('forced_reason', ''); $valid_statuses = ['confirmed', 'standby']; $status = in_array($status, $valid_statuses) ? $status : 'confirmed'; $paymentmeth = $this->get('id_payment', ''); $auto_paymeth = (bool) $this->get('auto_payment_method', false); $set_total = (float) $this->get('_total', 0); $set_taxes = (float) $this->get('_total_tax', 0); // stay dates $now_ts = time(); $checkin_ts = $this->get('checkin'); $checkout_ts = $this->get('checkout'); $realback_ts = $this->get('checkout_real', $checkout_ts); // room $inj_room = $this->getRoom(); $cust_cost = !empty($inj_room['cust_cost']) ? (float) $inj_room['cust_cost'] : 0; $room_cost = !empty($inj_room['room_cost']) ? (float) $inj_room['room_cost'] : 0; $id_price = !empty($inj_room['id_price']) ? (int) $inj_room['id_price'] : 0; $id_tax = !empty($inj_room['id_tax']) ? (int) $inj_room['id_tax'] : 0; $id_tariff = $this->get('id_tariff', 0); // custom rate modifier per night $totalpnight = !empty($inj_room['total_or_pnight']) ? $inj_room['total_or_pnight'] : 'total'; if ($cust_cost > 0.00 && !$set_closed && $totalpnight == 'pnight') { $cust_cost = $cust_cost * $daysdiff; } // customer $cpin = VikBooking::getCPinInstance(); $inj_customer = $this->getCustomer(); $customer_id = !empty($inj_customer['id']) ? $inj_customer['id'] : 0; $customer_pin = !empty($inj_customer['pin']) ? $inj_customer['pin'] : ''; $t_first_name = !empty($inj_customer['first_name']) ? $inj_customer['first_name'] : ''; $t_last_name = !empty($inj_customer['last_name']) ? $inj_customer['last_name'] : ''; $customer_data = !empty($inj_customer['data']) ? $inj_customer['data'] : ''; $customer_email = !empty($inj_customer['email']) ? $inj_customer['email'] : ''; $country_code = !empty($inj_customer['country']) ? $inj_customer['country'] : ''; $phone_number = !empty($inj_customer['phone']) ? $inj_customer['phone'] : ''; if ($set_closed) { $customer_data = JText::translate('VBDBTEXTROOMCLOSED'); } // check for default customer raw data if (!$customer_data && $t_first_name) { // build a default raw data string $customer_data = "Name: {$t_first_name}\n"; if ($t_last_name) { $customer_data .= "Last Name: {$t_last_name}\n"; } if ($customer_email) { $customer_data .= "eMail: {$customer_email}\n"; } if ($country_code) { $customer_data .= "Country: {$country_code}\n"; } if ($phone_number) { $customer_data .= "Phone: {$phone_number}\n"; } $customer_data = rtrim($customer_data, "\n"); } // generate booking SID $sid = VikBooking::getSecretLink(); // assign room specific unit $set_room_indexes = !$set_closed ? VikBooking::autoRoomUnit() : false; $num_rooms = $num_rooms > 0 ? $num_rooms : 1; // occupancy and loop limits $forend = 1; $or_forend = 1; $adults_map = []; $children_map = []; if ($set_closed && empty($split_stay_data)) { $forend = $room['units']; } elseif ($num_rooms > 1 && $num_rooms <= $room['units'] && empty($split_stay_data)) { $forend = $num_rooms; $or_forend = $num_rooms; // assign adults/children proportionally if (($adults + $children) < $num_rooms) { // the number of guests does not make much sense but we build the maps anyway for ($r = 1; $r <= $or_forend; $r++) { $adults_map[$r] = $adults; $children_map[$r] = $children; } } else { $adults_per_room = floor(($adults / $num_rooms)); $adults_left = ($adults % $num_rooms); $children_per_room = floor(($children / $num_rooms)); $children_left = ($children % $num_rooms); for ($r = 1; $r <= $or_forend; $r++) { $adults_map[$r] = $adults_per_room; $children_map[$r] = $children_per_room; if ($r == $or_forend) { $adults_map[$r] += $adults_left; $children_map[$r] += $children_left; } } } } // count total rooms booked $totalrooms = ($set_closed && $status == 'confirmed' ? count($rooms_pool) : ($num_rooms > 1 && $num_rooms <= $room['units'] ? $num_rooms : 1)); $totalrooms = !empty($split_stay_data) ? count($split_stay_data) : $totalrooms; // attempt to get the default payment method if (!$paymentmeth && ($auto_paymeth || $status == 'standby')) { // get the default payment method, if any $paymentmeth = $this->getDefaultPaymentMethod($auto_paymeth); } // prepare booking record $booking = new stdClass; $booking->custdata = $customer_data; $booking->ts = $now_ts; $booking->status = $status; $booking->days = $daysdiff; $booking->checkin = $checkin_ts; $booking->checkout = $checkout_ts; $booking->custmail = $customer_email; $booking->sid = $sid; $booking->idpayment = $paymentmeth; $booking->ujid = (int)$store_ujid; $booking->roomsnum = $totalrooms; $booking->total = $set_total > 0 ? $set_total : null; if ($this->get('admin_notes')) { $booking->adminnotes = $this->get('admin_notes', ''); } $booking->lang = VikBooking::guessBookingLangFromCountry($country_code); $booking->country = $country_code; $booking->tot_taxes = $set_taxes > 0 ? $set_taxes : null; $booking->phone = $phone_number; $booking->closure = ($status == 'standby' ? 0 : ($set_closed || $units_closed ? 1 : 0)); $booking->split_stay = !empty($split_stay_data) ? 1 : 0; // store reservation if ($status == 'confirmed') { // occupy the rooms $insertedbusy = []; if (empty($split_stay_data)) { // only when closing other rooms we have an array containing multiple rooms info foreach ($rooms_pool as $nowroom) { $nowforend = $set_closed ? $nowroom['units'] : $forend; for ($b = 1; $b <= $nowforend; $b++) { $busy_record = new stdClass; $busy_record->idroom = (int)$nowroom['id']; $busy_record->checkin = (int)$checkin_ts; $busy_record->checkout = (int)$checkout_ts; $busy_record->realback = (int)$realback_ts; // store busy record $dbo->insertObject('#__vikbooking_busy', $busy_record, 'id'); if (isset($busy_record->id)) { $insertedbusy[] = $busy_record->id; } } } } else { // for split stay bookings we occupy the rooms on the individual stay dates foreach ($split_stay_data as $split_stay) { $busy_record = new stdClass; $busy_record->idroom = (int)$split_stay['idroom']; $busy_record->checkin = (int)$split_stay['checkin_ts']; $busy_record->checkout = (int)$split_stay['checkout_ts']; $busy_record->realback = (int)$split_stay['realback_ts']; // store busy record $dbo->insertObject('#__vikbooking_busy', $busy_record, 'id'); if (isset($busy_record->id)) { $insertedbusy[] = $busy_record->id; } } } if (!$insertedbusy) { $this->setError('No records were occupied'); return false; } // store booking record $dbo->insertObject('#__vikbooking_orders', $booking, 'id'); if (!isset($booking->id)) { $this->setError('Could not store the reservation record'); return false; } // get the newly generated booking ID $newoid = $booking->id; // set the new booking ID $this->setNewBookingID($newoid); if (!empty($split_stay_data)) { // save transient on db for split stay information VBOFactory::getConfig()->set('split_stay_' . $newoid, json_encode($split_stay_data)); } // check if some of the rooms booked have shared calendars VikBooking::updateSharedCalendars($newoid, [$room['id']], $checkin_ts, $checkout_ts); // confirmation number $confirmnumber = VikBooking::generateConfirmNumber($newoid, true); // store busy records/booking relations foreach ($insertedbusy as $lid) { $obusy_record = new stdClass; $obusy_record->idorder = (int)$newoid; $obusy_record->idbusy = (int)$lid; // store busy relation record $dbo->insertObject('#__vikbooking_ordersbusy', $obusy_record, 'id'); } // write room records foreach ($rooms_pool as $rind => $nowroom) { $room_indexes_usemap = []; for ($r = 1; $r <= $or_forend; $r++) { // Assign room specific unit $info_room_avail = [ 'id' => $newoid, 'checkin' => (!empty($nowroom['checkin_ts']) ? $nowroom['checkin_ts'] : $checkin_ts), 'checkout' => (!empty($nowroom['checkout_ts']) ? $nowroom['checkout_ts'] : $checkout_ts), ]; $room_indexes = $set_room_indexes === true ? VikBooking::getRoomUnitNumsAvailable($info_room_avail, $nowroom['id']) : []; $use_ind_key = 0; if ($room_indexes) { if (!array_key_exists($nowroom['id'], $room_indexes_usemap)) { $room_indexes_usemap[$nowroom['id']] = $use_ind_key; } else { $use_ind_key = $room_indexes_usemap[$nowroom['id']]; } } // room custom cost $or_cust_cost = $cust_cost > 0.00 ? $cust_cost : 0; $or_cust_cost = $or_forend > 1 && $or_cust_cost > 0 ? round(($or_cust_cost / $or_forend), 2) : $or_cust_cost; // room cost from website rate plan is always based on one room $or_room_cost = $room_cost > 0.00 ? $room_cost : 0; if (!empty($split_stay_data) && $cust_cost > 0) { // set the average cost per room in case of split stay $cost_per_room = ($cust_cost / count($split_stay_data)); $or_cust_cost = round($cost_per_room, 2); if (isset($split_stay_data[$rind]) && isset($split_stay_data[$rind]['nights'])) { // count the average cost per room depending on the number of nights of stay $cost_per_room = $cust_cost / $daysdiff * $split_stay_data[$rind]['nights']; $or_cust_cost = round($cost_per_room, 2); } } // room guests $room_adults = isset($adults_map[$r]) && empty($split_stay_data) ? $adults_map[$r] : $adults; $room_children = isset($children_map[$r]) && empty($split_stay_data) ? $children_map[$r] : $children; // attempt to gather the children age for this room $room_children_age = null; if ($room_children && $children_age) { $children_age_pool = []; for ($ic = 0; $ic < $room_children; $ic++) { if (!$children_age) { $children_age_pool[] = 0; continue; } // shorten the list and push the current child age $current_child_age = array_shift($children_age); $children_age_pool[] = (int) $current_child_age; } $room_children_age = json_encode(['age' => $children_age_pool]); } // store room record $room_record = new stdClass; $room_record->idorder = (int) $newoid; $room_record->idroom = (int) $nowroom['id']; $room_record->adults = $room_adults; $room_record->children = $room_children; $room_record->idtar = !empty($id_tariff) ? $id_tariff : null; $room_record->childrenage = $room_children_age; $room_record->t_first_name = $t_first_name; $room_record->t_last_name = $t_last_name; $room_record->roomindex = count($room_indexes) ? (int) $room_indexes[$use_ind_key] : null; $room_record->cust_cost = $cust_cost > 0.00 ? $or_cust_cost : null; $room_record->cust_idiva = $cust_cost > 0.00 && !empty($id_tax) ? $id_tax : null; $room_record->room_cost = $room_cost > 0.00 ? $or_room_cost : null; $dbo->insertObject('#__vikbooking_ordersrooms', $room_record, 'id'); if (!isset($room_record->id)) { $this->setError('Could not store room reservation record for booking ID ' . $room_record->idorder); continue; } // Assign room specific unit if ($room_indexes) { $room_indexes_usemap[$nowroom['id']]++; } } } } elseif ($status == 'standby') { // store booking record $dbo->insertObject('#__vikbooking_orders', $booking, 'id'); if (!isset($booking->id)) { $this->setError('Could not store the reservation record'); return false; } // get the newly generated booking ID $newoid = $booking->id; // set the new booking ID $this->setNewBookingID($newoid); if (!empty($split_stay_data)) { // save transient on db for split stay information VBOFactory::getConfig()->set('split_stay_' . $newoid, json_encode($split_stay_data)); } // write room records foreach ($rooms_pool as $rind => $nowroom) { for ($r = 1; $r <= $or_forend; $r++) { // room custom cost $or_cust_cost = $cust_cost > 0.00 ? $cust_cost : 0; $or_cust_cost = $or_forend > 1 && $or_cust_cost > 0 ? round(($or_cust_cost / $or_forend), 2) : $or_cust_cost; // room cost from website rate plan is always based on one room $or_room_cost = $room_cost > 0.00 ? $room_cost : 0; if (!empty($split_stay_data) && $cust_cost > 0) { // set the average cost per room in case of split stay $cost_per_room = ($cust_cost / count($split_stay_data)); $or_cust_cost = round($cost_per_room, 2); if (isset($split_stay_data[$rind]) && isset($split_stay_data[$rind]['nights'])) { // count the average cost per room depending on the number of nights of stay $cost_per_room = $cust_cost / $daysdiff * $split_stay_data[$rind]['nights']; $or_cust_cost = round($cost_per_room, 2); } } // room guests $room_adults = isset($adults_map[$r]) && empty($split_stay_data) ? $adults_map[$r] : $adults; $room_children = isset($children_map[$r]) && empty($split_stay_data) ? $children_map[$r] : $children; // attempt to gather the children age for this room $room_children_age = null; if ($room_children && $children_age) { $children_age_pool = []; for ($ic = 0; $ic < $room_children; $ic++) { if (!$children_age) { $children_age_pool[] = 0; continue; } // shorten the list and push the current child age $current_child_age = array_shift($children_age); $children_age_pool[] = (int) $current_child_age; } $room_children_age = json_encode(['age' => $children_age_pool]); } // store room record $room_record = new stdClass; $room_record->idorder = (int) $newoid; $room_record->idroom = (int) $nowroom['id']; $room_record->adults = $room_adults; $room_record->children = $room_children; $room_record->idtar = !empty($id_tariff) ? $id_tariff : null; $room_record->childrenage = $room_children_age; $room_record->t_first_name = $t_first_name; $room_record->t_last_name = $t_last_name; $room_record->cust_cost = $cust_cost > 0.00 ? $or_cust_cost : null; $room_record->cust_idiva = $cust_cost > 0.00 && !empty($id_tax) ? $id_tax : null; $room_record->room_cost = $room_cost > 0.00 ? $or_room_cost : null; $dbo->insertObject('#__vikbooking_ordersrooms', $room_record, 'id'); if (!isset($room_record->id)) { $this->setError('Could not store room reservation record for booking ID ' . $room_record->idorder); continue; } if (empty($split_stay_data)) { // lock room for pending status $tmplock_record = new stdClass; $tmplock_record->idroom = (int)$room['id']; $tmplock_record->checkin = $checkin_ts; $tmplock_record->checkout = $checkout_ts; $tmplock_record->until = VikBooking::getMinutesLock(true); $tmplock_record->realback = $realback_ts; $tmplock_record->idorder = (int)$newoid; $dbo->insertObject('#__vikbooking_tmplock', $tmplock_record, 'id'); } } } if (!empty($split_stay_data)) { // lock rooms for pending status on proper stay dates foreach ($split_stay_data as $split_stay) { $tmplock_record = new stdClass; $tmplock_record->idroom = (int)$split_stay['idroom']; $tmplock_record->checkin = (int)$split_stay['checkin_ts']; $tmplock_record->checkout = (int)$split_stay['checkout_ts']; $tmplock_record->until = VikBooking::getMinutesLock(true); $tmplock_record->realback = (int)$split_stay['realback_ts']; $tmplock_record->idorder = (int)$newoid; $dbo->insertObject('#__vikbooking_tmplock', $tmplock_record, 'id'); } } } $newoid = $this->getNewBookingID(); if (!$newoid) { return false; } // assign booking to customer if (!$cpin->getNewCustomerId() && !empty($customer_id)) { $cpin->setNewPin($customer_pin); $cpin->setNewCustomerId($customer_id); } $cpin->saveCustomerBooking($newoid); // Booking History $history_obj = VikBooking::getBookingHistoryInstance($newoid); $forced_reason = !empty($forced_reason) ? " {$forced_reason}" : $forced_reason; $caller_id = $now_user->name ? "({$now_user->name})" : ''; if ($this->getCaller()) { $caller_id = '(' . $this->getCaller() . ')'; if ($this->getHistoryData()) { $history_obj->setExtraData($this->getHistoryData()); } } $history_obj->store('NB', trim($caller_id . $forced_reason)); if ($status == 'confirmed' || ($status == 'standby' && class_exists('VCMRequestAvailability'))) { // Invoke Channel Manager $vcm_autosync = VikBooking::vcmAutoUpdate(); if ($vcm_autosync > 0) { $vcm_obj = VikBooking::getVcmInvoker(); $vcm_obj->setOids([$newoid])->setSyncType('new'); $sync_result = $vcm_obj->doSync(); if ($sync_result === false) { // set error message $vcm_err = $vcm_obj->getError(); $this->setError(JText::translate('VBCHANNELMANAGERRESULTKO') . (!empty($vcm_err) ? ' - ' . $vcm_err : '')); // return true because the booking was actually stored return true; } } elseif (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'synch.vikbooking.php')) { // set the necessary action to invoke VCM $vcm_sync_url = 'index.php?option=com_vikbooking&task=invoke_vcm&stype=new&cid[]=' . $newoid . '&returl=' . urlencode('index.php?option=com_vikbooking&task=calendar&cid[]=' . $room['id']); $this->setChannelManagerAction(JText::translate('VBCHANNELMANAGERINVOKEASK') . ' <button type="button" class="btn btn-primary" onclick="document.location.href=\'' . $vcm_sync_url . '\';">' . JText::translate('VBCHANNELMANAGERSENDRQ') . '</button>'); } } if (VikBooking::isAdmin()) { /** * Trigger event to allow third party plugins to intercept the admin new booking event. * * @since 1.16.8 (J) - 1.6.8 (WP) */ VBOFactory::getPlatform()->getDispatcher()->trigger('onAfterCreateNewBookingAdmin', [$newoid]); } return true; } /** * Attempts to find the default payment method to be assigned to a booking. * * @param bool $auto If true, it was requested to automatically find the best payment method. * * @return string The payment method string for the reservation "ID=Name", or an empty string. * * @since 1.17.3 (J) - 1.7.3 (WP) */ protected function getDefaultPaymentMethod($auto = false) { $dbo = JFactory::getDbo(); $dbo->setQuery( $dbo->getQuery(true) ->select('*') ->from($dbo->qn('#__vikbooking_gpayments')) ->order($dbo->qn('published') . ' DESC') ->order($dbo->qn('ordering') . ' ASC') ->order($dbo->qn('setconfirmed') . ' ASC') ->order($dbo->qn('name') . ' ASC') ); $methods = $dbo->loadAssocList(); if ($auto) { // exclude all the offline or unpublished payment methods $methods = array_filter($methods, function($method) { return !((bool) intval($method['setconfirmed'])) && (bool) intval($method['published']); }); // reset array keys $methods = array_values($methods); } if ($methods) { // default payment method found return sprintf('%s=%s', $methods[0]['id'], $methods[0]['name']); } // nothing was found return ''; } }