<?php /** * @package VikBooking * @subpackage core * @author E4J s.r.l. * @copyright Copyright (C) 2021 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 bookings controller. * * @since 1.16.0 (J) - 1.6.0 (WP) */ class VikBookingControllerBookings extends JControllerAdmin { /** * AJAX endpoint to search for an extra service name. * * @return void */ public function search_service() { if (!JSession::checkToken()) { VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN')); } $dbo = JFactory::getDbo(); $service_name = VikRequest::getString('service_name', '', 'request'); $max_results = VikRequest::getInt('max_results', 10, 'request'); $sql_term = $dbo->quote("%{$service_name}%"); $sql_clause = !empty($service_name) ? 'LIKE ' . $sql_term : 'IS NOT NULL'; $q = "SELECT `or`.`idorder`, `or`.`idroom`, `or`.`adults`, `or`.`children`, `or`.`extracosts`, `o`.`days` AS `nights`, `o`.`ts`, `r`.`name` AS `room_name` FROM `#__vikbooking_ordersrooms` AS `or` LEFT JOIN `#__vikbooking_orders` AS `o` ON `or`.`idorder`=`o`.`id` LEFT JOIN `#__vikbooking_rooms` AS `r` ON `or`.`idroom`=`r`.`id` WHERE `or`.`extracosts` {$sql_clause} ORDER BY `or`.`idorder` DESC"; $dbo->setQuery($q, 0, $max_results); $dbo->execute(); if (!$dbo->getNumRows()) { // no results VBOHttpDocument::getInstance()->json([]); } $results = $dbo->loadAssocList(); $matching_services = []; foreach ($results as $k => $result) { $extra_services = json_decode($result['extracosts'], true); if (empty($extra_services)) { continue; } foreach ($extra_services as $extra_service) { if (empty($service_name) || stristr($extra_service['name'], $service_name) !== false || stristr($service_name, $extra_service['name']) !== false) { // matching service found $matching_service = $result; unset($matching_service['extracosts']); $matching_service['service'] = $extra_service; $matching_service['service']['format_cost'] = VikBooking::getCurrencySymb() . ' ' . VikBooking::numberFormat($extra_service['cost']); $matching_service['format_dt'] = VikBooking::formatDateTs($result['ts']); // push result $matching_services[] = $matching_service; if (count($matching_services) >= $max_results) { break 2; } } } } // output the JSON encoded list of matching results found VBOHttpDocument::getInstance()->json($matching_services); } /** * AJAX endpoint to count the number of uses for various coupon codes. * * @return void */ public function coupons_use_count() { if (!JSession::checkToken()) { VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN')); } $dbo = JFactory::getDbo(); $coupon_codes = VikRequest::getVar('coupon_codes', array()); $use_counts = []; foreach ($coupon_codes as $coupon_code) { $q = "SELECT COUNT(*) FROM `#__vikbooking_orders` WHERE `coupon` LIKE " . $dbo->quote("%;{$coupon_code}"); $dbo->setQuery($q); $dbo->execute(); $use_counts[] = [ 'code' => $coupon_code, 'count' => (int)$dbo->loadResult(), ]; } // output the JSON encoded list of coupon use counts VBOHttpDocument::getInstance()->json($use_counts); } /** * AJAX endpoint to dynamically search for customers. Compatible with select2. * * @return void */ public function customers_search() { if (!JSession::checkToken()) { VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN')); } $dbo = JFactory::getDbo(); $term = VikRequest::getString('term', '', 'request'); $response = [ 'results' => [], 'pagination' => [ 'more' => false, ], ]; if (empty($term)) { // output the JSON object with no results VBOHttpDocument::getInstance()->json($response); } $sql_term = $dbo->quote("%{$term}%"); $q = "SELECT `c`.`id`, `c`.`first_name`, `c`.`last_name`, `c`.`country`, (SELECT COUNT(*) FROM `#__vikbooking_customers_orders` AS `co` WHERE `co`.`idcustomer`=`c`.`id`) AS `tot_bookings` FROM `#__vikbooking_customers` AS `c` WHERE CONCAT_WS(' ', `c`.`first_name`, `c`.`last_name`) LIKE {$sql_term} OR `email` LIKE {$sql_term} ORDER BY `c`.`first_name` ASC, `c`.`last_name` ASC;"; $dbo->setQuery($q); $customers = $dbo->loadAssocList(); if ($customers) { foreach ($customers as $k => $customer) { $customers[$k]['text'] = trim($customer['first_name'] . ' ' . $customer['last_name']) . ' (' . $customer['tot_bookings'] . ')'; } // push results found $response['results'] = $customers; } // output the JSON encoded object with results found VBOHttpDocument::getInstance()->json($response); } /** * AJAX endpoint to dynamically search for rooms. Compatible with select2. * * @return void * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function rooms_search() { if (!JSession::checkToken()) { VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN')); } $dbo = JFactory::getDbo(); $app = JFactory::getApplication(); $term = $app->input->getString('term', ''); $response = [ 'results' => [], 'pagination' => [ 'more' => false, ], ]; if (empty($term)) { // output the JSON object with no results VBOHttpDocument::getInstance($app)->json($response); } $dbo->setQuery( $dbo->getQuery(true) ->select([ $dbo->qn('id'), $dbo->qn('name', 'text'), $dbo->qn('img'), ]) ->from($dbo->qn('#__vikbooking_rooms')) ->where($dbo->qn('name') . ' LIKE ' . $dbo->q("%{$term}%")) ->order($dbo->qn('avail') . ' DESC') ->order($dbo->qn('name') . ' ASC') ); // set results found $response['results'] = $dbo->loadAssocList(); // load and map mini thumbnails $mini_thumbnails = VBORoomHelper::getInstance()->loadMiniThumbnails($response['results']); $response['results'] = array_map(function($room) use ($mini_thumbnails) { if ($mini_thumbnails[$room['id']] ?? '') { // set mini thumbnail URL $room['img'] = $mini_thumbnails[$room['id']]; } else { unset($room['img']); } return $room; }, $response['results']); // output the JSON encoded object with results found VBOHttpDocument::getInstance()->json($response); } /** * AJAX endpoint to dynamically search for bookings. Compatible with select2. * * @return void * * @since 1.18.0 (J) - 1.8.0 (WP) */ public function bookings_search() { if (!JSession::checkToken()) { VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN')); } $dbo = JFactory::getDbo(); $app = JFactory::getApplication(); $booking_key = trim($app->input->getString('term', '')); $booking_status = array_values(array_filter((array) $app->input->get('status', [], 'array'))); $response = [ 'results' => [], 'pagination' => [ 'more' => false, ], ]; if (empty($booking_key)) { // output the JSON object with no results VBOHttpDocument::getInstance($app)->json($response); } // attempt to detect a booking ID $booking_id = 0; if (preg_match("/^[0-9]+$/", $booking_key)) { // only numbers should be a booking ID $booking_id = $booking_key; } elseif (preg_match('/^(?=.*?\d)(?=.*?[A-Z])[A-Z\d]+$/', $booking_key)) { /** * Matched both numbers and upper-case letters, so it has to be an OTA booking ID, not a customer name. * Regex breakdown: * beginning of string * lookahead for at least one digit * lookahead for at least one upper-case letter * match one or more upper-case letters or digits * end of string */ $booking_id = $booking_key; } // start the query $q = $dbo->getQuery(true) ->select([ $dbo->qn('o.id'), $dbo->qn('o.custdata'), $dbo->qn('o.days'), $dbo->qn('o.status'), $dbo->qn('o.checkin'), $dbo->qn('o.checkout'), $dbo->qn('o.idorderota'), $dbo->qn('o.channel'), $dbo->qn('c.first_name'), $dbo->qn('c.last_name'), $dbo->qn('c.pic'), ]) ->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($dbo->qn('o.closure') . ' = 0'); if ($booking_status) { // filter by booking status if (count($booking_status) === 1) { // single booking status $q->where($dbo->qn('o.status') . ' = ' . $dbo->q($booking_status[0])); } else { // multiple booking statuses $q->where($dbo->qn('o.status') . ' IN (' . implode(', ', array_map([$dbo, 'q'], $booking_status)) . ')'); } } if (!empty($booking_id)) { // search by booking ID or OTA booking ID only if (preg_match("/^[0-9]+$/", (string) $booking_id)) { // only numbers could be both website and OTA $q->andWhere([ $dbo->qn('o.id') . ' = ' . (int) $booking_id, $dbo->qn('o.idorderota') . ' = ' . $dbo->q($booking_id), ], $glue = 'OR'); } else { // alphanumeric IDs can only belong to an OTA reservation $q->where($dbo->qn('o.idorderota') . ' = ' . $dbo->q($booking_id)); } } else { // search by different values if (stripos($booking_key, 'id:') === 0) { // search by ID or OTA ID $seek_parts = explode('id:', $booking_key); $seek_value = trim($seek_parts[1]); $q->andWhere([ $dbo->qn('o.id') . ' = ' . $dbo->q($seek_value), $dbo->qn('o.idorderota') . ' = ' . $dbo->q($seek_value), ], $glue = 'OR'); } elseif (stripos($booking_key, 'otaid:') === 0) { // search by OTA Booking ID $seek_parts = explode('otaid:', $booking_key); $seek_value = trim($seek_parts[1]); $q->where($dbo->qn('o.idorderota') . ' = ' . $dbo->q($seek_value)); } elseif (stripos($booking_key, 'coupon:') === 0) { // search by coupon code $seek_parts = explode('coupon:', $booking_key); $seek_value = trim($seek_parts[1]); $q->where($dbo->qn('o.coupon') . ' LIKE ' . $dbo->q("%{$seek_value}%")); } elseif (stripos($booking_key, 'name:') === 0) { // search by customer nominative $seek_parts = explode('name:', $booking_key); $seek_value = trim($seek_parts[1]); $q->where('CONCAT_WS(\' \', ' . $dbo->qn('c.first_name') . ', ' . $dbo->qn('c.last_name') . ') LIKE ' . $dbo->q("%{$seek_value}%")); } elseif (strpos($booking_key, '@') !== false) { // search by customer email $q->where($dbo->qn('o.custmail') . ' = ' . $dbo->q($booking_key)); } elseif (strpos($booking_key, '+') === 0) { // search by customer phone $q->where($dbo->qn('o.phone') . ' = ' . $dbo->q($booking_key)); } else { // seek for various values if (preg_match("/^[a-z\s]+$/i", (string) $booking_key)) { // when only letters (or spaces) look only for the customer name $q->where('CONCAT_WS(\' \', ' . $dbo->qn('c.first_name') . ', ' . $dbo->qn('c.last_name') . ') LIKE ' . $dbo->q("%{$booking_key}%")); } else { // look for both customer name and booking ID $q->andWhere([ 'CONCAT_WS(\' \', ' . $dbo->qn('c.first_name') . ', ' . $dbo->qn('c.last_name') . ') LIKE ' . $dbo->q("%{$booking_key}%"), $dbo->qn('o.id') . ' = ' . $dbo->q($booking_key), $dbo->qn('o.idorderota') . ' = ' . $dbo->q($booking_key), ], $glue = 'OR'); } } } // order by most recent bookings $q->order($dbo->qn('id') . ' DESC'); $dbo->setQuery($q); // set results found $response['results'] = $dbo->loadAssocList(); // default icon for website reservations $source_def_icon_cls = VikBookingIcons::i('hotel'); // map the results with the required properties $response['results'] = array_map(function($booking) use ($source_def_icon_cls) { // build "text" property $text = $booking['id']; if (!empty($booking['first_name'])) { // use customer nominative when available $text = trim($booking['first_name'] . ' ' . $booking['last_name']); } elseif (!empty($booking['custdata'])) { $text = VikBooking::getFirstCustDataField($booking['custdata']); } $booking['text'] = $text; // build "img" property if (!empty($booking['pic'])) { // use guest profile picture $booking['img'] = strpos($booking['pic'], 'http') === 0 ? $booking['pic'] : VBO_SITE_URI . 'resources/uploads/' . $booking['pic']; } elseif (!empty($booking['channel'])) { // use channel logo $ch_logo_obj = VikBooking::getVcmChannelsLogo($booking['channel'], true); $booking['img'] = is_object($ch_logo_obj) ? $ch_logo_obj->getTinyLogoURL() : ''; } if (empty($booking['img'])) { // always set an empty string $booking['img'] = ''; // set the default icon class $booking['icon_class'] = $source_def_icon_cls; } // return the mapped booking element return $booking; }, $response['results']); // output the JSON encoded object with results found VBOHttpDocument::getInstance()->json($response); } /** * AJAX endpoint to dynamically search for customers and build elements. Compatible with select2. * * @return void * * @since 1.18.0 (J) - 1.8.0 (WP) */ public function customer_elements_search() { if (!JSession::checkToken()) { VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN')); } $dbo = JFactory::getDbo(); $app = JFactory::getApplication(); $search_key = trim($app->input->getString('term', '')); $response = [ 'results' => [], 'pagination' => [ 'more' => false, ], ]; if (empty($search_key)) { // output the JSON object with no results VBOHttpDocument::getInstance($app)->json($response); } // start the query $q = $dbo->getQuery(true) ->select('*') ->from($dbo->qn('#__vikbooking_customers')) ->where(1); if (preg_match('/^[a-z0-9\.\-\_]+\@[a-z0-9\.\-\_]+\.[a-z0-9\.\-\_]+$/i', $search_key)) { // full email address detected $q->where($dbo->qn('email') . ' = ' . $dbo->q($search_key)); } else { // search by different values $seek_clauses = []; // search by nominative $seek_clauses[] = 'CONCAT_WS(" ", ' . $dbo->qn('first_name') . ', ' . $dbo->qn('last_name') . ') LIKE ' . $dbo->q('%' . $search_key . '%'); // search by company name $seek_clauses[] = $dbo->qn('company') . ' LIKE ' . $dbo->q('%' . $search_key . '%'); if (preg_match('/^\+?[0-9\s]+$/i', $search_key)) { // search by phone number $seek_clauses[] = $dbo->qn('phone') . ' = ' . $dbo->q($search_key); } if (strpos($search_key, '@') !== false) { // search by email address $seek_clauses[] = $dbo->qn('email') . ' LIKE ' . $dbo->q('%' . $search_key . '%'); } if (preg_match('/[0-9]+/', $search_key)) { // search by company VAT number $seek_clauses[] = $dbo->qn('vat') . ' = ' . $dbo->q($search_key); // search by PIN code $seek_clauses[] = $dbo->qn('pin') . ' = ' . $dbo->q($search_key); } // set multiple search clauses $q->andWhere($seek_clauses, 'OR'); } // order by customer nominative $q->order($dbo->qn('first_name') . ' ASC'); $q->order($dbo->qn('last_name') . ' ASC'); $dbo->setQuery($q); // set results found $response['results'] = $dbo->loadAssocList(); // default icon for customers $source_def_icon_cls = VikBookingIcons::i('user'); // map the results with the required properties $response['results'] = array_map(function($customer) use ($source_def_icon_cls) { // build "text" property $customer['text'] = trim($customer['first_name'] . ' ' . $customer['last_name']); // build "img" property if (!empty($customer['pic'])) { // use customer profile picture $customer['img'] = strpos($customer['pic'], 'http') === 0 ? $customer['pic'] : VBO_SITE_URI . 'resources/uploads/' . $customer['pic']; } elseif (!empty($customer['country']) && is_file(implode(DIRECTORY_SEPARATOR, [VBO_ADMIN_PATH, 'resources', 'countries', $customer['country'] . '.png']))) { // use customer country flag $customer['img'] = VBO_ADMIN_URI . 'resources/countries/' . $customer['country'] . '.png'; $customer['img_title'] = $customer['country']; } if (empty($customer['img'])) { // always set an empty string $customer['img'] = ''; // set the default icon class $customer['icon_class'] = $source_def_icon_cls; } // handle custom fields if (!empty($customer['cfields'])) { $custom_fields = (array) json_decode($customer['cfields'], true); if ($custom_fields) { $customer['cfields'] = $custom_fields; } } if (!is_array($customer['cfields']) || !$customer['cfields']) { // ensure this is a null value $customer['cfields'] = null; } // return the mapped customer element return $customer; }, $response['results']); // output the JSON encoded object with results found VBOHttpDocument::getInstance()->json($response); } /** * Regular task to update the status of a cancelled booking to pending (stand-by). * * @return void */ public function set_to_pending() { $dbo = JFactory::getDbo(); $app = JFactory::getApplication(); $bid = $app->input->getInt('bid', 0); if (!JSession::checkToken() && !JSession::checkToken('get')) { $app->enqueueMessage(JText::translate('JINVALID_TOKEN'), 'error'); $app->redirect('index.php?option=com_vikbooking&task=editorder&cid[]=' . $bid); $app->close(); } $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . $bid; $dbo->setQuery($q, 0, 1); $dbo->execute(); if (!$dbo->getNumRows()) { $app->enqueueMessage('Booking not found', 'error'); $app->redirect('index.php?option=com_vikbooking&task=orders'); $app->close(); } $booking = $dbo->loadAssoc(); if ($booking['status'] != 'cancelled') { $app->enqueueMessage('Booking status must be -Cancelled-', 'error'); $app->redirect('index.php?option=com_vikbooking&task=editorder&cid[]=' . $booking['id']); $app->close(); } $q = "UPDATE `#__vikbooking_orders` SET `status`='standby' WHERE `id`=" . $booking['id']; $dbo->setQuery($q); $dbo->execute(); $app->enqueueMessage(JText::translate('JLIB_APPLICATION_SAVE_SUCCESS')); $app->redirect('index.php?option=com_vikbooking&task=editorder&cid[]=' . $booking['id']); $app->close(); } /** * AJAX endpoint to assign a room index to a room booking record. * * @return void */ public function set_room_booking_subunit() { if (!JSession::checkToken()) { VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN')); } $dbo = JFactory::getDbo(); $app = JFactory::getApplication(); $bid = $app->input->getInt('bid', 0); $rid = $app->input->getInt('rid', 0); $orkey = $app->input->getInt('orkey', 0); $rindex = $app->input->getInt('rindex', 0); if (empty($bid) || empty($rid)) { VBOHttpDocument::getInstance()->close(500, 'Missing request values'); } $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=" . $bid; $dbo->setQuery($q, 0, 1); $booking = $dbo->loadAssoc(); if (!$booking) { VBOHttpDocument::getInstance()->close(404, 'Booking not found'); } $booking_rooms = VikBooking::loadOrdersRoomsData($booking['id']); if (!$booking_rooms) { VBOHttpDocument::getInstance()->close(500, 'No rooms booking found'); } if (!isset($booking_rooms[$orkey]) || $booking_rooms[$orkey]['idroom'] != $rid) { VBOHttpDocument::getInstance()->close(500, 'Invalid room booking record'); } // update room record $room_record = new stdClass; $room_record->id = $booking_rooms[$orkey]['id']; $room_record->roomindex = $rindex; $dbo->updateObject('#__vikbooking_ordersrooms', $room_record, 'id'); // build list of affected nights $nights_list_ymd = []; $from_checkin_info = getdate($booking['checkin']); for ($n = 0; $n < $booking['days']; $n++) { // push affected night $nights_list_ymd[] = date('Y-m-d', mktime(0, 0, 0, $from_checkin_info['mon'], ($from_checkin_info['mday'] + $n), $from_checkin_info['year'])); } // build return values $response = [ 'bid' => $booking['id'], 'rid' => $booking_rooms[$orkey]['idroom'], 'rindex' => $rindex, 'from' => date('Y-m-d', $booking['checkin']), 'to' => date('Y-m-d', $booking['checkout']), 'nights' => $nights_list_ymd, ]; // output the JSON encoded object VBOHttpDocument::getInstance()->json($response); } /** * AJAX endpoint to swap one sub-unit index with another for the same room ID and dates. * * @return void * * @since 1.16.2 (J) - 1.6.2 (WP) */ public function swap_room_subunits() { if (!JSession::checkToken()) { VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN')); } $dbo = JFactory::getDbo(); $app = JFactory::getApplication(); $bid_one = $app->input->getInt('bid_one', 0); $bid_two = $app->input->getInt('bid_two', 0); $rid = $app->input->getInt('rid', 0); $index_one = $app->input->getInt('index_one', 0); $index_two = $app->input->getInt('index_two', 0); $checkin = $app->input->getString('checkin', ''); if (!$bid_one || !$bid_two || !$rid || !$index_one || !$index_two) { VBOHttpDocument::getInstance()->close(500, 'Missing request values'); } // collect the booking information $booking_one = VikBooking::getBookingInfoFromID($bid_one); $booking_two = VikBooking::getBookingInfoFromID($bid_two); if (!$booking_one || !$booking_two) { VBOHttpDocument::getInstance()->close(404, 'Could not find the involved reservations'); } // get room reservation records $rooms_one = VikBooking::loadOrdersRoomsData($bid_one); $rooms_two = VikBooking::loadOrdersRoomsData($bid_two); if (!$rooms_one || !$rooms_two) { VBOHttpDocument::getInstance()->close(404, 'Could not find the involved room reservation records'); } // find the record IDs involved and room name $update_id_one = null; $update_id_two = null; $room_name = ''; foreach ($rooms_one as $room_one) { if ($room_one['idroom'] == $rid && $room_one['roomindex'] == $index_one) { $update_id_one = $room_one['id']; $room_name = $room_one['room_name']; break; } } foreach ($rooms_two as $room_two) { if ($room_two['idroom'] == $rid && $room_two['roomindex'] == $index_two) { $update_id_two = $room_two['id']; $room_name = $room_two['room_name']; break; } } if (!$update_id_one || !$update_id_two) { VBOHttpDocument::getInstance()->close(500, 'Could not find the involved room reservation record IDs'); } // swap first room record $q = $dbo->getQuery(true); $q->update($dbo->qn('#__vikbooking_ordersrooms')) ->set($dbo->qn('roomindex') . ' = ' . $index_two) ->where($dbo->qn('id') . ' = ' . (int)$update_id_one); $dbo->setQuery($q); $dbo->execute(); $result = (bool)$dbo->getAffectedRows(); // swap second room record $q = $dbo->getQuery(true); $q->update($dbo->qn('#__vikbooking_ordersrooms')) ->set($dbo->qn('roomindex') . ' = ' . $index_one) ->where($dbo->qn('id') . ' = ' . (int)$update_id_two); $dbo->setQuery($q); $dbo->execute(); $result = $result || (bool)$dbo->getAffectedRows(); if (!$result) { VBOHttpDocument::getInstance()->close(500, 'No records were updated for the involved room reservation IDs'); } // update history records $user = JFactory::getUser(); VikBooking::getBookingHistoryInstance()->setBid($booking_one['id'])->store('MB', JText::sprintf('VBO_SWAP_ROOMS_LOG', $room_name, $index_one, $index_two) . " ({$user->name})"); if ($booking_one['id'] != $booking_two['id']) { VikBooking::getBookingHistoryInstance()->setBid($booking_two['id'])->store('MB', JText::sprintf('VBO_SWAP_ROOMS_LOG', $room_name, $index_two, $index_one) . " ({$user->name})"); } // output the JSON encoded response object VBOHttpDocument::getInstance()->json([ 'swap_from' => $index_one, 'swap_to' => $index_two, ]); } /** * AJAX endpoint to remove the type flag (i.e. "overbooking") from a booking ID. * * @return void */ public function delete_type_flag() { if (!JSession::checkToken()) { VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN')); } $dbo = JFactory::getDbo(); $app = JFactory::getApplication(); $bid = $app->input->getUInt('bid', 0); $flag = $app->input->getString('flag', ''); if (!$bid) { VBOHttpDocument::getInstance()->close(404, JText::translate('VBPEDITBUSYONE')); } $q = $dbo->getQuery(true); $q->update($dbo->qn('#__vikbooking_orders')) ->set($dbo->qn('type') . ' = ' . $dbo->q('')) ->where($dbo->qn('id') . ' = ' . $bid); $dbo->setQuery($q); $dbo->execute(); if (!(bool)$dbo->getAffectedRows()) { VBOHttpDocument::getInstance()->close(500, 'Could not update the booking record'); } if (!strcasecmp($flag, 'overbooking')) { // update history records $user = JFactory::getUser(); VikBooking::getBookingHistoryInstance($bid)->store('OB', JText::translate('VBO_OVERBOOKING_FLAG_REMOVED') . " ({$user->name})"); } VBOHttpDocument::getInstance()->json([$bid => 'ok']); } /** * AJAX endpoint to set the AI options related to automatic guest review for a booking. * * @return void * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function set_ai_auto_guest_review_opt() { if (!JSession::checkToken()) { VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN')); } $dbo = JFactory::getDbo(); $app = JFactory::getApplication(); $bid = $app->input->getInt('bid', 0); $opt = $app->input->get('opt', [], 'array'); if (!$bid || !$opt) { VBOHttpDocument::getInstance()->close(500, 'Missing request values'); } // ensure the booking exists $booking = VikBooking::getBookingInfoFromID($bid); if (!$booking) { VBOHttpDocument::getInstance()->close(404, 'Could not find the involved reservation'); } // get AI options for this booking $booking_ai_opts = (array) VBOFactory::getConfig()->getArray('ai_auto_guest_review_opt_' . $booking['id'], []); // update the requested options foreach ($opt as $param => $val) { if (is_bool($val) || is_numeric($val)) { $val = (int) $val; } // set new option value $booking_ai_opts[$param] = $val; } // update AI options for this booking VBOFactory::getConfig()->set('ai_auto_guest_review_opt_' . $booking['id'], $booking_ai_opts); // return the new preferences VBOHttpDocument::getInstance()->json($booking_ai_opts); } /** * AJAX endpoint to register a new taking (payment). * * @return void * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function add_taking() { if (!JSession::checkToken()) { VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN')); } $dbo = JFactory::getDbo(); $app = JFactory::getApplication(); $bid = $app->input->getInt('bid', 0); $amount = $app->input->getFloat('amount', 0); $payid = $app->input->getInt('payid', 0); $descr = $app->input->getString('descr', ''); if (!$bid || !$amount || $amount < 0) { VBOHttpDocument::getInstance()->close(500, 'Missing or invalid request values.'); } // ensure the booking exists $booking = VikBooking::getBookingInfoFromID($bid); if (!$booking) { VBOHttpDocument::getInstance()->close(404, 'Could not find the involved reservation.'); } $new_tot_paid = $booking['totpaid'] + $amount; // update booking record $dbo->setQuery( $dbo->getQuery(true) ->update($dbo->qn('#__vikbooking_orders')) ->set($dbo->qn('totpaid') . ' = ' . $dbo->q($new_tot_paid)) ->where($dbo->qn('id') . ' = ' . (int) $booking['id']) ); $dbo->execute(); // update booking history $extra_data = new stdClass; $extra_data->register_new = 1; $extra_data->amount_paid = $amount; $extra_data->payment_method = $descr; if (!empty($payid)) { $pay_info = VikBooking::getPayment($payid); if ($pay_info) { $extra_data->payment_method = $pay_info['name']; } } VikBooking::getBookingHistoryInstance($booking['id']) ->setExtraData($extra_data) ->store( 'PU', JText::sprintf('VBOPREVAMOUNTPAID', VikBooking::numberFormat($booking['totpaid']) . (!empty($extra_data->payment_method) ? ' (' . $extra_data->payment_method . ')' : '')) ); // process completed VBOHttpDocument::getInstance()->json([ 'url' => VBOFactory::getPlatform()->getUri()->admin('index.php?option=com_vikbooking&task=editorder&cid[]=' . $booking['id'], false), ]); } /** * AJAX endpoint to update a taking (payment) and related history record. * * @return void * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function update_taking() { if (!JSession::checkToken()) { VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN')); } $dbo = JFactory::getDbo(); $app = JFactory::getApplication(); $bid = $app->input->getInt('bid', 0); $hid = $app->input->getInt('hid', 0); $amount = $app->input->getFloat('amount', 0); $descr = $app->input->getString('descr', ''); $htype = $app->input->getString('htype', 'PU'); if (!$bid || !$hid || !$amount || $amount < 0) { VBOHttpDocument::getInstance()->close(500, 'Missing or invalid request values.'); } // ensure the booking exists $booking = VikBooking::getBookingInfoFromID($bid); if (!$booking) { VBOHttpDocument::getInstance()->close(404, 'Could not find the involved reservation.'); } // access the current history record $q = $dbo->getQuery(true) ->select('*') ->from($dbo->qn('#__vikbooking_orderhistory')) ->where($dbo->qn('id') . ' = ' . (int) $hid) ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id']) ->where($dbo->qn('type') . ' = ' . $dbo->q($htype)); $dbo->setQuery($q, 0, 1); $history = $dbo->loadAssoc(); if (!$history) { VBOHttpDocument::getInstance()->close(404, 'Could not find the history record to update.'); } // get previous amount paid $history_data = (object) json_decode(($history['data'] ?: '{}')); $prev_amount_paid = $history_data->amount_paid ?? 0; // calculate new amount paid if ($prev_amount_paid > $amount) { $new_tot_paid = $booking['totpaid'] - ($prev_amount_paid - $amount); } else { $new_tot_paid = $booking['totpaid'] + ($amount - $prev_amount_paid); } // update booking record $dbo->setQuery( $dbo->getQuery(true) ->update($dbo->qn('#__vikbooking_orders')) ->set($dbo->qn('totpaid') . ' = ' . $dbo->q($new_tot_paid)) ->where($dbo->qn('id') . ' = ' . (int) $booking['id']) ); $dbo->execute(); // get currently logged user $user = JFactory::getUser(); $uname = $user->name; // update history extra data $history_data->register_new = 1; $history_data->updated = JFactory::getDate()->toSql(); $history_data->updated_by = $uname; $history_data->amount_paid = $amount; $history_data->payment_method = $descr; // set new record description $new_descr = trim($history['descr'] . "\n* " . JText::sprintf('VBO_MODIFIED_ON_SMT', JFactory::getDate()->toSql(true) . ' (' . $uname . ')')); // update history record $dbo->setQuery( $dbo->getQuery(true) ->update($dbo->qn('#__vikbooking_orderhistory')) ->set($dbo->qn('descr') . ' = ' . $dbo->q($new_descr)) ->set($dbo->qn('totpaid') . ' = ' . $dbo->q($new_tot_paid)) ->set($dbo->qn('data') . ' = ' . $dbo->q(json_encode($history_data))) ->where($dbo->qn('id') . ' = ' . (int) $history['id']) ); $dbo->execute(); // process completed VBOHttpDocument::getInstance()->json([ 'url' => VBOFactory::getPlatform()->getUri()->admin('index.php?option=com_vikbooking&task=editorder&cid[]=' . $booking['id'], false), ]); } /** * AJAX endpoint to delete a booking history event. * * @return void * * @since 1.16.10 (J) - 1.6.10 (WP) */ public function delete_history_record() { if (!JSession::checkToken()) { VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN')); } $dbo = JFactory::getDbo(); $app = JFactory::getApplication(); $bid = $app->input->getInt('bid', 0); $hid = $app->input->getInt('hid', 0); $htype = $app->input->getString('htype', 'PU'); if (!$bid || !$hid) { VBOHttpDocument::getInstance()->close(500, 'Missing or invalid request values.'); } // ensure the booking exists $booking = VikBooking::getBookingInfoFromID($bid); if (!$booking) { VBOHttpDocument::getInstance()->close(404, 'Could not find the involved reservation.'); } // access the current history record $q = $dbo->getQuery(true) ->select('*') ->from($dbo->qn('#__vikbooking_orderhistory')) ->where($dbo->qn('id') . ' = ' . (int) $hid) ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id']) ->where($dbo->qn('type') . ' = ' . $dbo->q($htype)); $dbo->setQuery($q, 0, 1); $history = $dbo->loadAssoc(); if (!$history) { VBOHttpDocument::getInstance()->close(404, 'Could not find the history record to update.'); } // get previous amount paid $history_data = (object) json_decode(($history['data'] ?: '{}')); $prev_amount_paid = $history_data->amount_paid ?? 0; // calculate new amount paid $new_tot_paid = $booking['totpaid']; if ($prev_amount_paid) { $new_tot_paid = $booking['totpaid'] - $prev_amount_paid; } // update booking record $dbo->setQuery( $dbo->getQuery(true) ->update($dbo->qn('#__vikbooking_orders')) ->set($dbo->qn('totpaid') . ' = ' . $dbo->q($new_tot_paid)) ->where($dbo->qn('id') . ' = ' . (int) $booking['id']) ); $dbo->execute(); // delete history record $dbo->setQuery( $dbo->getQuery(true) ->delete($dbo->qn('#__vikbooking_orderhistory')) ->where($dbo->qn('id') . ' = ' . (int) $hid) ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id']) ->where($dbo->qn('type') . ' = ' . $dbo->q($htype)) ); $dbo->execute(); $aff_rows = $dbo->getAffectedRows(); // process completed VBOHttpDocument::getInstance()->json([ 'rows' => $aff_rows, 'url' => VBOFactory::getPlatform()->getUri()->admin('index.php?option=com_vikbooking&task=editorder&cid[]=' . $booking['id'], false), ]); } }