<?php
/**
 * @package     VikBooking
 * @subpackage  com_vikbooking
 * @author      Alessio Gaggii - e4j - Extensionsforjoomla.com
 * @copyright   Copyright (C) 2018 e4j - Extensionsforjoomla.com. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE
 * @link        https://vikwp.com
 */

defined('ABSPATH') or die('No script kiddies please!');

jimport('joomla.application.component.controller');

class VikBookingController extends JControllerVikBooking
{
	public function display($cachable = false, $urlparams = array())
	{
		$view = VikRequest::getVar('view', '');
		switch ($view) {
			case 'roomslist':
			case 'roomdetails':
			case 'searchdetails':
			case 'loginregister':
			case 'orderslist':
			case 'promotions':
			case 'availability':
			case 'packageslist':
			case 'packagedetails':
			case 'searchsuggestions':
			case 'booking':
			case 'operators':
			case 'tableaux':
			case 'precheckin':
			case 'revstay':
			case 'tinyurl':
				VikRequest::setVar('view', $view);
				break;
			default:
				VikRequest::setVar('view', 'vikbooking');
		}
		parent::display();
	}

	public function search()
	{
		VikRequest::setVar('view', 'search');
		parent::display();
	}

	public function showprc()
	{
		VikRequest::setVar('view', 'showprc');
		parent::display();
	}

	public function oconfirm()
	{
		$requirelogin = VikBooking::requireLogin();
		if($requirelogin) {
			if(VikBooking::userIsLogged()) {
				VikRequest::setVar('view', 'oconfirm');
			} else {
				VikRequest::setVar('view', 'loginregister');
			}
		} else {
			VikRequest::setVar('view', 'oconfirm');
		}
		parent::display();
	}
	
	public function register()
	{
		$mainframe = JFactory::getApplication();
		$dbo = JFactory::getDBO();

		//user data
		$pname = VikRequest::getString('fname', '', 'request');
		$plname = VikRequest::getString('lname', '', 'request');
		$pemail = VikRequest::getString('email', '', 'request');
		$pusername = VikRequest::getString('username', '', 'request');
		$ppassword = VikRequest::getString('password', '', 'request');
		$pconfpassword = VikRequest::getString('confpassword', '', 'request');
		//
		//order data
		$pitemid = VikRequest::getString('Itemid', '', 'request');
		$proomid = VikRequest::getVar('roomid', array());
		$pdays = VikRequest::getInt('days', '', 'request');
		$pcheckin = VikRequest::getInt('checkin', '', 'request');
		$pcheckout = VikRequest::getInt('checkout', '', 'request');
		$proomsnum = VikRequest::getInt('roomsnum', '', 'request');
		$padults = VikRequest::getVar('adults', array());
		$pchildren = VikRequest::getVar('children', array());
		$rooms = array();
		$arrpeople = array();
		for($ir = 1; $ir <= $proomsnum; $ir++) {
			$ind = $ir - 1;
			if (!empty($proomid[$ind])) {
				$q = "SELECT * FROM `#__vikbooking_rooms` WHERE `id`='".intval($proomid[$ind])."' AND `avail`='1';";
				$dbo->setQuery($q);
				$dbo->execute();
				if ($dbo->getNumRows() > 0) {
					$takeroom = $dbo->loadAssocList();
					$rooms[$ir] = $takeroom[0];
				}
			}
			if (!empty($padults[$ind])) {
				$arrpeople[$ir]['adults'] = intval($padults[$ind]);
			} else {
				$arrpeople[$ir]['adults'] = 0;
			}
			if (!empty($pchildren[$ind])) {
				$arrpeople[$ir]['children'] = intval($pchildren[$ind]);
			} else {
				$arrpeople[$ir]['children'] = 0;
			}
		}
		$prices = array();
		foreach($rooms as $num => $r) {
			$ppriceid = VikRequest::getString('priceid'.$num, '', 'request');
			if (!empty($ppriceid)) {
				$prices[$num] = intval($ppriceid);
			}
		}
		$selopt = array();
		$q = "SELECT * FROM `#__vikbooking_optionals` ORDER BY `#__vikbooking_optionals`.`ordering` ASC;";
		$dbo->setQuery($q);
		$dbo->execute();
		if ($dbo->getNumRows() > 0) {
			$optionals = $dbo->loadAssocList();
			foreach ($rooms as $num => $r) {
				foreach ($optionals as $opt) {
					if (!empty($opt['ageintervals']) && $arrpeople[$num]['children'] > 0) {
						$tmpvar = VikRequest::getInt('optid'.$num.$opt['id'], []);
						if (is_array($tmpvar) && $tmpvar) {
							$optagenames = VikBooking::getOptionIntervalsAges($opt['ageintervals']);
							$optagepcent = VikBooking::getOptionIntervalsPercentage($opt['ageintervals']);
							$optageovrct = VikBooking::getOptionIntervalChildOverrides($opt, $arrpeople[$num]['adults'], $arrpeople[$num]['children']);
							$optorigname = $opt['name'];
							foreach ($tmpvar as $child_num => $chvar) {
								$opt['quan'] = $chvar;
								$opt['chageintv'] = $chvar;
								//ignore calculation as percetage value to reconstruct the URL
								$ageintervals_child_string = isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $opt['ageintervals'];
								$optagecosts = VikBooking::getOptionIntervalsCosts($ageintervals_child_string);
								$opt['cost'] = $optagecosts[($chvar - 1)];
								$opt['name'] = $optorigname.' ('.$optagenames[($chvar - 1)].')';
								$selopt[$num][] = $opt;
							}
						}
					} else {
						$tmpvar = VikRequest::getString('optid'.$num.$opt['id'], '', 'request');
						if (!empty($tmpvar)) {
							$opt['quan'] = $tmpvar;
							$selopt[$num][] = $opt;
						}
					}
				}
			}
		}
		$strpriceid = "";
		foreach($prices as $num => $pid) {
			$strpriceid .= ($num > 1 ? "&" : "")."priceid".$num."=".$pid;
		}
		$stroptid = "";
		for($ir = 1; $ir <= $proomsnum; $ir++) {
			if (isset($selopt[$ir]) && is_array($selopt[$ir])) {
				foreach($selopt[$ir] as $opt) {
					if (array_key_exists('chageintv', $opt)) {
						$stroptid .= "&optid".$ir.$opt['id']."[]=".$opt['chageintv'];
					} else {
						$stroptid .= "&optid".$ir.$opt['id']."=".$opt['quan'];
					}
				}
			}
		}
		$strroomid = "";
		foreach ($rooms as $num => $r) {
			$strroomid .= "&roomid[]=".$r['id'];
		}
		$straduchild = "";
		foreach ($arrpeople as $indroom => $aduch) {
			$straduchild .= "&adults[]=".$aduch['adults'];
			$straduchild .= "&children[]=".$aduch['children'];
		}
		
		$qstring = $strpriceid.$stroptid.$strroomid.$straduchild."&roomsnum=".$proomsnum."&days=".$pdays."&checkin=".$pcheckin."&checkout=".$pcheckout.(!empty($pitemid) ? "&Itemid=".$pitemid : "");
		//
		if (!VikBooking::userIsLogged()) {
			if (!empty($pname) && !empty($plname) && !empty($pusername) && !empty($pemail) && $ppassword == $pconfpassword) {
				//save user
				$newuserid=VikBooking::addJoomlaUser($pname." ".$plname, $pusername, $pemail, $ppassword);

				if ($newuserid!=false && strlen($newuserid)) {

					/**
					 * @wponly 	the return URL should be passed within the $option array of $app->login()
					 */
					$redirect_to = JRoute::rewrite('index.php?option=com_vikbooking&task=oconfirm&'.$qstring, false);

					//registration success
					$credentials = array('username' => $pusername, 'password' => $ppassword );
					//autologin
					$mainframe->login($credentials, array('redirect' => $redirect_to));
					$currentUser = JFactory::getUser();
					$currentUser->setLastVisit(time());
					$currentUser->set('guest', 0);
					//
					$mainframe->redirect($redirect_to);
				} else {
					//error while saving new user
					VikError::raiseWarning('', JText::translate('VBREGERRSAVING'));
					$mainframe->redirect(JRoute::rewrite('index.php?option=com_vikbooking&view=loginregister&'.$qstring, false));
				}
			} else {
				//invalid data
				VikError::raiseWarning('', JText::translate('VBREGERRINSDATA'));
				$mainframe->redirect(JRoute::rewrite('index.php?option=com_vikbooking&view=loginregister&'.$qstring, false));
			}
		} else {
			//user is already logged in, proceed
			$mainframe->redirect(JRoute::rewrite('index.php?option=com_vikbooking&task=oconfirm&'.$qstring, false));
		}
	}

	public function saveorder()
	{
		$dbo = JFactory::getDbo();
		$session = JFactory::getSession();
		$app = JFactory::getApplication();
		$vbo_tn = VikBooking::getTranslator();

		// availability helper
		$av_helper = VikBooking::getAvailabilityInstance();

		$prooms = VikRequest::getVar('rooms', array());
		$proomindex = VikRequest::getVar('roomindex', array());
		$proomsnum = VikRequest::getInt('roomsnum', 0, 'request');
		$padults = VikRequest::getVar('adults', array());
		$pchildren = VikRequest::getVar('children', array());
		$pdays = VikRequest::getInt('days', 0, 'request');
		$pcouponcode = VikRequest::getString('couponcode', '', 'request');
		$pcheckin = VikRequest::getInt('checkin', 0, 'request');
		$pcheckout = VikRequest::getInt('checkout', 0, 'request');
		$pprtar = VikRequest::getVar('prtar', array());
		$ppriceid = VikRequest::getVar('priceid', array());
		$poptionals = VikRequest::getString('optionals', '', 'request');
		$ptotdue = VikRequest::getString('totdue', '', 'request');
		$pgpayid = VikRequest::getString('gpayid', '', 'request');
		$ppkg_id = VikRequest::getInt('pkg_id', '', 'request');
		$pnodep = VikRequest::getInt('nodep', '', 'request');
		$split_stay = VikRequest::getVar('split_stay', array());
		$pitemid = VikRequest::getInt('Itemid', '', 'request');

		$validtoken = true;
		if (VikBooking::tokenForm()) {
			$validtoken = false;
			$pviktoken = VikRequest::getString('viktoken', '', 'request');
			$sessvbtkn = $session->get('vikbtoken', '');
			if (!empty($pviktoken) && $sessvbtkn == $pviktoken) {
				$session->set('vikbtoken', '');
				$validtoken = true;
			}
			if (!$validtoken) {
				$validtoken = JSession::checkToken();
			}
		}

		if (!$validtoken) {
			showSelectVb(JText::translate('VBINVALIDTOKEN'));
			return;
		}

		$q = "SELECT * FROM `#__vikbooking_custfields` ORDER BY `#__vikbooking_custfields`.`ordering` ASC;";
		$dbo->setQuery($q);
		$cfields = $dbo->loadAssocList();

		$suffdata = true;
		$useremail = "";
		$usercountry = '';
		$nominatives = [];
		$t_first_name = '';
		$t_last_name = '';
		$phone_number = '';
		$fieldflags = [];
		if ($cfields) {
			$vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
			foreach ($cfields as $cf) {
				if (intval($cf['required']) == 1 && $cf['type'] != 'separator' && $cf['type'] != 'state') {
					$tmpcfval = VikRequest::getString('vbf' . $cf['id'], '', 'request');
					if (!strlen(str_replace(' ', '', trim($tmpcfval)))) {
						$suffdata = false;
						break;
					}
				}
			}
			//save user email, nominatives, phone number and create custdata array
			$arrcustdata = [];
			$arrcfields = [];
			$emailwasfound = false;
			foreach ($cfields as $cf) {
				$user_inp_val = VikRequest::getString('vbf' . $cf['id'], '', 'request');
				if (intval($cf['isemail']) == 1 && $emailwasfound == false) {
					$useremail = trim($user_inp_val);
					$emailwasfound = true;
				}
				if ($cf['isnominative'] == 1) {
					if (strlen(str_replace(' ', '', trim($user_inp_val)))) {
						$nominatives[] = $user_inp_val;
					}
				}
				if ($cf['isphone'] == 1) {
					if (strlen(str_replace(' ', '', trim($user_inp_val)))) {
						$phone_number = $user_inp_val;
					}
				}
				if (!empty($cf['flag'])) {
					if (strlen(str_replace(' ', '', trim($user_inp_val)))) {
						$fieldflags[$cf['flag']] = $user_inp_val;
					}
				}
				if ($cf['type'] != 'separator' && $cf['type'] != 'country' && ( $cf['type'] != 'checkbox' || ($cf['type'] == 'checkbox' && intval($cf['required']) != 1) ) ) {
					// check the input value to store for the customer raw information string
					$def_user_inp_val = $user_inp_val;
					// check for state/province field
					if ($cf['type'] == 'state' && strlen(str_replace(' ', '', trim($user_inp_val)))) {
						/**
						 * In order to assign the proper state/province to the customer,
						 * we treat this type of field as if it was a "field flag" type.
						 * 
						 * @since 	1.16.0 (J) - 1.6.0 (WP)
						 */
						$fieldflags['state'] = $user_inp_val;

						// attempt to save the full state name, not the 2-char code
						$def_user_inp_val = VBOStateHelper::getFullName($user_inp_val, $usercountry);
					}
					$arrcustdata[JText::translate($cf['name'])] = $def_user_inp_val;

					// store the original input value for this custom field ID
					$arrcfields[$cf['id']] = $user_inp_val;
				} elseif ($cf['type'] == 'country') {
					$countryval = $user_inp_val;
					if (!empty($countryval) && strstr($countryval, '::') !== false) {
						$countryparts = explode('::', $countryval);
						$usercountry = $countryparts[0];
						$arrcustdata[JText::translate($cf['name'])] = $countryparts[1];
					} else {
						$arrcustdata[JText::translate($cf['name'])] = '';
					}
				}
			}
		}
		if (!empty($phone_number) && !empty($usercountry)) {
			$phone_number = VikBooking::checkPhonePrefixCountry($phone_number, $usercountry);
		}

		if ($suffdata !== true) {
			showSelectVb(JText::translate('VBINSUFDATA'));
			return;
		}

		if (count($nominatives) >= 2) {
			$t_last_name = array_pop($nominatives);
			$t_first_name = array_pop($nominatives);
		}

		$secdiff = $pcheckout - $pcheckin;
		$daysdiff = $secdiff / 86400;
		if (is_int($daysdiff)) {
			if ($daysdiff < 1) {
				$daysdiff = 1;
			}
		} else {
			if ($daysdiff < 1) {
				$daysdiff = 1;
			} else {
				$sum = floor($daysdiff) * 86400;
				$newdiff = $secdiff - $sum;
				$maxhmore = VikBooking::getHoursMoreRb() * 3600;
				if ($maxhmore >= $newdiff) {
					$daysdiff = floor($daysdiff);
				} else {
					$daysdiff = ceil($daysdiff);
				}
			}
		}

		if (!VikBooking::dayValidTs($pdays, $pcheckin, $pcheckout) || $pdays != $daysdiff) {
			showSelectVb(JText::translate('VBINCONGRDATA'));
			return;
		}

		// get check-in and check-out dates information
		$checkin_info  = getdate($pcheckin);
		$checkout_info = getdate($pcheckout);

		/**
		 * Check split stay information.
		 * 
		 * @since 	1.16.0 (J) - 1.6.0 (WP)
		 */
		if (!empty($split_stay) && count($split_stay) == count($prooms) && count($split_stay) == $proomsnum && $proomsnum > 1) {
			// valid split stay request vars received
			$split_stay_checkins  = [];
			$split_stay_checkouts = [];
			$split_stay_nights 	  = [];
			foreach ($split_stay as $sps_k => $split_room) {
				// calculate and set the exact check-in and check-out timestamps for this split-room
				$room_checkin  = VikBooking::getDateTimestamp($split_room['checkin'], $checkin_info['hours'], $checkin_info['minutes'], $checkin_info['seconds']);
				$room_checkout = VikBooking::getDateTimestamp($split_room['checkout'], $checkout_info['hours'], $checkout_info['minutes'], $checkout_info['seconds']);
				$split_stay_checkins[]  = $room_checkin;
				$split_stay_checkouts[] = $room_checkout;
				// update split stay information
				$split_room['checkin_ts']  = $room_checkin;
				$split_room['checkout_ts'] = $room_checkout;
				$split_room['nights'] 	   = $av_helper->countNightsOfStay($room_checkin, $room_checkout);
				$split_stay_nights[] 	   = $split_room['nights'];
				$split_stay[$sps_k] 	   = $split_room;
			}
			// validate minimum and maximum stay dates for the split stay
			if (empty($split_stay_checkins) || empty($split_stay_checkouts)) {
				// error
				showSelectVb('Empty stay dates for split stay rooms');
				return;
			}
			if (array_sum($split_stay_nights) != $daysdiff) {
				showSelectVb('Invalid sum of total nights for split stay rooms');
				return;
			}
			if (min($split_stay_checkins) != $pcheckin) {
				// error
				showSelectVb('Invalid checkin stay date for split stay rooms');
				return;
			}
			if (max($split_stay_checkouts) != $pcheckout) {
				// error
				showSelectVb('Invalid checkout stay date for split stay rooms');
				return;
			}
		} else {
			// unset any possible value as it's invalid
			$split_stay = [];
		}

		$currencyname = VikBooking::getCurrencyName();
		$rooms = [];
		$prices = [];
		$arrpeople = [];
		for ($ir = 1; $ir <= $proomsnum; $ir++) {
			$ind = $ir - 1;
			if (!empty($prooms[$ind])) {
				$q = "SELECT * FROM `#__vikbooking_rooms` WHERE `id`=" . (int)$prooms[$ind] . " AND `avail`='1';";
				$dbo->setQuery($q);
				$rdata = $dbo->loadAssoc();
				if ($rdata) {
					$rooms[$ir] = $rdata;
				}
			}
			if (!empty($padults[$ind])) {
				$arrpeople[$ir]['adults'] = intval($padults[$ind]);
			} else {
				$arrpeople[$ir]['adults'] = 0;
			}
			if (!empty($pchildren[$ind])) {
				$arrpeople[$ir]['children'] = intval($pchildren[$ind]);
			} else {
				$arrpeople[$ir]['children'] = 0;
			}
			$arrpeople[$ir]['pets'] = 0;
			$prices[$ir] = intval($ppriceid[$ind]);
		}
		if (count($rooms) != $proomsnum) {
			VikError::raiseWarning('', JText::translate('VBROOMNOTFND'));
			$app->redirect(JRoute::rewrite('index.php?option=com_vikbooking'));
			exit;
		}
		$vbo_tn->translateContents($rooms, '#__vikbooking_rooms');

		// package
		$pkg = [];
		if (!empty($ppkg_id)) {
			$pkg = VikBooking::validateRoomPackage($ppkg_id, $rooms, $daysdiff, $pcheckin, $pcheckout);
			if (!is_array($pkg) || (is_array($pkg) && !(count($pkg) > 0)) ) {
				if (!is_array($pkg)) {
					VikError::raiseWarning('', $pkg);
				}
				$app->redirect(JRoute::rewrite("index.php?option=com_vikbooking&view=packagedetails&pkgid=".$ppkg_id.(!empty($pitemid) ? "&Itemid=".$pitemid : ""), false));
				exit;
			}
		}

		$tars = [];
		$validfares = true;
		foreach ($rooms as $num => $r) {
			if (count($pkg)) {
				break;
			}

			// determine the number of nights of stay and dates to consider
			$use_los = (int)$daysdiff;
			$room_checkin  = $pcheckin;
			$room_checkout = $pcheckout;
			if (!empty($split_stay) && !empty($split_stay[($num - 1)]) && $split_stay[($num - 1)]['idroom'] == $r['id']) {
				$use_los = (int)$split_stay[($num - 1)]['nights'];
				$room_checkin  = $split_stay[($num - 1)]['checkin_ts'];
				$room_checkout = $split_stay[($num - 1)]['checkout_ts'];
			}

			$q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `idroom`=" . (int)$r['id'] . " AND `days`=" . $use_los . " AND `idprice`=" . $prices[$num];
			$dbo->setQuery($q, 0, 1);
			$dbo->execute();
			if (!$dbo->getNumRows()) {
				$validfares = false;
				break;
			}
			$tar = $dbo->loadAssocList();

			// apply seasonal rates
			$tar = VikBooking::applySeasonsRoom($tar, $room_checkin, $room_checkout);

			// apply OBP rules
			$tar = VBORoomHelper::getInstance()->applyOBPRules($tar, $r, $arrpeople[$num]['adults']);

			// push room tariffs
			$tars[$num] = $tar;
		}

		if ($validfares !== true) {
			showSelectVb(JText::translate('VBINCONGRDATAREC'));
			return;
		}

		$isdue = 0;
		$tot_taxes = 0;
		$tot_city_taxes = 0;
		$tot_fees = 0;
		$tot_damage_dep = 0;
		$rooms_costs_map = [];
		$is_package = (bool)(count($pkg) > 0);
		if ($is_package === true) {
			foreach ($rooms as $num => $r) {
				$pkg_cost = $pkg['pernight_total'] == 1 ? ($pkg['cost'] * $daysdiff) : $pkg['cost'];
				$pkg_cost = $pkg['perperson'] == 1 ? ($pkg_cost * ($arrpeople[$num]['adults'] > 0 ? $arrpeople[$num]['adults'] : 1)) : $pkg_cost;
				$cost_plus_tax = VikBooking::sayPackagePlusIva($pkg_cost, $pkg['idiva']);
				$isdue += $cost_plus_tax;
				if ($cost_plus_tax == $pkg_cost) {
					$cost_minus_tax = VikBooking::sayPackageMinusIva($pkg_cost, $pkg['idiva']);
					$tot_taxes += ($pkg_cost - $cost_minus_tax);
				} else {
					$tot_taxes += ($cost_plus_tax - $pkg_cost);
				}
			}
		} else {
			foreach ($tars as $num => $tar) {
				$cost_plus_tax = VikBooking::sayCostPlusIva($tar[0]['cost'], $tar[0]['idprice']);
				$isdue += $cost_plus_tax;
				if ($cost_plus_tax == $tar[0]['cost']) {
					$cost_minus_tax = VikBooking::sayCostMinusIva($tar[0]['cost'], $tar[0]['idprice']);
					$tot_taxes += ($tar[0]['cost'] - $cost_minus_tax);
				} else {
					$tot_taxes += ($cost_plus_tax - $tar[0]['cost']);
				}
				$rooms_costs_map[$num] = $tar[0]['cost'];
			}
		}

		/**
		 * Custom check-in/out times due to late check-out/early check-in options or custom listing settings.
		 * 
		 * @since 	1.17.2 (J) - 1.7.2 (WP)
		 * @since 	1.18.3 (J) - 1.8.3 (WP) added support to check-in/out times at listing-level.
		 */
		$custom_checkinout = [];
		if (count($rooms) === 1) {
			$listing_custom_checkin = VikBooking::getRoomParam('checkin', ($rooms[(key($rooms))]['params'] ?? ''));
			$listing_custom_checkout = VikBooking::getRoomParam('checkout', ($rooms[(key($rooms))]['params'] ?? ''));
			if ($listing_custom_checkin) {
				// set listing-level check-in time in seconds
				$listing_custom_checkin_parts = explode(':', $listing_custom_checkin);
				$listing_custom_checkin = (intval($listing_custom_checkin_parts[0]) * 3600) + (intval($listing_custom_checkin_parts[1]) * 60);
			}
			if ($listing_custom_checkout) {
				// set listing-level check-out time in seconds
				$listing_custom_checkout_parts = explode(':', $listing_custom_checkout);
				$listing_custom_checkout = (intval($listing_custom_checkout_parts[0]) * 3600) + (intval($listing_custom_checkout_parts[1]) * 60);
			}
			if ($listing_custom_checkin || $listing_custom_checkout) {
				// set listing-level check-in/out times in seconds
				$custom_checkinout = [
					(int) $listing_custom_checkin,
					(int) $listing_custom_checkout,
				];
			}
		}

		$selopt = [];
		$optstr = [];
		$children_age = [];
		if (!empty($poptionals)) {
			$stepo = explode(";", $poptionals);
			foreach ($stepo as $roptkey => $oo) {
				if (empty($oo)) {
					continue;
				}
				$stept = explode(":", $oo);
				$rnoid = explode("_", $stept[0]);
				$room_ind = $rnoid[0] - 1;

				$q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . (int)$rnoid[1];
				$dbo->setQuery($q, 0, 1);
				$actopt = $dbo->loadAssocList();
				if (!$actopt) {
					continue;
				}
				$vbo_tn->translateContents($actopt, '#__vikbooking_optionals');

				// option params
				$opt_params = !empty($actopt[0]['oparams']) ? json_decode($actopt[0]['oparams'], true) : [];
				$opt_params = is_array($opt_params) ? $opt_params : [];

				// determine the number of nights of stay and dates to consider
				$use_los = (int)$daysdiff;
				$room_checkin  = $pcheckin;
				$room_checkout = $pcheckout;
				if (!empty($split_stay) && !empty($split_stay[$room_ind])) {
					$use_los = (int)$split_stay[$room_ind]['nights'];
					$room_checkin  = $split_stay[$room_ind]['checkin_ts'];
					$room_checkout = $split_stay[$room_ind]['checkout_ts'];
				}

				$chvar = '';
				if (!empty($actopt[0]['ageintervals']) && $arrpeople[$rnoid[0]]['children'] > 0 && strstr($stept[1], '-') != false) {
					$optagenames = VikBooking::getOptionIntervalsAges($actopt[0]['ageintervals']);
					$optagepcent = VikBooking::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
					$optageovrct = VikBooking::getOptionIntervalChildOverrides($actopt[0], $arrpeople[$rnoid[0]]['adults'], $arrpeople[$rnoid[0]]['children']);
					$child_num 	 = VikBooking::getRoomOptionChildNumber($poptionals, $actopt[0]['id'], $roptkey, $arrpeople[$rnoid[0]]['children']);
					$optagecosts = VikBooking::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
					$agestept = explode('-', $stept[1]);
					$stept[1] = $agestept[0];
					$chvar = $agestept[1];
					if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
						//percentage value of the adults tariff
						if ($is_package === true) {
							$optagecosts[($chvar - 1)] = ($pkg['pernight_total'] == 1 ? ($pkg['cost'] * $daysdiff) : $pkg['cost']) * $optagecosts[($chvar - 1)] / 100;
						} else {
							$optagecosts[($chvar - 1)] = $tars[$rnoid[0]][0]['cost'] * $optagecosts[($chvar - 1)] / 100;
						}
					} elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
						//VBO 1.10 - percentage value of room base cost
						if ($is_package === true) {
							$optagecosts[($chvar - 1)] = ($pkg['pernight_total'] == 1 ? ($pkg['cost'] * $daysdiff) : $pkg['cost']) * $optagecosts[($chvar - 1)] / 100;
						} else {
							$display_rate = isset($tars[$rnoid[0]][0]['room_base_cost']) ? $tars[$rnoid[0]][0]['room_base_cost'] : $tars[$rnoid[0]][0]['cost'];
							$optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
						}
					}
					$actopt[0]['chageintv'] = $chvar;
					$actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
					$actopt[0]['quan'] = $stept[1];
					$selopt[$rnoid[0]][] = $actopt[0];
					$selopt['room'.$rnoid[0]] = $selopt['room'.$rnoid[0]].$actopt[0]['id'].":".$stept[1]."-".$chvar.";";
					$realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $use_los * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
					$children_age[$rnoid[0]][] = array('ageinterval' => $optagenames[($chvar - 1)], 'age' => '', 'cost' => $realcost);
				} else {
					$actopt[0]['quan'] = $stept[1];
					// VBO 1.11 - options percentage cost of the room total fee
					if ($is_package === true) {
						$deftar_basecosts = $pkg['pernight_total'] == 1 ? ($pkg['cost'] * $daysdiff) : $pkg['cost'];
					} else {
						$deftar_basecosts = $tars[$rnoid[0]][0]['cost'];
					}
					$actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
					//
					$selopt[$rnoid[0]][] = $actopt[0];
					if (!isset($selopt['room'.$rnoid[0]])) {
						$selopt['room'.$rnoid[0]] = '';
					}
					$selopt['room'.$rnoid[0]] .= $actopt[0]['id'] . ":" . $stept[1] . ";";
					$realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $use_los * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
				}
				if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
					$realcost = $actopt[0]['maxprice'];
					if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
						$realcost = $actopt[0]['maxprice'] * $stept[1];
					}
				}

				/**
				 * Count pets, if any.
				 * 
				 * @since 	1.16.2 (J) - 1.6.2 (WP)
				 */
				if ($opt_params['pet_fee'] ?? 0) {
					$tot_pets = 1;
					if ($actopt[0]['hmany'] > 0 && $stept[1] > 1) {
						$tot_pets = (int)$stept[1];
					}
					$arrpeople[$rnoid[0]]['pets'] = $tot_pets;
				}

				/**
				 * Custom check-in/out times due to late check-out/early check-in options.
				 * 
				 * @since 	1.17.2 (J) - 1.7.2 (WP)
				 */
				if (($opt_params['custom_checkinout'] ?? 0) && (($opt_params['set_checkin'] ?? 0) || ($opt_params['set_checkout'] ?? 0))) {
					$custom_checkinout = [
						($opt_params['set_checkin'] ?? 0),
						($opt_params['set_checkout'] ?? 0),
					];
				}

				$realcost = ($actopt[0]['perperson'] == 1 ? ($realcost * $arrpeople[$rnoid[0]]['adults']) : $realcost);

				/**
				 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
				 * 
				 * @since 	1.17.7 (J) - 1.7.7 (WP)
				 */
				$use_room_cost = $is_package === true ? $pkg['cost'] : ($tars[$rnoid[0]][0]['cost'] ?? 0);
				$custom_calc_booking = ['days' => $use_los];
				$custom_calc_booking_room = array_merge($arrpeople[$rnoid[0]], ['room_cost' => $use_room_cost]);
				$custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$realcost, &$actopt[0], $custom_calc_booking, $custom_calc_booking_room]);
				if ($custom_calculation) {
					$realcost = (float) $custom_calculation[0];
				}

				$opt_minus_iva = VikBooking::sayOptionalsMinusIva($realcost, $actopt[0]['idiva']);
				$tmpopr = VikBooking::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
				if ($actopt[0]['is_citytax'] == 1) {
					$tot_city_taxes += $opt_minus_iva;
				} elseif ($actopt[0]['is_fee'] == 1) {
					$tot_fees += $opt_minus_iva;
				} elseif ($opt_params['damagedep'] ?? 0) {
					$tot_damage_dep += $opt_minus_iva;
				}
				// VBO 1.11 - always calculate the amount of tax no matter if this is already a tax or a fee
				if ($tmpopr == $realcost) {
					$tot_taxes += ($realcost - $opt_minus_iva);
				} else {
					$tot_taxes += ($tmpopr - $realcost);
				}
				//
				$isdue += $tmpopr;
				$optstr[$rnoid[0]][] = ($stept[1] > 1 ? $stept[1] . " " : "") . $actopt[0]['name'] . ": " . $tmpopr . " " . $currencyname . "\n";
			}
		}

		$origtotdue = $isdue;
		$usedcoupon = false;
		$strcouponeff = '';

		// access current customer
		$cpin = VikBooking::getCPinIstance();
		$customer_details = $cpin->loadCustomerDetails();

		// coupon
		if (strlen($pcouponcode) && $is_package !== true) {
			$coupon = VikBooking::getCouponInfo($pcouponcode);
			$valid_customer_coupon = true;
			if (!empty($coupon) && !empty($coupon['customers'])) {
				if (empty($customer_details['id']) || !in_array($customer_details['id'], $coupon['customers'])) {
					$valid_customer_coupon = false;
				}
			}
			if (!empty($coupon) && $valid_customer_coupon) {
				$coupondateok = true;
				if (strlen((string)$coupon['datevalid'])) {
					$dateparts = explode("-", $coupon['datevalid']);
					$pickinfo = $checkin_info;
					$dropinfo = $checkout_info;
					$checkpick = mktime(0, 0, 0, $pickinfo['mon'], $pickinfo['mday'], $pickinfo['year']);
					$checkdrop = mktime(0, 0, 0, $dropinfo['mon'], $dropinfo['mday'], $dropinfo['year']);
					if (!($checkpick >= $dateparts[0] && $checkpick <= $dateparts[1] && $checkdrop >= $dateparts[0] && $checkdrop <= $dateparts[1])) {
						$coupondateok = false;
					}
				}
				if (!empty($coupon['minlos']) && $coupon['minlos'] > $daysdiff) {
					$coupondateok = false;
				}
				if ($coupondateok) {
					$couponroomok = true;
					if (!$coupon['allvehicles']) {
						foreach ($rooms as $num => $r) {
							if (!(preg_match("/;".$r['id'].";/i", $coupon['idrooms']))) {
								$couponroomok = false;
								break;
							}
						}
					}
					if ($couponroomok) {
						$coupontotok = true;
						if (strlen((string)$coupon['mintotord'])) {
							if ($isdue < $coupon['mintotord']) {
								$coupontotok = false;
							}
						}
						if ($coupon['maxtotord'] > 0 && $isdue > $coupon['maxtotord']) {
							$coupontotok = false;
						}

						/**
						 * Trigger event to allow third-party plugins to implement additional coupon validations or manipulation.
						 * 
						 * @since 	1.16.7 (J) - 1.6.7 (WP)
						 */
						if ($coupontotok) {
							$coupon_validation = VBOFactory::getPlatform()->getDispatcher()->filter('onValidateCouponCode', [&$coupon]);
							if (is_array($coupon_validation) && in_array(false, $coupon_validation, true)) {
								$coupontotok = false;
							}
						}

						if ($coupontotok) {
							$usedcoupon = true;
							if ($coupon['percentot'] == 1) {
								// percent value
								$minuscoupon = 100 - $coupon['value'];
								/**
								 * We allow coupon codes to be applied on the entire reservation or as always just on the total minus mandatory taxes.
								 * 
								 * @since 	1.13.5 (J) - 1.3.5 (WP)
								 * @since 	1.14.3 (J) - 1.4.3 (WP) we also exclude the amount of taxes beside the mandatory fees.
								 * @since 	1.16.0 (J) - 1.6.0 (WP) taxes are proportionally calculated when coupon before tax.
								 * @since 	1.16.8 (J) - 1.6.8 (WP) with coupon before taxes, discounted amount calculation is an equal subtration.
								 */
								$prev_isdue = $isdue;
								$tot_net = ($isdue - $tot_taxes - $tot_city_taxes - $tot_fees - $tot_damage_dep);
								$coupondiscount = ($coupon['excludetaxes'] ? $tot_net : $isdue) * $coupon['value'] / 100;
								$isdue = ($coupon['excludetaxes'] ? $tot_net : $isdue) * $minuscoupon / 100;
								$tot_taxes = $coupon['excludetaxes'] ? ($tot_taxes * ($tot_net - $coupondiscount) / $tot_net) : $tot_taxes;
								$isdue += $coupon['excludetaxes'] ? ($tot_taxes + $tot_city_taxes + $tot_fees + $tot_damage_dep) : 0;
								$coupondiscount = abs($prev_isdue - $isdue);
							} else {
								// total value
								$coupondiscount = $coupon['value'];
								// isdue : taxes = coupon_discount : x
								$tax_prop = $tot_taxes * $coupon['value'] / $isdue;
								$tot_taxes -= $tax_prop;
								$tot_taxes = $tot_taxes < 0 ? 0 : $tot_taxes;
								$isdue -= $coupon['value'];
								$isdue = $isdue < 0 ? 0 : $isdue;
							}
							$strcouponeff = $coupon['id'].';'.$coupondiscount.';'.$coupon['code'];
						}
					}
				}
			}
		}

		$strisdue = number_format($isdue, 2) . 'vikbooking';
		$ptotdue = number_format($ptotdue, 2) . 'vikbooking';
		if ($strisdue != $ptotdue && abs(round($isdue, 2) - round((float) $ptotdue, 2)) <= 0.01) {
			showSelectVb(JText::translate('VBINCONGRTOT'));
			return;
		}

		// pay full amount cookie (2 weeks)
		$nodep_set = !empty($pnodep) ? '1' : '0';
		$nodep_time_set = !empty($pnodep) ? (time() + (86400 * 14)) : (time() - (86400 * 14));
		$cookie = JFactory::getApplication()->input->cookie;
		VikRequest::setCookie('vboFA', $nodep_set, $nodep_time_set, '/');

		// modify booking
		$mod_booking = [];
		$skip_busy_ids = [];
		$cur_mod = $session->get('vboModBooking', '');
		if (is_array($cur_mod) && $cur_mod) {
			$mod_booking = $cur_mod;
			$skip_busy_ids = VikBooking::loadBookingBusyIds($mod_booking['id']);
		}

		$nowts = time();
		$checkts = $nowts;
		$today_bookings = VikBooking::todayBookings();
		if ($today_bookings) {
			$checkts = mktime(0, 0, 0, date('n'), date('j'), date('Y'));
		}
		if (!($checkts <= $pcheckin && $checkts < $pcheckout && $pcheckin < $pcheckout)) {
			showSelectVb(JText::translate('VBINVALIDDATES'));
			return;
		}

		$roomsavailable = true;
		foreach ($rooms as $num => $r) {
			// determine the number of nights of stay and dates to consider
			$use_los = (int)$daysdiff;
			$room_checkin  = $pcheckin;
			$room_checkout = $pcheckout;
			if (!empty($split_stay) && !empty($split_stay[($num - 1)]) && $split_stay[($num - 1)]['idroom'] == $r['id']) {
				$use_los = (int)$split_stay[($num - 1)]['nights'];
				$room_checkin  = $split_stay[($num - 1)]['checkin_ts'];
				$room_checkout = $split_stay[($num - 1)]['checkout_ts'];
			}

			if (!VikBooking::roomNotLocked($r['id'], $r['units'], $room_checkin, $room_checkout, true, $skip_busy_ids)) {
				$roomsavailable = false;
				break;
			}
		}
		if ($roomsavailable !== true) {
			showSelectVb(JText::translate('VBROOMBOOKEDBYOTHER'));
			return;
		}

		// save in session the checkin and checkout time of the reservation made
		$session->set('vikbooking_order_checkin', $pcheckin);
		$session->set('vikbooking_order_checkout', $pcheckout);

		// handle booking sid and customer information summary string
		$sid = $mod_booking ? $mod_booking['sid'] : VikBooking::getSecretLink();
		$custdata = VikBooking::buildCustData($arrcustdata, "\r\n");

		if (VBOPlatformDetection::isWordPress()) {
			$viklink = JURI::root() . "index.php?option=com_vikbooking&view=booking&sid=" . $sid . "&ts=" . $nowts . (!empty($pnodep) ? "&nodep=".$pnodep : "") . (!empty($pitemid) ? "&Itemid=" . $pitemid : "");
		} else {
			$bestitemid = VikBooking::findProperItemIdType(array('booking'));
			$viklink = VikBooking::externalroute("index.php?option=com_vikbooking&view=booking&sid=" . $sid . "&ts=" . $nowts . (!empty($pnodep) ? "&nodep=".$pnodep : ""), false, (!empty($bestitemid) ? $bestitemid : null));
		}

		$admail = VikBooking::getAdminMail();
		$ftitle = VikBooking::getFrontTitle();
		$pricestr = [];
		if ($is_package === true) {
			foreach ($rooms as $num => $r) {
				$pkg_cost = $pkg['pernight_total'] == 1 ? ($pkg['cost'] * $daysdiff) : $pkg['cost'];
				$pkg_cost = $pkg['perperson'] == 1 ? ($pkg_cost * ($arrpeople[$num]['adults'] > 0 ? $arrpeople[$num]['adults'] : 1)) : $pkg_cost;
				$cost_plus_tax = VikBooking::sayPackagePlusIva($pkg_cost, $pkg['idiva']);
				$pricestr[$num] = $pkg['name'].": ".$cost_plus_tax." ".$currencyname;
			}
		} else {
			foreach ($tars as $num => $tar) {
				$pricestr[$num] = VikBooking::getPriceName($tar[0]['idprice'], $vbo_tn) . ": " . VikBooking::sayCostPlusIva($tar[0]['cost'], $tar[0]['idprice'])  . " " . $currencyname . (!empty($tar[0]['attrdata']) ? "\n" . VikBooking::getPriceAttr($tar[0]['idprice'], $vbo_tn) . ": " . $tar[0]['attrdata'] : "");
			}
		}

		$currentUser = JFactory::getUser();
		$langtag = $vbo_tn->current_lang;
		$vcmchanneldata = $session->get('vcmChannelData', '');
		$vcmchanneldata = !empty($vcmchanneldata) && is_array($vcmchanneldata) && count($vcmchanneldata) > 0 ? $vcmchanneldata : '';

		// attempt to save customer
		$cpin->setCustomerExtraInfo($fieldflags);
		$cpin->saveCustomerDetails($t_first_name, $t_last_name, $useremail, $phone_number, $usercountry, $arrcfields);

		// collect all room IDs involved
		$rooms_involved = [];
		foreach ($rooms as $room_booked) {
			if (!in_array($room_booked['id'], $rooms_involved)) {
				$rooms_involved[] = $room_booked['id'];
			}
		}

		$must_payment = $mod_booking ? false : VikBooking::areTherePayments($rooms_involved);
		$payment = [];
		if ($must_payment) {
			$payment = VikBooking::getPayment($pgpayid);
		}
		if ($must_payment && empty($payment)) {
			// error, payment was not selected
			VikError::raiseWarning('', JText::translate('ERRSELECTPAYMENT'));

			// build redirect URI values
			$redirect_uri_vals = [
				'option' => 'com_vikbooking',
				'task' => 'oconfirm',
			];

			foreach ($prices as $num => $pid) {
				$redirect_uri_vals['priceid' . $num] = $pid;
			}

			for ($ir = 1; $ir <= $proomsnum; $ir++) {
				if (isset($selopt[$ir]) && is_array($selopt[$ir])) {
					foreach ($selopt[$ir] as $opt) {
						if (array_key_exists('chageintv', $opt)) {
							if (!isset($redirect_uri_vals['optid' . $ir . $opt['id']])) {
								$redirect_uri_vals['optid' . $ir . $opt['id']] = [];
							}
							$redirect_uri_vals['optid' . $ir . $opt['id']][] = $opt['chageintv'];
						} else {
							$redirect_uri_vals['optid' . $ir . $opt['id']] = $opt['quan'];
						}
					}
				}
			}

			$redirect_uri_vals['roomid'] = [];
			foreach ($rooms as $num => $r) {
				$redirect_uri_vals['roomid'][] = $r['id'];
			}

			$redirect_uri_vals['adults'] = [];
			$redirect_uri_vals['children'] = [];
			foreach ($arrpeople as $indroom => $aduch) {
				$redirect_uri_vals['adults'][] = $aduch['adults'];
				$redirect_uri_vals['children'][] = $aduch['children'];
			}

			$redirect_uri_vals['roomsnum'] = $proomsnum;
			$redirect_uri_vals['days'] = $pdays;
			$redirect_uri_vals['checkin'] = $pcheckin;
			$redirect_uri_vals['checkout'] = $pcheckout;
			if (!empty($split_stay)) {
				$redirect_uri_vals['split_stay'] = $split_stay;
			}
			$redirect_uri_vals['Itemid'] = !empty($pitemid) ? $pitemid : null;

			$app->redirect(JRoute::rewrite('index.php?' . http_build_query($redirect_uri_vals), false));
			exit;
		}

		// turnover seconds
		$turnover_secs = VikBooking::getHoursRoomAvail() * 3600;
		$realback = $turnover_secs + $pcheckout;

		// push data to tracker for conversion
		$vbo_tracker = VikBooking::getTracker();
		$vbo_tracker->pushDates($pcheckin, $pcheckout, $pdays)->pushParty($arrpeople)->pushData('idcustomer', $cpin->getNewCustomerId());

		/**
		 * Custom check-in/out times due to late check-out/early check-in options.
		 * 
		 * @since 	1.17.2 (J) - 1.7.2 (WP)
		 */
		if ($custom_checkinout) {
			// overwrite check-in and/or check-out timestamp(s)
			if ($custom_checkinout[0] >= 3600) {
				// overwrite check-in timestamp
				$time_hours = floor($custom_checkinout[0] / 3600);
				$time_minutes = floor(($custom_checkinout[0] - ($time_hours * 3600)) / 60);
				$pcheckin = mktime($time_hours, $time_minutes, 0, $checkin_info['mon'], $checkin_info['mday'], $checkin_info['year']);
			}
			if ($custom_checkinout[1] >= 3600) {
				// overwrite check-out timestamp
				$time_hours = floor($custom_checkinout[1] / 3600);
				$time_minutes = floor(($custom_checkinout[1] - ($time_hours * 3600)) / 60);
				$pcheckout = mktime($time_hours, $time_minutes, 0, $checkout_info['mon'], $checkout_info['mday'], $checkout_info['year']);
				// overwrite "realback" timestamp as well
				$realback = $turnover_secs + $pcheckout;
			}
		}

		if (!$mod_booking && ((!empty($payment) && intval($payment['setconfirmed']) == 1) || !$must_payment || ($usedcoupon && $isdue <= 0))) {
			// we enter this statement to set the booking to Confirmed when: no booking modification and, payment selected sets status to confirmed or no payments enabled or 100% coupon
			$arrbusy = [];
			foreach ($rooms as $num => $r) {
				// determine the number of nights of stay and dates to consider
				$room_checkin  = $pcheckin;
				$room_checkout = $pcheckout;
				$room_realback = $realback;
				if (!empty($split_stay) && !empty($split_stay[($num - 1)]) && $split_stay[($num - 1)]['idroom'] == $r['id']) {
					$room_checkin  = $split_stay[($num - 1)]['checkin_ts'];
					$room_checkout = $split_stay[($num - 1)]['checkout_ts'];
					$room_realback = $turnover_secs + $room_checkout;
				}

				$busy_record = new stdClass;
				$busy_record->idroom = $r['id'];
				$busy_record->checkin = $room_checkin;
				$busy_record->checkout = $room_checkout;
				$busy_record->realback = $room_realback;

				$dbo->insertObject('#__vikbooking_busy', $busy_record, 'id');

				if (!isset($busy_record->id)) {
					showSelectVb('Critical error while occupying the rooms. Please try again');
					return;
				}

				$arrbusy[$num] = $busy_record->id;
			}

			// store booking
			$booking_record = new stdClass;
			$booking_record->custdata = $custdata;
			$booking_record->ts = $nowts;
			$booking_record->status = 'confirmed';
			$booking_record->days = $pdays;
			$booking_record->checkin = $pcheckin;
			$booking_record->checkout = $pcheckout;
			$booking_record->custmail = $useremail;
			$booking_record->sid = $sid;
			$booking_record->idpayment = !empty($payment) ? ($payment['id'] . '=' . $payment['name']) : null;
			$booking_record->ujid = $currentUser->id;
			$booking_record->coupon = $usedcoupon === true ? $strcouponeff : null;
			$booking_record->roomsnum = count($rooms);
			$booking_record->total = (float)$isdue;
			$booking_record->channel = is_array($vcmchanneldata) && !empty($vcmchanneldata['name']) ? $vcmchanneldata['name'] : null;
			$booking_record->lang = $langtag;
			$booking_record->country = !empty($usercountry) ? $usercountry : null;
			$booking_record->tot_taxes = (float)$tot_taxes;
			$booking_record->tot_city_taxes = (float)$tot_city_taxes;
			$booking_record->tot_fees = (float)$tot_fees;
			if ($tot_damage_dep) {
				$booking_record->tot_damage_dep = (float) $tot_damage_dep;
			}
			$booking_record->phone = $phone_number;
			$booking_record->pkg = $is_package === true ? (int)$pkg['id'] : null;
			$booking_record->split_stay = !empty($split_stay) ? 1 : 0;

			/**
			 * Trigger event to allow third party plugins to overwrite any booking property before it gets created.
			 * 
			 * @since 	1.18.3 (J) - 1.8.3 (WP)
			 */
			VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeCreateBookingRecord', [$booking_record, $rooms, $tars, $selopt, $arrpeople]);

			$dbo->insertObject('#__vikbooking_orders', $booking_record, 'id');

			if (!isset($booking_record->id)) {
				showSelectVb('Critical error while saving the booking. Please try again');
				return;
			}
			$neworderid = $booking_record->id;

			// ConfirmationNumber
			$confirmnumber = VikBooking::generateConfirmNumber($neworderid, true);

			// assign room specific unit
			$set_room_indexes = (VikBooking::autoRoomUnit() || (count($proomindex) == count($rooms)));
			$room_indexes_usemap = [];
			$room_indexes_forcemap = [];

			foreach ($rooms as $num => $r) {
				$q = "INSERT INTO `#__vikbooking_ordersbusy` (`idorder`,`idbusy`) VALUES(" . (int)$neworderid . ", " . (int)$arrbusy[$num] . ");";
				$dbo->setQuery($q);
				$dbo->execute();
				$json_ch_age = '';
				if (array_key_exists($num, $children_age)) {
					$json_ch_age = json_encode($children_age[$num]);
				}
				// assign room specific unit
				$room_indexes = $set_room_indexes === true ? VikBooking::getRoomUnitNumsAvailable(array('id' => $neworderid, 'checkin' => $pcheckin, 'checkout' => $pcheckout), $r['id']) : array();
				$use_ind_key = 0;
				$force_rindex = 0;
				if ($room_indexes && isset($room_indexes_forcemap[$r['id']])) {
					// an index for this same room was forced already, reset the values
					foreach ($room_indexes as $av_key => $av_index) {
						if (in_array((int)$av_index, $room_indexes_forcemap[$r['id']])) {
							unset($room_indexes[$av_key]);
						}
					}
					$room_indexes = array_values($room_indexes);
				}
				if ($room_indexes) {
					if (count($proomindex) == count($rooms) && !empty($proomindex[($num - 1)])) {
						// exact distinctive feature index selected
						foreach ($room_indexes as $av_index) {
							if ((int)$av_index == (int)$proomindex[($num - 1)]) {
								// requested index is available
								$force_rindex = (int)$proomindex[($num - 1)];
								if (isset($room_indexes_forcemap[$r['id']]) && in_array($force_rindex, $room_indexes_forcemap[$r['id']])) {
									// cannot book the same unit twice
									$force_rindex = 0;
									continue;
								}
								break;
							}
						}
						if ($force_rindex) {
							// store the forced index for any possible equal room booked later in the same loop
							if (!isset($room_indexes_forcemap[$r['id']])) {
								$room_indexes_forcemap[$r['id']] = [];
							}
							array_push($room_indexes_forcemap[$r['id']], $force_rindex);
						}
					}
					if (!array_key_exists($r['id'], $room_indexes_usemap)) {
						$room_indexes_usemap[$r['id']] = $use_ind_key;
					} else {
						$use_ind_key = $room_indexes_usemap[$r['id']];
					}
					if (isset($room_indexes[$use_ind_key])) {
						$rooms[$num]['roomindex'] = (int)$room_indexes[$use_ind_key];
					}
				}
				//
				$pkg_cost = 0;
				if ($is_package === true) {
					$pkg_cost = $pkg['pernight_total'] == 1 ? ($pkg['cost'] * $daysdiff) : $pkg['cost'];
					$pkg_cost = $pkg['perperson'] == 1 ? ($pkg_cost * ($arrpeople[$num]['adults'] > 0 ? $arrpeople[$num]['adults'] : 1)) : $pkg_cost;
					// $pkg_cost = VikBooking::sayPackagePlusIva($pkg_cost, $pkg['idiva']);
				}
				
				$oroom_record = new stdClass;
				$oroom_record->idorder 		= (int)$neworderid;
				$oroom_record->idroom 		= (int)$r['id'];
				$oroom_record->adults 		= (int)$arrpeople[$num]['adults'];
				$oroom_record->children 	= (int)$arrpeople[$num]['children'];
				$oroom_record->pets 		= isset($arrpeople[$num]['pets']) ? (int)$arrpeople[$num]['pets'] : 0;
				$oroom_record->idtar 		= (int)$tars[$num][0]['id'];
				$oroom_record->optionals 	= isset($selopt['room'.$num]) ? $selopt['room'.$num] : null;
				$oroom_record->childrenage 	= (!empty($json_ch_age) ? $json_ch_age : null);
				$oroom_record->t_first_name = $t_first_name;
				$oroom_record->t_last_name 	= $t_last_name;
				$oroom_record->roomindex 	= null;
				if ($force_rindex) {
					$oroom_record->roomindex = $force_rindex;
				} elseif ($room_indexes && isset($room_indexes[$use_ind_key])) {
					$oroom_record->roomindex = (int)$room_indexes[$use_ind_key];
				}
				$oroom_record->pkg_id 		= ($is_package === true ? (int)$pkg['id'] : null);
				$oroom_record->pkg_name 	= ($is_package === true ? $pkg['name'] : null);
				$oroom_record->cust_cost 	= ($is_package === true ? $pkg_cost : null);
				$oroom_record->cust_idiva 	= ($is_package === true ? (int)$pkg['idiva'] : null);
				$oroom_record->room_cost 	= (array_key_exists($num, $rooms_costs_map) ? $rooms_costs_map[$num] : null);

				$dbo->insertObject('#__vikbooking_ordersrooms', $oroom_record, 'id');

				if ($room_indexes) {
					$room_indexes_usemap[$r['id']]++;
				}
			}

			if (!empty($split_stay)) {
				// save transient on db for split stay information
				VBOFactory::getConfig()->set('split_stay_' . $neworderid, json_encode($split_stay));
			}

			// customer booking
			$cpin->saveCustomerBooking($neworderid);

			if ($usedcoupon === true && $coupon['type'] == 2) {
				$q = "DELETE FROM `#__vikbooking_coupons` WHERE `id`='".$coupon['id']."';";
				$dbo->setQuery($q);
				$dbo->execute();
			}

			// check if some of the rooms booked have shared calendars
			VikBooking::updateSharedCalendars($neworderid, array(), $pcheckin, $pcheckout);

			// send email notification to guest and admin
			VikBooking::sendBookingEmail($neworderid, ['guest', 'admin']);

			//SMS
			VikBooking::sendBookingSMS($neworderid);

			//Booking History
			VikBooking::getBookingHistoryInstance()->setBid($neworderid)->store('NC', 'IP: '.VikRequest::getVar('REMOTE_ADDR', '', 'server'));

			//invoke VikChannelManager
			if (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "synch.vikbooking.php")) {
				require_once(VCM_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "synch.vikbooking.php");
				$vcm = new SynchVikBooking($neworderid);
				$vcm->setPushType('new')->sendRequest();
			}

			// VBO 1.11 - push data to tracker for conversion
			$vbo_tracker->pushData('idorder', $neworderid)->closeTrack();
			$vbo_tracker->resetTrack();

			$app->redirect(JRoute::rewrite("index.php?option=com_vikbooking&view=booking&sid=" . $sid . "&ts=" . $nowts . (!empty($pnodep) ? "&nodep=".$pnodep : "") . (!empty($pitemid) ? "&Itemid=" . $pitemid : ""), false));
		} elseif ($mod_booking) {
			// booking modification statement
			// get current orders-busy relations
			$old_busy_ids = [];
			$q = "SELECT `idbusy` FROM `#__vikbooking_ordersbusy` WHERE `idorder`=".(int)$mod_booking['id'].";";
			$dbo->setQuery($q);
			$getbusy = $dbo->loadAssocList();
			if ($getbusy) {
				foreach ($getbusy as $gbu) {
					array_push($old_busy_ids, $gbu['idbusy']);
				}
			}
			//remove current busy records
			if (count($old_busy_ids)) {
				$q = "DELETE FROM `#__vikbooking_busy` WHERE `id` IN (".implode(', ', $old_busy_ids).");";
				$dbo->setQuery($q);
				$dbo->execute();
			}
			$q = "DELETE FROM `#__vikbooking_ordersbusy` WHERE `idorder`=".(int)$mod_booking['id'].";";
			$dbo->setQuery($q);
			$dbo->execute();
			//get current rooms (for VCM and for composing the log)
			$q = "SELECT `or`.*,`r`.`name`,`r`.`idopt`,`r`.`units`,`r`.`fromadult`,`r`.`toadult` FROM `#__vikbooking_ordersrooms` AS `or`,`#__vikbooking_rooms` AS `r` WHERE `or`.`idorder`=".(int)$mod_booking['id']." AND `or`.`idroom`=`r`.`id` ORDER BY `or`.`id` ASC;";
			$dbo->setQuery($q);
			$dbo->execute();
			$old_ordersrooms = $dbo->loadAssocList();
			$mod_booking['rooms_info'] = $old_ordersrooms;
			//remove current rooms
			$q = "DELETE FROM `#__vikbooking_ordersrooms` WHERE `idorder`=".(int)$mod_booking['id'].";";
			$dbo->setQuery($q);
			$dbo->execute();
			//update the booking by creating first the new busy records
			$arrbusy = [];
			foreach ($rooms as $num => $r) {
				$q = "INSERT INTO `#__vikbooking_busy` (`idroom`,`checkin`,`checkout`,`realback`) VALUES(".(int)$r['id'].", ".$dbo->quote($pcheckin).", ".$dbo->quote($pcheckout).", ".$dbo->quote($realback).");";
				$dbo->setQuery($q);
				$dbo->execute();
				$lid = $dbo->insertid();
				$arrbusy[$num] = $lid;
			}
			// assign room specific unit
			$set_room_indexes = (VikBooking::autoRoomUnit() || (count($proomindex) == count($rooms)));
			$room_indexes_usemap = [];
			$room_indexes_forcemap = [];
			//create the new rooms and orders-busy relations
			foreach ($rooms as $num => $r) {
				$q = "INSERT INTO `#__vikbooking_ordersbusy` (`idorder`,`idbusy`) VALUES(".(int)$mod_booking['id'].", ".(int)$arrbusy[$num].");";
				$dbo->setQuery($q);
				$dbo->execute();
				$json_ch_age = '';
				if (array_key_exists($num, $children_age)) {
					$json_ch_age = json_encode($children_age[$num]);
				}
				// assign room specific unit
				$room_indexes = $set_room_indexes === true ? VikBooking::getRoomUnitNumsAvailable(array('id' => $mod_booking['id'], 'checkin' => $pcheckin, 'checkout' => $pcheckout), $r['id']) : array();
				$use_ind_key = 0;
				$force_rindex = 0;
				if ($room_indexes && isset($room_indexes_forcemap[$r['id']])) {
					// an index for this same room was forced already, reset the values
					foreach ($room_indexes as $av_key => $av_index) {
						if (in_array((int)$av_index, $room_indexes_forcemap[$r['id']])) {
							unset($room_indexes[$av_key]);
						}
					}
					$room_indexes = array_values($room_indexes);
				}
				if ($room_indexes) {
					if (count($proomindex) == count($rooms) && !empty($proomindex[($num - 1)])) {
						// exact distinctive feature index selected
						foreach ($room_indexes as $av_index) {
							if ((int)$av_index == (int)$proomindex[($num - 1)]) {
								// requested index is available
								$force_rindex = (int)$proomindex[($num - 1)];
								if (isset($room_indexes_forcemap[$r['id']]) && in_array($force_rindex, $room_indexes_forcemap[$r['id']])) {
									// cannot book the same unit twice
									$force_rindex = 0;
									continue;
								}
								break;
							}
						}
						if ($force_rindex) {
							// store the forced index for any possible equal room booked later in the same loop
							if (!isset($room_indexes_forcemap[$r['id']])) {
								$room_indexes_forcemap[$r['id']] = [];
							}
							array_push($room_indexes_forcemap[$r['id']], $force_rindex);
						}
					}
					if (!array_key_exists($r['id'], $room_indexes_usemap)) {
						$room_indexes_usemap[$r['id']] = $use_ind_key;
					} else {
						$use_ind_key = $room_indexes_usemap[$r['id']];
					}
					if (isset($room_indexes[$use_ind_key])) {
						$rooms[$num]['roomindex'] = (int)$room_indexes[$use_ind_key];
					}
				}
				//
				$pkg_cost = 0;
				if ($is_package === true) {
					$pkg_cost = $pkg['pernight_total'] == 1 ? ($pkg['cost'] * $daysdiff) : $pkg['cost'];
					$pkg_cost = $pkg['perperson'] == 1 ? ($pkg_cost * ($arrpeople[$num]['adults'] > 0 ? $arrpeople[$num]['adults'] : 1)) : $pkg_cost;
					// $pkg_cost = VikBooking::sayPackagePlusIva($pkg_cost, $pkg['idiva']);
				}

				$oroom_record = new stdClass;
				$oroom_record->idorder 		= (int)$mod_booking['id'];
				$oroom_record->idroom 		= (int)$r['id'];
				$oroom_record->adults 		= (int)$arrpeople[$num]['adults'];
				$oroom_record->children 	= (int)$arrpeople[$num]['children'];
				$oroom_record->pets 		= isset($arrpeople[$num]['pets']) ? (int)$arrpeople[$num]['pets'] : 0;
				$oroom_record->idtar 		= (int)$tars[$num][0]['id'];
				$oroom_record->optionals 	= isset($selopt['room'.$num]) ? $selopt['room'.$num] : null;
				$oroom_record->childrenage 	= (!empty($json_ch_age) ? $json_ch_age : null);
				$oroom_record->t_first_name = $t_first_name;
				$oroom_record->t_last_name 	= $t_last_name;
				$oroom_record->roomindex 	= null;
				if ($force_rindex) {
					$oroom_record->roomindex = $force_rindex;
				} elseif ($room_indexes && isset($room_indexes[$use_ind_key])) {
					$oroom_record->roomindex = (int)$room_indexes[$use_ind_key];
				}
				$oroom_record->pkg_id 		= ($is_package === true ? (int)$pkg['id'] : null);
				$oroom_record->pkg_name 	= ($is_package === true ? $pkg['name'] : null);
				$oroom_record->cust_cost 	= ($is_package === true ? $pkg_cost : null);
				$oroom_record->cust_idiva 	= ($is_package === true ? (int)$pkg['idiva'] : null);
				$oroom_record->room_cost 	= (array_key_exists($num, $rooms_costs_map) ? $rooms_costs_map[$num] : null);

				$dbo->insertObject('#__vikbooking_ordersrooms', $oroom_record, 'id');

				if ($room_indexes) {
					$room_indexes_usemap[$r['id']]++;
				}
			}

			// update the booking record (do not touch information like sid, confirmnumber, payment method etc..)
			$logmod = VikBooking::getLogBookingModification($mod_booking);
			$mod_notes = $logmod.(!empty($mod_booking['adminnotes']) ? "\n\n".$mod_booking['adminnotes'] : '');
			// if old total lower than new total, increment paymcount to allow a new payment (if configuration setting enabled)
			$mod_paymcount = (int)$mod_booking['paymcount'];
			if ($mod_booking['total'] < $isdue) {
				$mod_paymcount++;
			}

			$q = $dbo->getQuery(true)
				->update($dbo->qn('#__vikbooking_orders'))
				->set($dbo->qn('custdata') . ' = ' . $dbo->q($custdata))
				->set($dbo->qn('ts') . ' = ' . $nowts)
				->set($dbo->qn('days') . ' = ' . $pdays)
				->set($dbo->qn('checkin') . ' = ' . $dbo->q($pcheckin))
				->set($dbo->qn('checkout') . ' = ' . $dbo->q($pcheckout))
				->set($dbo->qn('custmail') . ' = ' . $dbo->q($useremail))
				->set($dbo->qn('ujid') . ' = ' . (int) $currentUser->id)
				->set($dbo->qn('coupon') . ' = ' . ($usedcoupon === true ? $dbo->q($strcouponeff) : 'NULL'))
				->set($dbo->qn('roomsnum') . ' = ' . count($rooms))
				->set($dbo->qn('total') . ' = ' . $isdue)
				->set($dbo->qn('channel') . ' = ' . (is_array($vcmchanneldata) ? $dbo->q($vcmchanneldata['name']) : (!empty($mod_booking['channel']) ? $dbo->q($mod_booking['channel']) : 'NULL')))
				->set($dbo->qn('paymcount') . ' = ' . $mod_paymcount)
				->set($dbo->qn('adminnotes') . ' = ' . $dbo->q($mod_notes))
				->set($dbo->qn('lang') . ' = ' . $dbo->q($langtag))
				->set($dbo->qn('country') . ' = ' . (!empty($usercountry) ? $dbo->q($usercountry) : 'NULL'))
				->set($dbo->qn('tot_taxes') . ' = ' . $tot_taxes)
				->set($dbo->qn('tot_city_taxes') . ' = ' . $tot_city_taxes)
				->set($dbo->qn('tot_fees') . ' = ' . $tot_fees)
				->set($dbo->qn('tot_damage_dep') . ' = ' . $tot_damage_dep)
				->set($dbo->qn('phone') . ' = ' . $dbo->q($phone_number))
				->set($dbo->qn('pkg') . ' = ' . ($is_package === true ? (int) $pkg['id'] : 'NULL'))
				->where($dbo->qn('id') . ' = ' . (int) $mod_booking['id']);

			$dbo->setQuery($q);
			$dbo->execute();

			/**
			 * Trigger event to allow third party plugins to run after the booking is modified.
			 * 
			 * @since 	1.18.3 (J) - 1.8.3 (WP)
			 */
			VBOFactory::getPlatform()->getDispatcher()->trigger('onAfterModifyBookingRecord', [$mod_booking, $rooms, $tars, $selopt, $arrpeople]);

			// remove the coupon used (should never been allowed for modifications)
			if ($usedcoupon == true && $coupon['type'] == 2) {
				$q = "DELETE FROM `#__vikbooking_coupons` WHERE `id`=".(int)$coupon['id'].";";
				$dbo->setQuery($q);
				$dbo->execute();
			}

			// unset any previously booked room due to calendar sharing (should not be necessary because busy records have already been purged)
			VikBooking::cleanSharedCalendarsBusy($mod_booking['id']);
			// check if some of the rooms booked have shared calendars
			VikBooking::updateSharedCalendars($mod_booking['id'], array(), $pcheckin, $pcheckout);

			//send email messages (admin and customer) and invoke SMS send
			VikBooking::sendBookingEmail($mod_booking['id'], array('guest', 'admin'), true, false, $type = 'modified');

			//SMS
			VikBooking::sendBookingSMS($mod_booking['id']);

			//Booking History
			VikBooking::getBookingHistoryInstance()->setBid($mod_booking['id'])->store('MW', $logmod);

			//invoke VikChannelManager
			if (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "synch.vikbooking.php")) {
				$vcm_obj = VikBooking::getVcmInvoker();
				$vcm_obj->setOids(array($mod_booking['id']))->setSyncType('modify')->setOriginalBooking($mod_booking);
				$vcm_obj->doSync();
			}

			//unset the session value
			$session->set('vboModBooking', '');

			// VBO 1.11 - push data to tracker for conversion
			$vbo_tracker->pushData('idorder', $mod_booking['id'])->pushMessage(JText::translate('VBOBOOKINGMODOK'))->closeTrack();
			$vbo_tracker->resetTrack();

			$app->enqueueMessage(JText::translate('VBOBOOKINGMODOK'));
			$app->redirect(JRoute::rewrite("index.php?option=com_vikbooking&view=booking&sid=" . $sid . "&ts=" . $nowts . (!empty($pnodep) ? "&nodep=".$pnodep : "") . (!empty($pitemid) ? "&Itemid=" . $pitemid : ""), false));
		} else {
			// booking must have status stand-by and proceed to the payment
			$booking_record = new stdClass;
			$booking_record->custdata = $custdata;
			$booking_record->ts = $nowts;
			$booking_record->status = 'standby';
			$booking_record->days = $pdays;
			$booking_record->checkin = $pcheckin;
			$booking_record->checkout = $pcheckout;
			$booking_record->custmail = $useremail;
			$booking_record->sid = $sid;
			$booking_record->idpayment = !empty($payment) ? ($payment['id'] . '=' . $payment['name']) : null;
			$booking_record->ujid = $currentUser->id;
			$booking_record->coupon = $usedcoupon === true ? $strcouponeff : null;
			$booking_record->roomsnum = count($rooms);
			$booking_record->total = (float)$isdue;
			$booking_record->channel = is_array($vcmchanneldata) && !empty($vcmchanneldata['name']) ? $vcmchanneldata['name'] : null;
			$booking_record->lang = $langtag;
			$booking_record->country = !empty($usercountry) ? $usercountry : null;
			$booking_record->tot_taxes = (float)$tot_taxes;
			$booking_record->tot_city_taxes = (float)$tot_city_taxes;
			$booking_record->tot_fees = (float)$tot_fees;
			if ($tot_damage_dep) {
				$booking_record->tot_damage_dep = (float) $tot_damage_dep;
			}
			$booking_record->phone = $phone_number;
			$booking_record->pkg = $is_package === true ? (int)$pkg['id'] : null;
			$booking_record->split_stay = !empty($split_stay) ? 1 : 0;

			/**
			 * Trigger event to allow third party plugins to overwrite any booking property before it gets created.
			 * 
			 * @since 	1.18.3 (J) - 1.8.3 (WP)
			 */
			VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeCreateBookingRecord', [$booking_record, $rooms, $tars, $selopt, $arrpeople]);

			$dbo->insertObject('#__vikbooking_orders', $booking_record, 'id');

			if (!isset($booking_record->id)) {
				showSelectVb('Critical error while saving the booking. Please try again');
				return;
			}
			$neworderid = $booking_record->id;

			$room_indexes_forcemap = [];
			foreach ($rooms as $num => $r) {
				$json_ch_age = '';
				if (array_key_exists($num, $children_age)) {
					$json_ch_age = json_encode($children_age[$num]);
				}

				$pkg_cost = 0;
				if ($is_package === true) {
					$pkg_cost = $pkg['pernight_total'] == 1 ? ($pkg['cost'] * $daysdiff) : $pkg['cost'];
					$pkg_cost = $pkg['perperson'] == 1 ? ($pkg_cost * ($arrpeople[$num]['adults'] > 0 ? $arrpeople[$num]['adults'] : 1)) : $pkg_cost;
					// $pkg_cost = VikBooking::sayPackagePlusIva($pkg_cost, $pkg['idiva']);
				}
				
				$oroom_record = new stdClass;
				$oroom_record->idorder 		= (int)$neworderid;
				$oroom_record->idroom 		= (int)$r['id'];
				$oroom_record->adults 		= (int)$arrpeople[$num]['adults'];
				$oroom_record->children 	= (int)$arrpeople[$num]['children'];
				$oroom_record->pets 		= isset($arrpeople[$num]['pets']) ? (int)$arrpeople[$num]['pets'] : 0;
				$oroom_record->idtar 		= (int)$tars[$num][0]['id'];
				$oroom_record->optionals 	= isset($selopt['room'.$num]) ? $selopt['room'.$num] : null;
				$oroom_record->childrenage 	= (!empty($json_ch_age) ? $json_ch_age : null);
				$oroom_record->t_first_name = $t_first_name;
				$oroom_record->t_last_name 	= $t_last_name;
				$oroom_record->roomindex 	= null;
				if (count($proomindex) == count($rooms) && !empty($proomindex[($num - 1)])) {
					// check if the sub-unit requested is available
					if (!isset($room_indexes_forcemap[$r['id']])) {
						$room_indexes_forcemap[$r['id']] = [];
					}
					$room_indexes = VikBooking::getRoomUnitNumsAvailable(array('id' => $neworderid, 'checkin' => $pcheckin, 'checkout' => $pcheckout), $r['id']);
					$force_rindex = 0;
					foreach ($room_indexes as $av_index) {
						if ((int)$av_index == (int)$proomindex[($num - 1)] && !in_array((int)$proomindex[($num - 1)], $room_indexes_forcemap[$r['id']])) {
							// requested index is available
							$force_rindex = (int)$proomindex[($num - 1)];
							array_push($room_indexes_forcemap[$r['id']], $force_rindex);
							break;
						}
					}
					if (!empty($force_rindex)) {
						$oroom_record->roomindex = $force_rindex;
					}
				}
				$oroom_record->pkg_id 		= ($is_package === true ? (int)$pkg['id'] : null);
				$oroom_record->pkg_name 	= ($is_package === true ? $pkg['name'] : null);
				$oroom_record->cust_cost 	= ($is_package === true ? $pkg_cost : null);
				$oroom_record->cust_idiva 	= ($is_package === true ? (int)$pkg['idiva'] : null);
				$oroom_record->room_cost 	= (array_key_exists($num, $rooms_costs_map) ? $rooms_costs_map[$num] : null);

				$dbo->insertObject('#__vikbooking_ordersrooms', $oroom_record, 'id');
			}
			
			if ($usedcoupon === true && $coupon['type'] == 2) {
				$q = "DELETE FROM `#__vikbooking_coupons` WHERE `id`=" . (int)$coupon['id'] . ";";
				$dbo->setQuery($q);
				$dbo->execute();
			}

			// lock rooms waiting to be confirmed
			$lock_until_ts = VikBooking::getMinutesLock(true);
			foreach ($rooms as $num => $r) {
				// determine the number of nights of stay and dates to consider
				$room_checkin  = $pcheckin;
				$room_checkout = $pcheckout;
				$room_realback = $realback;
				if (!empty($split_stay) && !empty($split_stay[($num - 1)]) && $split_stay[($num - 1)]['idroom'] == $r['id']) {
					$room_checkin  = $split_stay[($num - 1)]['checkin_ts'];
					$room_checkout = $split_stay[($num - 1)]['checkout_ts'];
					$room_realback = $turnover_secs + $room_checkout;
				}

				$tmp_lock_record = new stdClass;
				$tmp_lock_record->idroom = $r['id'];
				$tmp_lock_record->checkin = $room_checkin;
				$tmp_lock_record->checkout = $room_checkout;
				$tmp_lock_record->until = $lock_until_ts;
				$tmp_lock_record->realback = $room_realback;
				$tmp_lock_record->idorder = (int)$neworderid;

				$dbo->insertObject('#__vikbooking_tmplock', $tmp_lock_record, 'id');
			}

			if (!empty($split_stay)) {
				// save transient on db for split stay information
				VBOFactory::getConfig()->set('split_stay_' . $neworderid, json_encode($split_stay));
			}

			// Customer Booking
			$cpin->saveCustomerBooking($neworderid);

			// send email notification to guest and admin
			VikBooking::sendBookingEmail($neworderid, ['guest', 'admin']);

			//SMS
			VikBooking::sendBookingSMS($neworderid);

			//Booking History
			VikBooking::getBookingHistoryInstance()->setBid($neworderid)->store('NP', 'IP: ' . VikRequest::getVar('REMOTE_ADDR', '', 'server'));

			/**
			 * Invoke VikChannelManager also in case of pending reservations.
			 * 
			 * @since 		1.16.5 (J) - 1.6.5 (WP)
			 * 
			 * @requires 	VCM >= 1.8.20
			 */
			if (class_exists('VCMRequestAvailability')) {
				VikBooking::getVcmInvoker()
					->setOids([$neworderid])
					->setSyncType('new')
					->doSync();
			}

			// VBO 1.11 - push data to tracker for conversion
			$vbo_tracker->pushData('idorder', $neworderid)->closeTrack();
			$vbo_tracker->resetTrack();

			// redirect URI to pending booking details
			$booking_details_uri = "index.php?option=com_vikbooking&view=booking&sid=" . $sid . "&ts=" . $nowts . (!empty($pnodep) ? "&nodep=".$pnodep : "") . (!empty($pitemid) ? "&Itemid=" . $pitemid : "");

			/**
			 * Trigger event to allow third-party plugins to manipulate the redirect URI.
			 * 
			 * @since 	1.17.2 (J) - 1.7.2 (WP)
			 */
			VBOFactory::getPlatform()->getDispatcher()->trigger('onRedirectOrder', [&$booking_details_uri, $neworderid]);

			// redirect to booking details page
			$app->redirect(JRoute::rewrite($booking_details_uri, false));
		}
	}

	public function vieworder()
	{
		VikRequest::setVar('view', 'booking');
		parent::display();
	}
	
	public function cancelrequest()
	{
		if (!JSession::checkToken()) {
			// missing CSRF-proof token
			VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
		}

		$dbo = JFactory::getDbo();
		$mainframe = JFactory::getApplication();

		$psid = VikRequest::getString('sid', '', 'request');
		$pidorder = VikRequest::getString('idorder', '', 'request');

		if (!empty($psid) && !empty($pidorder)) {
			$q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=".intval($pidorder)." AND `sid`=".$dbo->quote($psid).";";
			$dbo->setQuery($q);
			$order = $dbo->loadAssocList();
			if ($order) {
				$pemail = VikRequest::getString('email', '', 'request');
				$preason = VikRequest::getString('reason', '', 'request');
				if (!empty($pemail) && !empty($preason)) {
					$to = VikBooking::getAdminMail();
					if(strpos($to, ',') !== false) {
						$all_recipients = explode(',', $to);
						foreach ($all_recipients as $k => $v) {
							if(empty($v)) {
								unset($all_recipients[$k]);
							}
						}
						if(count($all_recipients) > 0) {
							$to = $all_recipients;
						}
					}
					//Booking History
					VikBooking::getBookingHistoryInstance()->setBid($order[0]['id'])->store('CR', $pemail."\n".$preason);
					//
					$subject = JText::translate('VBCANCREQUESTEMAILSUBJ') . ' #' . $order[0]['id'];
					// @wponly 	we do not need to pass the "best item ID" to externalroute()
					$uri = VikBooking::externalroute("index.php?option=com_vikbooking&view=booking&sid=" . $order[0]['sid'] . "&ts=" . $order[0]['ts'], false);
					$msg = JText::sprintf('VBCANCREQUESTEMAILHEAD', $order[0]['id'], $uri)."\n\n".$preason;
					$vbo_app = VikBooking::getVboApplication();
					$adsendermail = VikBooking::getSenderMail();
					$vbo_app->sendMail($adsendermail, $adsendermail, $to, $pemail, $subject, $msg, false);
					$mainframe->enqueueMessage(JText::translate('VBCANCREQUESTMAILSENT'));
					$mainframe->redirect(JRoute::rewrite("index.php?option=com_vikbooking&view=booking&sid=".$order[0]['sid']."&ts=".$order[0]['ts']."&Itemid=".VikRequest::getString('Itemid', '', 'request'), false));
				} else {
					$mainframe->redirect(JRoute::rewrite("index.php?option=com_vikbooking&view=booking&sid=".$order[0]['sid']."&ts=".$order[0]['ts'], false));
				}
			} else {
				$mainframe->redirect("index.php");
			}
		} else {
			$mainframe->redirect("index.php");
		}
	}

	public function reqinfo()
	{
		$proomid = VikRequest::getInt('roomid', '', 'request');
		$preqinfotoken = VikRequest::getInt('reqinfotoken', '', 'request');
		$pitemid = VikRequest::getInt('Itemid', '', 'request');
		$dbo = JFactory::getDBO();
		$session = JFactory::getSession();
		$mainframe = JFactory::getApplication();
		$vbo_app = VikBooking::getVboApplication();
		if (!empty($proomid)) {
			$q = "SELECT `id`,`name` FROM `#__vikbooking_rooms` WHERE `id`=".(int)$proomid.";";
			$dbo->setQuery($q);
			$dbo->execute();
			if ($dbo->getNumRows() == 1) {
				$room = $dbo->loadAssocList();
				$goto = JRoute::rewrite('index.php?option=com_vikbooking&view=roomdetails&roomid='.$room[0]['id'].'&Itemid='.$pitemid, false);
				$preqname = VikRequest::getString('reqname', '', 'request');
				$preqemail = VikRequest::getString('reqemail', '', 'request');
				$preqmess = VikRequest::getString('reqmess', '', 'request');
				if (!empty($preqemail) && !empty($preqmess)) {
					/**
					 * captcha verification
					 * 
					 * @since 	1.2.3
					 */
					if ($vbo_app->isCaptcha() && !$vbo_app->reCaptcha('check')) {
						VikError::raiseWarning('', 'Invalid Captcha');
						$mainframe->redirect($goto);
						exit;
					}
					//
					$sesstoken = $session->get('vboreqinfo'.$room[0]['id'], '');
					if((int)$sesstoken == (int)$preqinfotoken) {
						$session->set('vboreqinfo'.$room[0]['id'], '');
						$to = VikBooking::getAdminMail();
						if(strpos($to, ',') !== false) {
							$all_recipients = explode(',', $to);
							foreach ($all_recipients as $k => $v) {
								if(empty($v)) {
									unset($all_recipients[$k]);
								}
							}
							if(count($all_recipients) > 0) {
								$to = $all_recipients;
							}
						}
						$subject = JText::sprintf('VBOROOMREQINFOSUBJ', $room[0]['name']);
						$msg = JText::translate('VBOROOMREQINFONAME').": ".$preqname."\n\n".JText::translate('VBOROOMREQINFOEMAIL').": ".$preqemail."\n\n".JText::translate('VBOROOMREQINFOMESS').":\n\n".$preqmess;
						$adsendermail = VikBooking::getSenderMail();
						$vbo_app->sendMail($adsendermail, $adsendermail, $to, $preqemail, $subject, $msg, false);
						$mainframe->enqueueMessage(JText::translate('VBOROOMREQINFOSENTOK'));
					} else {
						VikError::raiseWarning('', JText::translate('VBOROOMREQINFOTKNERR'));
					}
					$mainframe->redirect($goto);
				} else {
					VikError::raiseWarning('', JText::translate('VBOROOMREQINFOMISSFIELD'));
					$mainframe->redirect($goto);
				}
			} else {
				$mainframe->redirect("index.php");
			}
		} else {
			$mainframe->redirect("index.php");
		}
	}

	public function cron_exec()
	{
		if (VBOPlatformDetection::isWordPress())
		{
			// in WordPress it is no more needed to schedule a server cron job
			VBOHttpDocument::getInstance()->close(406, 'Cron jobs execution is scheduled by WordPress since VikBooking 1.5.10. Please remove any scheduled execution to this end-point.');
		}

		$app = JFactory::getApplication();

		$id_cron = $app->input->getUint('cron_id', 0);
		$key     = $app->input->getString('cronkey', '');

		$model = VBOMvcModel::getInstance('cronjob');

		// dispatch the cron job by injecting the cron key within the
		// configuration array, in order to make sure that the execution
		// of the job has been requested by a reliable caller
		$response = $model->dispatch($id_cron, ['key' => $key]);

		if ($response === false)
		{
			// an error has occurred
			$error = $model->getError();

			if (!$error instanceof Exception)
			{
				// wrap error message in an exception for a better ease of use
				$error = new Exception($error ?: 'Error', 500);
			}
			
			// terminate session with an error
			VBOHttpDocument::getInstance($app)->close($error->getCode(), $error->getMessage());
		}

		// display response code and teminate the session
		echo $response;
		$app->close();
	}
	
	public function notifypayment()
	{
		$app = JFactory::getApplication();
		$dbo = JFactory::getDbo();

		$session = JFactory::getSession();

		$config = VBOFactory::getConfig();
		$av_helper = VikBooking::getAvailabilityInstance();

		$psid = VikRequest::getString('sid', '', 'request');
		$pts = VikRequest::getString('ts', '', 'request');

		$nowdf = VikBooking::getDateFormat();
		if ($nowdf == "%d/%m/%Y") {
			$df = 'd/m/Y';
		} elseif ($nowdf == "%m/%d/%Y") {
			$df = 'm/d/Y';
		} else {
			$df = 'Y/m/d';
		}

		if (!strlen($psid) || !strlen($pts)) {
			VBOHttpDocument::getInstance()->close(500, 'Missing information for fetching the booking');
		}

		$admail = VikBooking::getAdminMail();
		$recipient_mail = $admail;
		if (!is_array($recipient_mail) && strpos($recipient_mail, ',') !== false) {
			$all_recipients = explode(',', $recipient_mail);
			foreach ($all_recipients as $k => $v) {
				if (empty($v)) {
					unset($all_recipients[$k]);
				}
			}
			if (count($all_recipients) > 0) {
				$recipient_mail = $all_recipients;
			}
		}

		// load booking details
		$q = "SELECT * FROM `#__vikbooking_orders` WHERE (`sid`=" . $dbo->quote($psid) . " OR `idorderota`=" . $dbo->quote($psid) . ") AND `ts`=" . $dbo->quote($pts);
		$dbo->setQuery($q, 0, 1);
		$row = $dbo->loadAssoc();
		if (!$row) {
			VBOHttpDocument::getInstance()->close(404, 'Booking not found');
		}

		// check if the language in use is the same as the one used during the checkout
		if (!empty($row['lang'])) {
			$lang = JFactory::getLanguage();
			if ($lang->getTag() != $row['lang']) {
				$lang->load('com_vikbooking', (VBOPlatformDetection::isWordPress() ? VIKBOOKING_SITE_LANG : JPATH_SITE), $row['lang'], true);
				if (VBOPlatformDetection::isJoomla()) {
					$lang->load('joomla', JPATH_SITE, $row['lang'], true);
				}
			}
		}

		// translator
		$vbo_tn = VikBooking::getTranslator();

		if ($row['status'] == 'confirmed' && !(VikBooking::multiplePayments() && $row['paymcount'] > 0)) {
			// booking can be paid only if not confirmed or if multiple payments are enabled and payment counter for booking greater than zero
			VBOHttpDocument::getInstance()->close(409, 'Conflicting and unexpected payment validation for this reservation');
		}

		/**
		 * Check split stay reservation data.
		 * 
		 * @since 	1.16.0 (J) - 1.6.0 (WP)
		 */
		$split_stay = [];
		if ($row['split_stay'] && $row['status'] != 'confirmed') {
			// check for transient on DB
			$split_stay = $config->getArray('split_stay_' . $row['id'], []);
		}

		// inject admin email
		$row['admin_email'] = $admail;

		// turnover seconds
		$turnover_secs = VikBooking::getHoursRoomAvail() * 3600;
		$realback = $turnover_secs + $row['checkout'];

		$currencyname = VikBooking::getCurrencyName();
		$ftitle = VikBooking::getFrontTitle();
		$nowts = time();

		$rooms = [];
		$tars = [];
		$arrpeople = [];
		$is_package = (bool)(!empty($row['pkg']));

		// load booked rooms
		$q = "SELECT `or`.`id` AS `or_id`,`or`.`idroom`,`or`.`adults`,`or`.`children`,`or`.`idtar`,`or`.`optionals`,`or`.`roomindex`,`or`.`pkg_id`,`or`.`pkg_name`,`or`.`cust_cost`,`or`.`cust_idiva`,`or`.`extracosts`,`or`.`otarplan`,`r`.`id` AS `r_reference_id`,`r`.`name`,`r`.`img`,`r`.`idcarat`,`r`.`fromadult`,`r`.`toadult`,`r`.`params` FROM `#__vikbooking_ordersrooms` AS `or`,`#__vikbooking_rooms` AS `r` WHERE `or`.`idorder`=" . $row['id'] . " AND `or`.`idroom`=`r`.`id` ORDER BY `or`.`id` ASC;";
		$dbo->setQuery($q);
		$orderrooms = $dbo->loadAssocList();
		if ($orderrooms) {
			$vbo_tn->translateContents($orderrooms, '#__vikbooking_rooms', array('id' => 'r_reference_id'));
			foreach ($orderrooms as $kor => $or) {
				$num = $kor + 1;
				$rooms[$num] = $or;
				$arrpeople[$num]['adults'] = $or['adults'];
				$arrpeople[$num]['children'] = $or['children'];
				if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
					// package or custom cost set from the back-end
					continue;
				}

				// determine the number of nights of stay and dates to consider
				$use_los = $row['days'];
				$room_checkin  = $row['checkin'];
				$room_checkout = $row['checkout'];
				if (!empty($split_stay) && !empty($split_stay[$kor]) && $split_stay[$kor]['idroom'] == $or['idroom']) {
					$use_los = (int)$split_stay[$kor]['nights'];
					$room_checkin  = $split_stay[$kor]['checkin_ts'];
					$room_checkout = $split_stay[$kor]['checkout_ts'];
				}

				$q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `id`=" . (int)$or['idtar'];
				$dbo->setQuery($q, 0, 1);
				$tar = $dbo->loadAssocList();
				if (!$tar) {
					continue;
				}

				$tar = VikBooking::applySeasonsRoom($tar, $room_checkin, $room_checkout);

				// apply OBP rules
				$tar = VBORoomHelper::getInstance()->applyOBPRules($tar, $or, $or['adults']);

				// push tariff
				$tars[$num] = $tar[0];
			}
		}

		// inject values
		$row['order_rooms'] = $orderrooms;
		$row['fares'] = $tars;

		// invoke the payment method class
		$exppay = explode('=', ($row['idpayment'] ?? ''));
		$payment = VikBooking::getPayment($exppay[0], $vbo_tn);

		/**
		 * Scan the booking and related rooms for damage deposit payment data.
		 * 
		 * @since 	1.17.6 (J) - 1.7.6 (WP)
		 */
		$damage_deposit_payment = VBORoomHelper::getInstance()->getDamageDepositSplitPayment($row, $orderrooms);

		if ($app->input->getBool('dd') && !empty($damage_deposit_payment['payment_window']['pay_id'])) {
			// load the proper payment driver
			$payment = VikBooking::getPayment($damage_deposit_payment['payment_window']['pay_id']) ?: $payment;
		}

		if (!$payment) {
			VBOHttpDocument::getInstance()->close(500, 'Could not load payment processor for validation.');
		}

		// calculate booking totals
		$isdue = 0;
		$tot_taxes = 0;
		$tot_city_taxes = 0;
		$tot_fees = 0;
		$tot_damage_dep = 0;
		$pricestr = [];
		$optstr = [];
		foreach ($orderrooms as $kor => $or) {
			$num = $kor + 1;

			// determine the number of nights of stay and dates to consider
			$use_los = $row['days'];
			$room_checkin  = $row['checkin'];
			$room_checkout = $row['checkout'];
			if (!empty($split_stay) && !empty($split_stay[$kor]) && $split_stay[$kor]['idroom'] == $or['idroom']) {
				$use_los = (int)$split_stay[$kor]['nights'];
				$room_checkin  = $split_stay[$kor]['checkin_ts'];
				$room_checkout = $split_stay[$kor]['checkout_ts'];
			}

			if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
				// package cost or cust_cost may not be inclusive of taxes if prices tax included is off
				$calctar = VikBooking::sayPackagePlusIva($or['cust_cost'], $or['cust_idiva']);
				$isdue += $calctar;
				if ($calctar == $or['cust_cost']) {
					$cost_minus_tax = VikBooking::sayPackageMinusIva($or['cust_cost'], $or['cust_idiva']);
					$tot_taxes += ($or['cust_cost'] - $cost_minus_tax);
				} else {
					$tot_taxes += ($calctar - $or['cust_cost']);
				}
				$pricestr[$num] = (!empty($or['pkg_name']) ? $or['pkg_name'] : (!empty($or['otarplan']) ? ucwords($or['otarplan']) : JText::translate('VBOROOMCUSTRATEPLAN'))).": ".$calctar." ".$currencyname;
			} elseif (array_key_exists($num, $tars) && is_array($tars[$num])) {
				$calctar = VikBooking::sayCostPlusIva($tars[$num]['cost'], $tars[$num]['idprice']);
				$tars[$num]['calctar'] = $calctar;
				$isdue += $calctar;
				if ($calctar == $tars[$num]['cost']) {
					$cost_minus_tax = VikBooking::sayCostMinusIva($tars[$num]['cost'], $tars[$num]['idprice']);
					$tot_taxes += ($tars[$num]['cost'] - $cost_minus_tax);
				} else {
					$tot_taxes += ($calctar - $tars[$num]['cost']);
				}
				$pricestr[$num] = VikBooking::getPriceName($tars[$num]['idprice'], $vbo_tn) . ": " . $calctar . " " . $currencyname . (!empty($tars[$num]['attrdata']) ? "\n" . VikBooking::getPriceAttr($tars[$num]['idprice'], $vbo_tn) . ": " . $tars[$num]['attrdata'] : "");
			}
			if (!empty($or['optionals'])) {
				$stepo = explode(";", $or['optionals']);
				foreach ($stepo as $roptkey => $oo) {
					if (empty($oo)) {
						continue;
					}
					$stept = explode(":", $oo);
					$q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . $dbo->quote($stept[0]) . ";";
					$dbo->setQuery($q);
					$actopt = $dbo->loadAssocList();
					if ($actopt) {
						$vbo_tn->translateContents($actopt, '#__vikbooking_optionals');

						// option params
						$opt_params = !empty($actopt[0]['oparams']) ? json_decode($actopt[0]['oparams'], true) : [];
						$opt_params = is_array($opt_params) ? $opt_params : [];

						$chvar = '';
						if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
							$optagenames = VikBooking::getOptionIntervalsAges($actopt[0]['ageintervals']);
							$optagepcent = VikBooking::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
							$optageovrct = VikBooking::getOptionIntervalChildOverrides($actopt[0], $or['adults'], $or['children']);
							$child_num 	 = VikBooking::getRoomOptionChildNumber($or['optionals'], $actopt[0]['id'], $roptkey, $or['children']);
							$optagecosts = VikBooking::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
							$agestept = explode('-', $stept[1]);
							$stept[1] = $agestept[0];
							$chvar = $agestept[1];
							if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
								//percentage value of the adults tariff
								if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
									$optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
								} else {
									$optagecosts[($chvar - 1)] = $tars[$num]['cost'] * $optagecosts[($chvar - 1)] / 100;
								}
							} elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
								//VBO 1.10 - percentage value of room base cost
								if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
									$optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
								} else {
									$display_rate = isset($tars[$num]['room_base_cost']) ? $tars[$num]['room_base_cost'] : $tars[$num]['cost'];
									$optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
								}
							}
							$actopt[0]['chageintv'] = $chvar;
							$actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
							$actopt[0]['quan'] = $stept[1];
							$realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $use_los * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
						} else {
							$actopt[0]['quan'] = $stept[1];
							// VBO 1.11 - options percentage cost of the room total fee
							if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
								$deftar_basecosts = $or['cust_cost'];
							} else {
								$deftar_basecosts = $tars[$num]['cost'];
							}
							$actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
							//
							$realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $use_los * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
						}
						if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
							$realcost = $actopt[0]['maxprice'];
							if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
								$realcost = $actopt[0]['maxprice'] * $stept[1];
							}
						}
						if ($actopt[0]['perperson'] == 1) {
							$realcost = $realcost * $or['adults'];
						}

						/**
						 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
						 * 
						 * @since 	1.17.7 (J) - 1.7.7 (WP)
						 */
						$custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$realcost, &$actopt[0], $row, $or]);
						if ($custom_calculation) {
							$realcost = (float) $custom_calculation[0];
						}

						$opt_minus_tax = VikBooking::sayOptionalsMinusIva($realcost, $actopt[0]['idiva']);
						$tmpopr = VikBooking::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
						if ($actopt[0]['is_citytax'] == 1) {
							$tot_city_taxes += $opt_minus_tax;
						} elseif ($actopt[0]['is_fee'] == 1) {
							$tot_fees += $opt_minus_tax;
						} elseif ($opt_params['damagedep'] ?? 0) {
							$tot_damage_dep += $opt_minus_tax;
						}
						// always calculate the amount of tax no matter if this is already a tax or a fee
						if ($tmpopr == $realcost) {
							$tot_taxes += ($realcost - $opt_minus_tax);
						} else {
							$tot_taxes += ($tmpopr - $realcost);
						}
						//
						$isdue += $tmpopr;
						$optstr[$num][] = ($stept[1] > 1 ? $stept[1] . " " : "") . $actopt[0]['name'] . ": " . $tmpopr . " " . $currencyname . "\n";
					}
				}
			}

			// custom extra costs
			if (!empty($or['extracosts'])) {
				$cur_extra_costs = json_decode($or['extracosts'], true);
				foreach ($cur_extra_costs as $eck => $ecv) {
					$ecplustax = !empty($ecv['idtax']) ? VikBooking::sayOptionalsPlusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
					$isdue += $ecplustax;
					$optstr[$num][] = $ecv['name'] . ": " . $ecplustax . " " . $currencyname."\n";
				}
			}
		}

		// coupon
		$usedcoupon = false;
		$origisdue = $isdue;
		if (strlen($row['coupon']) > 0) {
			$usedcoupon = true;
			$expcoupon = explode(";", $row['coupon']);
			$isdue = $isdue - $expcoupon[1];
		}

		if (empty($row['sid']) && !empty($row['idorderota']) && !empty($row['channel'])) {
			$row['sid'] = $row['idorderota'];
		}

		/**
         * Trigger event to allow third-party plugins to manipulate the transaction data.
         * 
         * @since 	1.18.5 (J) - 1.8.5 (WP)
         */
        VBOFactory::getPlatform()->getDispatcher()->trigger('onInitPaymentTransaction', [&$row, &$payment['params'], []]);

		if (VBOPlatformDetection::isWordPress()) {
			/**
			 * @wponly 	The payment gateway is now loaded 
			 * 			using the apposite dispatcher.
			 *
			 * @since 1.0.5
			 */
			JLoader::import('adapter.payment.dispatcher');
			$return_url = JUri::root() . "index.php?option=com_vikbooking&view=booking&sid=" . (!empty($row['idorderota']) && !empty($row['channel']) ? $row['idorderota'] : $row['sid']) . "&ts=" . $row['ts'];
			$error_url = JUri::root() . "index.php?option=com_vikbooking&view=booking&sid=" . (!empty($row['idorderota']) && !empty($row['channel']) ? $row['idorderota'] : $row['sid']) . "&ts=" . $row['ts'];
			$notify_url = JUri::root() . "index.php?option=com_vikbooking&task=notifypayment" . ($app->input->getBool('dd') ? '&dd=1' : '') . "&sid=" . (!empty($row['idorderota']) && !empty($row['channel']) ? $row['idorderota'] : $row['sid']) . "&ts=" . $row['ts'] . "&tmpl=component";
			$model 	= JModel::getInstance('vikbooking', 'shortcodes', 'admin');
			$itemid = $model->best(array('booking'), (!empty($row['lang']) ? $row['lang'] : null));
			$extra_data = [];
			if ($itemid) {
				$return_url = str_replace(JUri::root(), '', $return_url);
				$error_url = str_replace(JUri::root(), '', $error_url);
				$notify_url = str_replace(JUri::root(), '', $notify_url);
				$return_url = JRoute::rewrite($return_url . "&Itemid={$itemid}", false);
				$error_url = JRoute::rewrite($error_url . "&Itemid={$itemid}", false);
				$notify_url = JRoute::rewrite($notify_url . "&Itemid={$itemid}", false);
				$extra_data = array(
					'return_url' => $return_url,
					'error_url'  => $error_url,
					'notify_url' => $notify_url,
				);
			}
			$extra_data['transaction_currency'] = VikBooking::getCurrencyCodePp();

			$obj = JPaymentDispatcher::getInstance('vikbooking', $payment['file'], array_merge($row, $extra_data), $payment['params']);
		} else {
			/**
			 * @joomlaonly 	The Payment Factory library will invoke the gateway.
			 * Make sure to pass the payment gateway some common variables together with the order record.
			 * 
			 * @since 	1.14.3
			 */
			require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'payments' . DIRECTORY_SEPARATOR . 'libraries' . DIRECTORY_SEPARATOR . 'factory.php';

			$bestitemid = VikBooking::findProperItemIdType(array('booking'));
			$extra_data = array(
				'return_url' => VikBooking::externalroute("index.php?option=com_vikbooking&view=booking&sid=" . (!empty($row['idorderota']) && !empty($row['channel']) ? $row['idorderota'] : $row['sid']) . "&ts=" . $row['ts'], false, (!empty($bestitemid) ? $bestitemid : null)),
				'error_url'  => VikBooking::externalroute("index.php?option=com_vikbooking&view=booking&sid=" . (!empty($row['idorderota']) && !empty($row['channel']) ? $row['idorderota'] : $row['sid']) . "&ts=" . $row['ts'], false, (!empty($bestitemid) ? $bestitemid : null)),
				'notify_url' => VikBooking::externalroute("index.php?option=com_vikbooking&task=notifypayment" . ($app->input->getBool('dd') ? '&dd=1' : '') . "&sid=" . (!empty($row['idorderota']) && !empty($row['channel']) ? $row['idorderota'] : $row['sid']) . "&ts=" . $row['ts'] . "&tmpl=component", false, null),
			);
			$extra_data['transaction_currency'] = VikBooking::getCurrencyCodePp();

			$obj = VBOPaymentFactory::getPaymentInstance($payment['file'], array_merge($row, $extra_data), $payment['params']);
		}

		try {
			// let the gateway validate the payment transaction
			$array_result = $obj->validatePayment();
		} catch (Throwable $e) {
			// silently catch the error and set the log
			$array_result = [
				'verified' => 0,
				'log' => $e->getMessage() ?: 'Transaction error exception.',
			];
		}

		// build payment log
		$newpaymentlog = date('c')."\n".$array_result['log']."\n----------\n".$row['paymentlog'];

		/**
		 * OTA reservations containing PCI-DSS card details may receive additional payments through
		 * the website for upselling or for payments requested. Therefore, the previous card logs
		 * should be appended, not prepended to the current payment logs for the card details.
		 * 
		 * @since 	1.15.0 (J) - 1.5.0 (WP)
		 */
		if (!empty($row['idorderota']) && !empty($row['channel']) && !empty($row['paymentlog'])) {
			if (stripos($row['paymentlog'], 'card number') !== false && strpos($row['paymentlog'], '*') !== false) {
				$newpaymentlog = $row['paymentlog'] . "\n----------\n" . date('c') . "\n" . $array_result['log'];
			}
		}

		/**
		 * Ensure the size of the log does not make the query fail.
		 * 
		 * @since 	1.16.9 (J) - 1.6.9 (WP)
		 */
		if (strlen($newpaymentlog) > 55000) {
			$newpaymentlog = substr($newpaymentlog, 0, 55000) . '...';
		}

		if ($array_result['verified'] == 1) {
			// valid payment
			$shouldpay = $isdue;

			if ($payment['charge'] > 0.00) {
				if ($payment['ch_disc'] == 1) {
					// charge
					if ($payment['val_pcent'] == 1) {
						// fixed value
						$shouldpay += $payment['charge'];
					} else {
						// percent value
						$percent_to_pay = $shouldpay * $payment['charge'] / 100;
						$shouldpay += $percent_to_pay;
					}
				} else {
					// discount
					if ($payment['val_pcent'] == 1) {
						// fixed value
						$shouldpay -= $payment['charge'];
					} else {
						// percent value
						$percent_to_pay = $shouldpay * $payment['charge'] / 100;
						$shouldpay -= $percent_to_pay;
					}
				}
			}

			// deposit may be skipped by customer choice
			$shouldpay_befdep = $shouldpay;

			if (!VikBooking::payTotal()) {
				$percentdeposit = VikBooking::getAccPerCent();
				if ($percentdeposit > 0) {
					if (VikBooking::getTypeDeposit() == "fixed") {
						$shouldpay = $percentdeposit;
					} else {
						$shouldpay = $shouldpay * $percentdeposit / 100;
					}
				}
			}

			// check if a damage deposit was allowed to be paid
			$shouldpay_dd = $damage_deposit_payment['damagedep_gross'] ?? 0;
			$shouldpay_befdd = $shouldpay - $shouldpay_dd;

			// check if the total amount paid is the same as the order total
			if (isset($array_result['tot_paid'])) {
				$shouldpay = round($shouldpay, 2);
				$shouldpay_befdep = round($shouldpay_befdep, 2);
				$shouldpay_less_damagedep = round(($row['total'] - $row['tot_damage_dep']), 2);
				$totreceived = round($array_result['tot_paid'], 2);
				if ($shouldpay != $totreceived && $shouldpay_befdep != $totreceived && $shouldpay_befdd != $totreceived && $shouldpay_less_damagedep != $totreceived && $shouldpay_dd != $totreceived && $row['paymcount'] == 0) {
					// the amount paid is different than the order total
					// fares might have changed or the deposit might be different
					// Sending just an email to the admin that will check
					$vbo_app = VikBooking::getVboApplication();
					$adsendermail = VikBooking::getSenderMail();
					$vbo_app->sendMail($adsendermail, $adsendermail, $recipient_mail, $adsendermail, JText::translate('VBTOTPAYMENTINVALID'), JText::sprintf('VBTOTPAYMENTINVALIDTXT', $row['id'], $totreceived." (".$array_result['tot_paid'].")", $shouldpay), false);
				}

				// amount paid should be stored as exclusive of transaction fees/discounts
				if ($payment['charge'] > 0.00) {
					if ($payment['ch_disc'] == 1) {
						// charge
						if ($payment['val_pcent'] == 1) {
							// fixed value
							$array_result['tot_paid'] -= $payment['charge'];
						} else {
							// percent value
							$array_result['tot_paid'] = ($array_result['tot_paid'] / ((100 + $payment['charge']) / 100));
						}
					} else {
						// discount
						if ($payment['val_pcent'] == 1) {
							// fixed value
							$array_result['tot_paid'] += $payment['charge'];
						} else {
							// percent value
							$array_result['tot_paid'] = $array_result['tot_paid'] * (100 + $payment['charge']) / 100;
						}
					}
					$array_result['tot_paid'] = round($array_result['tot_paid'], 2);
				}
			}

			if ($row['paymcount'] == 0 || $row['status'] == 'standby') {
				foreach ($orderrooms as $indnum => $r) {
					$num = $indnum + 1;

					// determine the number of nights of stay and dates to consider
					$room_checkin  = $row['checkin'];
					$room_checkout = $row['checkout'];
					$room_realback = $turnover_secs + $row['checkout'];
					if (!empty($split_stay) && !empty($split_stay[$indnum]) && $split_stay[$indnum]['idroom'] == $r['idroom']) {
						$room_checkin  = $split_stay[$indnum]['checkin_ts'];
						$room_checkout = $split_stay[$indnum]['checkout_ts'];
						$room_realback = $turnover_secs + $split_stay[$indnum]['checkout_ts'];
					}

					$busy_record = new stdClass;
					$busy_record->idroom = $r['idroom'];
					$busy_record->checkin = $room_checkin;
					$busy_record->checkout = $room_checkout;
					$busy_record->realback = $room_realback;

					$dbo->insertObject('#__vikbooking_busy', $busy_record, 'id');

					if (!isset($busy_record->id)) {
						continue;
					}

					$q = "INSERT INTO `#__vikbooking_ordersbusy` (`idorder`,`idbusy`) VALUES(" . (int)$row['id'] . ", " . (int)$busy_record->id . ");";
					$dbo->setQuery($q);
					$dbo->execute();
				}
			}

			// ConfirmationNumber
			if ($row['paymcount'] == 0 || $row['status'] == 'standby') {
				$confirmnumber = VikBooking::generateConfirmNumber($row['id'], true);
			}

			// update payable amount in case of up-sells or simply in case of another payment received
			$new_payable = isset($array_result['tot_paid']) && $array_result['tot_paid'] ? ($row['payable'] - $array_result['tot_paid']) : 0;
			$new_payable = $new_payable < 0 ? 0 : $new_payable;

			// update booking record
			$booking_record = new stdClass;
			$booking_record->id = $row['id'];
			$booking_record->status = 'confirmed';
			if (isset($array_result['tot_paid']) && $array_result['tot_paid']) {
				$booking_record->totpaid = ($array_result['tot_paid'] + $row['totpaid']);
			}
			$booking_record->paymcount = ($row['paymcount'] + 1);
			if (!empty($array_result['log'])) {
				$booking_record->paymentlog = $newpaymentlog;
			}
			$booking_record->payable = $new_payable;

			// update booking record
			if (!$dbo->updateObject('#__vikbooking_orders', $booking_record, 'id')) {
				// payments log content may break the update
				unset($booking_record->paymentlog);
				// try once more
				$dbo->updateObject('#__vikbooking_orders', $booking_record, 'id');
			}

			// assign room specific unit
			$set_room_indexes = VikBooking::autoRoomUnit();
			$room_indexes_usemap = [];
			if ($set_room_indexes === true) {
				$q = "SELECT `id`,`idroom`,`roomindex` FROM `#__vikbooking_ordersrooms` WHERE `idorder`=".(int)$row['id'].";";
				$dbo->setQuery($q);
				$orooms = $dbo->loadAssocList();
				foreach ($orooms as $oroom) {
					if (!empty($oroom['roomindex'])) {
						// room specific unit has already been assigned
						continue;
					}
					$room_indexes = VikBooking::getRoomUnitNumsAvailable($row, $oroom['idroom']);
					$use_ind_key = 0;
					if ($room_indexes) {
						if (!array_key_exists($oroom['idroom'], $room_indexes_usemap)) {
							$room_indexes_usemap[$oroom['idroom']] = $use_ind_key;
						} else {
							$use_ind_key = $room_indexes_usemap[$oroom['idroom']];
						}
						$q = "UPDATE `#__vikbooking_ordersrooms` SET `roomindex`=".(int)$room_indexes[$use_ind_key]." WHERE `id`=".(int)$oroom['id'].";";
						$dbo->setQuery($q);
						$dbo->execute();
						// update rooms references for the customer email sending function
						foreach ($rooms as $rnum => $rr) {
							if ($rr['or_id'] == $oroom['id']) {
								$rooms[$rnum]['roomindex'] = (int)$room_indexes[$use_ind_key];
								break;
							}
						}
						$room_indexes_usemap[$oroom['idroom']]++;
					}
				}
			}

			// unlock room(s) for other imminent bookings
			$q = "DELETE FROM `#__vikbooking_tmplock` WHERE `idorder`=" . intval($row['id']) . ";";
			$dbo->setQuery($q);
			$dbo->execute();

			// customer booking
			$q = "SELECT `idcustomer` FROM `#__vikbooking_customers_orders` WHERE `idorder`=".(int)$row['id'].";";
			$dbo->setQuery($q);
			$customer_id = $dbo->loadResult();
			if ($customer_id) {
				$cpin = VikBooking::getCPinIstance();
				$cpin->updateBookingCommissions($row['id'], $customer_id);
			}

			// check if some of the rooms booked have shared calendars
			VikBooking::updateSharedCalendars($row['id'], array(), $row['checkin'], $row['checkout']);

			/**
			 * Trigger event to allow third-party plugins to choose whether payment notifications should be sent
			 * 
			 * @since 	1.16.10 (J) - 1.6.10 (WP)
			 */
			$send_notifications = true;
			$should_send = VBOFactory::getPlatform()->getDispatcher()->filter('onPaymentReceivedShouldSendNotifications', [$row]);
			if (is_array($should_send) && in_array(false, $should_send, true)) {
				$send_notifications = false;
			}

			if ($send_notifications) {
				// send email notification to guest and admin
				VikBooking::sendBookingEmail($row['id'], array('guest', 'admin'));

				// SMS
				VikBooking::sendBookingSMS($row['id']);
			}

			/**
			 * Payment gateways may set and return the transaction information
			 * to eventually support a later transaction of type refund.
			 * 
			 * @since 	1.14 (J) - 1.4.0 (WP)
			 * @since 	1.16.2 (J) - 1.6.2 (WP) we attempt to always store the amount paid with this transaction.
			 * @since 	1.16.9 (J) - 1.6.9 (WP) we attempt to store the amount of payment processing fees for the transaction.
			 * @since 	1.17.6 (J) - 1.7.6 (WP) added support to separate payment for damage deposit.
			 */
			$tn_data = $array_result['transaction'] ?? null;
			if (isset($array_result['tot_paid']) && $array_result['tot_paid']) {
				// check event data payload to store
				if (is_array($tn_data)) {
					// set key
					$tn_data['amount_paid'] = (float) $array_result['tot_paid'];
				} elseif (is_object($tn_data)) {
					// set property
					$tn_data->amount_paid = (float) $array_result['tot_paid'];
				} elseif (!$tn_data) {
					// build an array (we add the payment name because we know there is no other transaction data)
					$tn_data = [
						'amount_paid' 	 => (float) $array_result['tot_paid'],
						'payment_method' => $payment['name'],
					];
				}
			}
			if ($tn_data && isset($array_result['tot_fees']) && $array_result['tot_fees']) {
				// check event data payload to store
				if (is_array($tn_data)) {
					// set key
					$tn_data['processing_fees'] = (float) $array_result['tot_fees'];
				} elseif (is_object($tn_data)) {
					// set property
					$tn_data->processing_fees = (float) $array_result['tot_fees'];
				}
			}
			if ($app->input->getBool('dd') && ($damage_deposit_payment['damagedep_gross'] ?? null)) {
				if ($tn_data) {
					$tn_data = (array) $tn_data;
				} else {
					$tn_data = [];
				}
				// damage deposit may be authorized without an amount paid returned
				$tn_data['damage_deposit'] = (float) $damage_deposit_payment['damagedep_gross'];
			}

			// Booking History
			VikBooking::getBookingHistoryInstance()->setBid($row['id'])->setExtraData($tn_data)->store('P' . ($row['paymcount'] > 0 ? 'N' : '0'), $payment['name']);

			// invoke VikChannelManager
			if (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "synch.vikbooking.php")) {
				require_once(VCM_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "synch.vikbooking.php");
				$vcm = new SynchVikBooking($row['id']);
				$vcm->setPushType('new')->sendRequest();
			}
			$vcmchanneldata = $session->get('vcmChannelData', '');
			if (!empty($vcmchanneldata)) {
				$session->set('vcmChannelData', '');
			}
			//end invoke VikChannelManager
			if (method_exists($obj, 'afterValidation')) {
				$obj->afterValidation(1);
			}
		} else {
			if (empty($array_result['skip_email'])) {
				$vbo_app = VikBooking::getVboApplication();
				$adsendermail = VikBooking::getSenderMail();
				$vbo_app->sendMail($adsendermail, $adsendermail, $recipient_mail, $adsendermail, JText::translate('VBPAYMENTNOTVER'), JText::translate('VBSERVRESP') . ":\n\n" . $array_result['log'], false);
			}
			if (!empty($array_result['log'])) {
				$q = "UPDATE `#__vikbooking_orders` SET `paymentlog`=".$dbo->quote($newpaymentlog)." WHERE `id`='" . $row['id'] . "';";
				$dbo->setQuery($q);
				$dbo->execute();
			}
			if (method_exists($obj, 'afterValidation')) {
				$obj->afterValidation(0);
			}
		}
	}

	public function currencyconverter()
	{
		if (!JSession::checkToken()) {
			// missing CSRF-proof token
			VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
		}

		$session = JFactory::getSession();
		$pprices = VikRequest::getVar('prices', array(0));
		$pfromsymbol = VikRequest::getString('fromsymbol', '', 'request');
		$ptocurrency = VikRequest::getString('tocurrency', '', 'request');
		$pfromcurrency = VikRequest::getString('fromcurrency', '', 'request');
		$default_cur = !empty($pfromcurrency) ? $pfromcurrency : VikBooking::getCurrencyName();
		$response = array();
		if (!empty($default_cur) && !empty($pprices) && count($pprices) > 0 && !empty($ptocurrency)) {
			require_once(VBO_SITE_PATH . DS . "helpers" . DS ."currencyconverter.php");
			if ($default_cur != $ptocurrency) {
				$format = VikBooking::getNumberFormatData();
				$converter = new VboCurrencyConverter($default_cur, $ptocurrency, $pprices, explode(':', $format));
				$exchanged = $converter->convert();
				if (count($exchanged) > 0) {
					$response = $exchanged;
					$session->set('vboLastCurrency', $ptocurrency);
				} else {
					$conv_error = $converter->getError();
					$response['error'] = !empty($conv_error) ? $conv_error : JText::translate('VBERRCURCONVINVALIDDATA');
				}
			} else {
				$session->set('vboLastCurrency', $ptocurrency);
				foreach ($pprices as $i => $price) {
					$response[$i]['symbol'] = $pfromsymbol;
					$response[$i]['price'] = $price;
				}
			}
		} else {
			$response['error'] = JText::translate('VBERRCURCONVNODATA');
		}
		if(array_key_exists('error', $response)) {
			$session->set('vboLastCurrency', $ptocurrency);
		}
		echo json_encode($response);
		exit;
	}

	public function signature()
	{
		VikRequest::setVar('view', 'signature');
		parent::display();
	}

	public function storesignature()
	{
		if (!JSession::checkToken()) {
			// missing CSRF-proof token
			VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
		}

		$sid = VikRequest::getString('sid', '', 'request');
		$ts = VikRequest::getString('ts', '', 'request');
		$psignature = VikRequest::getString('signature', '', 'request', VIKREQUEST_ALLOWRAW);
		$ppad_width = VikRequest::getInt('pad_width', '', 'request');
		$ppad_ratio = VikRequest::getInt('pad_ratio', '', 'request');
		$pitemid = VikRequest::getInt('Itemid', '', 'request');
		$ptmpl = VikRequest::getString('tmpl', '', 'request');
		$dbo = JFactory::getDBO();
		$mainframe = JFactory::getApplication();
		$q = "SELECT * FROM `#__vikbooking_orders` WHERE `ts`=" . $dbo->quote($ts) . " AND `sid`=" . $dbo->quote($sid) . " AND `status`='confirmed';";
		$dbo->setQuery($q);
		$dbo->execute();
		if ($dbo->getNumRows() < 1) {
			VikError::raiseWarning('', 'Booking not found');
			$mainframe->redirect('index.php');
			exit;
		}
		$row = $dbo->loadAssoc();
		$tonight = mktime(23, 59, 59, date('n'), date('j'), date('Y'));
		if ($tonight > $row['checkout']) {
			VikError::raiseWarning('', 'Check-out date is in the past');
			$mainframe->redirect('index.php');
			exit;
		}
		$customer = array();
		$q = "SELECT `c`.*,`co`.`idorder`,`co`.`signature`,`co`.`pax_data`,`co`.`comments` FROM `#__vikbooking_customers` AS `c` LEFT JOIN `#__vikbooking_customers_orders` `co` ON `c`.`id`=`co`.`idcustomer` WHERE `co`.`idorder`=".(int)$row['id'].";";
		$dbo->setQuery($q);
		$dbo->execute();
		if ($dbo->getNumRows() > 0) {
			$customer = $dbo->loadAssoc();
		}
		if (!(count($customer) > 0)) {
			VikError::raiseWarning('', 'Customer not found');
			$mainframe->redirect('index.php');
			exit;
		}
		//check if the signature has been submitted
		$signature_data = '';
		$cont_type = '';
		if (!empty($psignature)) {
			/**
			 * Implemented safe filtering of base64-encoded signature image
			 * to obtain content and file extension.
			 * 
			 * @since 	1.15.1 (J) - 1.5.4 (WP)
			 */
			if (preg_match("/^data:image\/(png|jpe?g|svg);base64,([A-Za-z0-9\/=+]+)$/", $psignature, $safe_match)) {
				$signature_data = base64_decode($safe_match[2]);
				$cont_type = $safe_match[1];
			}
		}
		$ret_link = JRoute::rewrite('index.php?option=com_vikbooking&task=signature&sid='.$row['sid'].'&ts='.$row['ts'].(!empty($pitemid) ? '&Itemid='.$pitemid : '').($ptmpl == 'component' ? '&tmpl=component' : ''), false);
		if (empty($signature_data)) {
			VikError::raiseWarning('', JText::translate('VBOSIGNATUREISEMPTY'));
			$mainframe->redirect($ret_link);
			exit;
		}
		//write file
		$sign_fname = $row['id'].'_'.$row['sid'].'_'.$customer['id'].'.'.$cont_type;
		$filepath = VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'idscans' . DIRECTORY_SEPARATOR . $sign_fname;
		$fp = fopen($filepath, 'w+');
		$bytes = fwrite($fp, $signature_data);
		fclose($fp);
		if ($bytes !== false && $bytes > 0) {
			//update the signature in the DB
			$q = "UPDATE `#__vikbooking_customers_orders` SET `signature`=".$dbo->quote($sign_fname)." WHERE `idorder`=".(int)$row['id'].";";
			$dbo->setQuery($q);
			$dbo->execute();
			$mainframe->enqueueMessage(JText::translate('VBOSIGNATURETHANKS'));
			//resize image for screens with high resolution
			if ($ppad_ratio > 1) {
				$new_width = floor(($ppad_width / 2));
				$creativik = new vikResizer();
				$creativik->proportionalImage($filepath, $filepath, $new_width, $new_width);
			}
			//
		} else {
			VikError::raiseWarning('', JText::translate('VBOERRSTORESIGNFILE'));
		}
		$mainframe->redirect($ret_link);
		exit;
	}

	public function validatepin()
	{
		if (!JSession::checkToken()) {
			// missing CSRF-proof token
			VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
		}

		$cpin = VikBooking::getCPinIstance();

		$ppin = VikRequest::getString('pin', '', 'request');

		$response = [];

		$customer = $cpin->getCustomerByPin($ppin);
		if ($customer) {
			$response = $customer;
			$response['success'] = 1;

			if ($cpin->getCustomerCoupon($customer)) {
				// set flag indicating that the customer has got dedicated discounts
				$response['has_discounts'] = 1;
			}
		}

		echo json_encode($response);
		exit;
	}

	public function docancelbooking()
	{
		if (!JSession::checkToken()) {
			// missing CSRF-proof token
			VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
		}

		$dbo = JFactory::getDbo();
		$mainframe = JFactory::getApplication();

		$psid = VikRequest::getString('sid', '', 'request');
		$pidorder = VikRequest::getString('idorder', '', 'request');

		if (!empty($psid) && !empty($pidorder)) {
			$q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=".intval($pidorder)." AND `sid`=".$dbo->quote($psid)." AND `status`='confirmed';";
			$dbo->setQuery($q);
			$order = $dbo->loadAssocList();
			if ($order) {
				$pemail = VikRequest::getString('email', '', 'request');
				$preason = VikRequest::getString('reason', '', 'request');
				if (!empty($pemail) && !empty($preason)) {
					$to = VikBooking::getAdminMail();
					if (strpos($to, ',') !== false) {
						$all_recipients = explode(',', $to);
						foreach ($all_recipients as $k => $v) {
							if (empty($v)) {
								unset($all_recipients[$k]);
							}
						}
						if (count($all_recipients) > 0) {
							$to = $all_recipients;
						}
					}
					//check if the booking can be cancelled
					$days_to_arrival = 0;
					$is_refundable = 0;
					$daysadv_refund_arr = array();
					$daysadv_refund = 0;
					$now_info = getdate();
					$checkin_info = getdate($order[0]['checkin']);
					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']));
						}
					}
					$tars = array();
					$is_package = !empty($order[0]['pkg']) ? true : false;
					$orderrooms = array();
					$q = "SELECT `or`.`idroom`,`or`.`adults`,`or`.`children`,`or`.`idtar`,`or`.`optionals`,`or`.`roomindex`,`or`.`pkg_id`,`or`.`pkg_name`,`or`.`cust_cost`,`or`.`cust_idiva`,`or`.`extracosts`,`or`.`room_cost`,`or`.`otarplan`,`r`.`id` AS `r_reference_id`,`r`.`name`,`r`.`img`,`r`.`idcarat`,`r`.`fromadult`,`r`.`toadult` FROM `#__vikbooking_ordersrooms` AS `or`,`#__vikbooking_rooms` AS `r` WHERE `or`.`idorder`='".$order[0]['id']."' AND `or`.`idroom`=`r`.`id` ORDER BY `or`.`id` ASC;";
					$dbo->setQuery($q);
					$orderrooms = $dbo->loadAssocList();
					if ($orderrooms) {
						foreach($orderrooms as $kor => $or) {
							$num = $kor + 1;
							if($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
								//package or custom cost set from the back-end
								continue;
							}
							$q = "SELECT `t`.*,`p`.`name`,`p`.`free_cancellation`,`p`.`canc_deadline`,`p`.`canc_policy` FROM `#__vikbooking_dispcost` AS `t` LEFT JOIN `#__vikbooking_prices` AS `p` ON `t`.`idprice`=`p`.`id` WHERE `t`.`id`='" . $or['idtar'] . "';";
							$dbo->setQuery($q);
							$tar = $dbo->loadAssocList();
							if ($tar) {
								$tars[$num] = $tar[0];
							}
						}
					}
					foreach ($tars as $num => $tar) {
						if ($tar['free_cancellation'] < 1) {
							//if at least one rate plan is non-refundable, the whole reservation cannot be cancelled
							$is_refundable = 0;
							$daysadv_refund_arr = array();
							break;
						}
						$is_refundable = 1;
						$daysadv_refund_arr[] = $tar['canc_deadline'];
					}
					//get the rate plan with the lowest cancellation deadline
					$daysadv_refund = count($daysadv_refund_arr) > 0 ? min($daysadv_refund_arr) : $daysadv_refund;
					$resmodcanc = VikBooking::getReservationModCanc();
					$resmodcanc = $days_to_arrival < 1 ? 0 : $resmodcanc;
					$resmodcancmin = VikBooking::getReservationModCancMin();
					$canc_allowed = ($resmodcanc > 1 && $resmodcanc != 2 && $is_refundable > 0 && $daysadv_refund <= $days_to_arrival && $days_to_arrival >= $resmodcancmin);
					if (!$canc_allowed) {
						VikError::raiseWarning('', JText::translate('VBOERRCANNOTCANCBOOK'));
						$mainframe->redirect(JRoute::rewrite("index.php?option=com_vikbooking&view=booking&sid=".$order[0]['sid']."&ts=".$order[0]['ts']."&Itemid=".VikRequest::getString('Itemid', '', 'request'), false));
						exit;
					}
					//make the cancellation in the db and update the administrator notes with the reason specified by the customer
					$new_adminotes = JText::translate('VBOBOOKCANCELLEDEMAILSUBJ').' ('.$pemail.")\n".$preason."\n\n".$order[0]['adminnotes'];
					$q = "UPDATE `#__vikbooking_orders` SET `status`='cancelled',`adminnotes`=".$dbo->quote($new_adminotes)." WHERE `id`=".(int)$order[0]['id'].";";
					$dbo->setQuery($q);
					$dbo->execute();
					$q = "SELECT * FROM `#__vikbooking_ordersbusy` WHERE `idorder`=".(int)$order[0]['id'].";";
					$dbo->setQuery($q);
					$ordbusy = $dbo->loadAssocList();
					if ($ordbusy) {
						foreach ($ordbusy as $ob) {
							$q = "DELETE FROM `#__vikbooking_busy` WHERE `id`=".(int)$ob['idbusy'].";";
							$dbo->setQuery($q);
							$dbo->execute();
						}
					}
					$q = "DELETE FROM `#__vikbooking_ordersbusy` WHERE `idorder`=".(int)$order[0]['id'].";";
					$dbo->setQuery($q);
					$dbo->execute();

					if ($order[0]['split_stay']) {
						// attempt to remove the transient record
						VBOFactory::getConfig()->remove('split_stay_' . $order[0]['id']);
					}

					// Booking History
					$history_obj = VikBooking::getBookingHistoryInstance()->setBid($order[0]['id']);
					$history_obj->store('CW', $preason);

					/**
					 * Check if the amount paid can be refunded.
					 * 
					 * @since 	1.14 (J) - 1.4.0 (WP)
					 */
					$admin_refund_error = '';
					$currencysymb = VikBooking::getCurrencySymb();
					$payment = VikBooking::getPayment($order[0]['idpayment']);
					$tn_driver = is_array($payment) ? $payment['file'] : null;

					// transaction data validation callback
					$tn_data_callback = function($data) use ($tn_driver) {
						return (is_object($data) && isset($data->driver) && basename($data->driver, '.php') == basename($tn_driver, '.php'));
					};
					// get previous transactions
					$prev_tn_data = $history_obj->getEventsWithData(array('P0', 'PN'), $tn_data_callback);

					if (is_array($prev_tn_data) && count($prev_tn_data) && $order[0]['totpaid'] > 0) {
						// previous transactions found and total paid > 0
						$refund_amount = $order[0]['totpaid'];

						// push refund information for the payment gateway
						$order[0]['total_to_refund'] = $refund_amount;
						$order[0]['transaction'] = $prev_tn_data;
						$order[0]['refund_reason'] = $preason;

						// push the transaction currency information
						$order[0]['transaction_currency'] = VikBooking::getCurrencyCodePp();

						/**
				         * Trigger event to allow third-party plugins to manipulate the transaction data.
				         * 
				         * @since 	1.18.5 (J) - 1.8.5 (WP)
				         */
				        VBOFactory::getPlatform()->getDispatcher()->trigger('onInitRefundTransaction', [&$order[0], &$payment['params']]);

						/**
						 * @wponly 	The payment gateway is loaded 
						 * 			through the apposite dispatcher.
						 */
						JLoader::import('adapter.payment.dispatcher');
						$obj = JPaymentDispatcher::getInstance('vikbooking', $payment['file'], $order[0], $payment['params']);

						// check if refund is supported by this gateway
						if (method_exists($obj, 'isRefundSupported') && $obj->isRefundSupported()) {
							// perform the refund transaction
							$array_result = $obj->refund();

							if ($array_result['verified'] != 1) {
								// refund failed
								$admin_refund_error .= "\nRefund transaction failed\n";
								// get the refund error message
								$admin_refund_error .= !empty($array_result['log']) && is_string($array_result['log']) ? $array_result['log'] : '';
							} else {
								// refund was successful

								/**
								 * The history event extra data will contain the "amount_paid" (refunded).
								 * 
								 * @since 	1.16.9 (J) - 1.6.9 (WP)
								 */
								if (!empty($array_result['tot_paid'])) {
									// overwrite the requested amount with the returned one
									$refund_amount = (float)$array_result['tot_paid'];
								}
								$history_obj->setExtraData([
									'amount_paid' => $refund_amount,
								]);

								// update total paid, total and refund columns for the booking
								$booking = new stdClass;
								$booking->id = $order[0]['id'];
								if ($order[0]['totpaid'] > 0) {
									$booking->totpaid = (float)($order[0]['totpaid'] - $refund_amount);
								}
								if ($order[0]['total'] > 0) {
									$booking->total = (float)($order[0]['total'] - $refund_amount);
								}
								$booking->refund = (float)$order[0]['refund'] + $refund_amount;
								// update record in db
								$dbo->updateObject('#__vikbooking_orders', $booking, 'id');

								// store the refund event
								$event_descr = [
									'(' . $payment['name'] . ')',
									$currencysymb . ' ' . VikBooking::numberFormat($refund_amount),
								];
								$history_obj->store('RF', implode("\n", $event_descr));
							}
						}
					}

					// invoke VikChannelManager
					if (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "synch.vikbooking.php")) {
						$vcm_obj = VikBooking::getVcmInvoker();
						$vcm_obj->setOids(array($order[0]['id']))->setSyncType('cancel');
						$vcm_obj->doSync();
					}
					// end invoke VikChannelManager

					//send email to the administrator
					$subject = JText::translate('VBOBOOKCANCELLEDEMAILSUBJ');
					// @wponly 	we do not need to pass the "best item id"
					$uri = VikBooking::externalroute("index.php?option=com_vikbooking&view=booking&sid=" . $order[0]['sid'] . "&ts=" . $order[0]['ts'], false);
					$msg = JText::sprintf('VBOBOOKCANCELLEDEMAILHEAD', $order[0]['id'], $uri) . "\n\n" . $preason . $admin_refund_error;
					$vbo_app = VikBooking::getVboApplication();
					$adsendermail = VikBooking::getSenderMail();
					$vbo_app->sendMail($adsendermail, $adsendermail, $to, $pemail, $subject, $msg, false);

					// SMS
					VikBooking::sendBookingSMS($order[0]['id']);

					// send cancellation email notification to guest
					VikBooking::sendBookingEmail($order[0]['id'], ['guest']);

					// go back to the booking details page to show the new status
					$mainframe->enqueueMessage(JText::translate('VBOBOOKCANCELLEDRESP'));
					$mainframe->redirect(JRoute::rewrite("index.php?option=com_vikbooking&view=booking&sid=".$order[0]['sid']."&ts=".$order[0]['ts']."&Itemid=".VikRequest::getString('Itemid', '', 'request'), false));
				} else {
					VikError::raiseWarning('', JText::translate('VBOERRMISSDATA'));
					$mainframe->redirect(JRoute::rewrite("index.php?option=com_vikbooking&view=booking&sid=".$order[0]['sid']."&ts=".$order[0]['ts']."&Itemid=".VikRequest::getString('Itemid', '', 'request'), false));
				}
			} else {
				$mainframe->redirect("index.php");
			}
		} else {
			$mainframe->redirect("index.php");
		}
	}

	public function cancelmodification()
	{
		$psid = VikRequest::getString('sid', '', 'request');
		$pidorder = VikRequest::getString('id', '', 'request');
		$dbo = JFactory::getDBO();
		$session = JFactory::getSession();
		$mainframe = JFactory::getApplication();
		if (!empty($psid) && !empty($pidorder)) {
			$q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=".intval($pidorder)." AND `sid`=".$dbo->quote($psid)." AND `status`='confirmed';";
			$dbo->setQuery($q);
			$dbo->execute();
			if ($dbo->getNumRows() == 1) {
				$order = $dbo->loadAssocList();
				//unset the session value and redirect
				$session->set('vboModBooking', '');
				$mainframe->redirect(JRoute::rewrite("index.php?option=com_vikbooking&view=booking&sid=".$order[0]['sid']."&ts=".$order[0]['ts'], false));
			} else {
				$mainframe->redirect("index.php");
			}
		} else {
			$mainframe->redirect("index.php");
		}
	}
	
	public function tac_av_l()
	{
		require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'tac.vikbooking.php');
		
		//Channel Rates Module
		$pvbomodule = VikRequest::getInt('vbomodule', 0, 'request');
		$pshow_tax = VikRequest::getInt('show_tax', 0, 'request');
		$pdef_rplan = VikRequest::getInt('def_rplan', 0, 'request');
		$pchannels_sel = VikRequest::getVar('channels_sel', array());
		$pcheckin = VikRequest::getString('checkin', '', 'request');
		$pcheckout = VikRequest::getString('checkout', '', 'request');
		if ($pvbomodule > 0 && !empty($pcheckin) && !empty($pcheckout)) {
			//this is an ajax request, probably made by the module Vik Booking Channel Rates
			//we need to prepare some variables before calling the method.
			$start_date = date('Y-m-d', VikBooking::getDateTimestamp($pcheckin, 12, 0));
			$end_date = date('Y-m-d', VikBooking::getDateTimestamp($pcheckout, 10, 0));
			//set (only some) request variables (the rest is sent via Ajax)
			VikRequest::setVar('e4jauth', md5('vbo.e4j.vbo'));
			VikRequest::setVar('req_type', 'hotel_availability');
			VikRequest::setVar('start_date', $start_date);
			VikRequest::setVar('end_date', $end_date);
			//make call to get the result
			TACVBO::$getArray = true;
			$website_rates = TACVBO::tac_av_l([
				// always force adults and children to be injected as arguments to avoid request conflicts
				'adults'   => VikRequest::getVar('adults', array()),
				'children' => VikRequest::getVar('children', array()) ?: [0],
			]);
			//validate response
			if (!is_array($website_rates)) {
				//error returned
				echo json_encode(array('e4j.error' => $website_rates));
				exit;
			}
			if (is_array($website_rates) && isset($website_rates['e4j.error'])) {
				//another type of error returned
				echo json_encode($website_rates);
				exit;
			}
			if (is_array($website_rates) && !(count($website_rates) > 0)) {
				//empty response
				echo json_encode(array('e4j.error' => 'empty response'));
				exit;
			}
			//get the list of channels connected, filtered by ID
			$channels_map = VikBooking::getChannelsMap($pchannels_sel);
			//get the array with the lowest and preferred room rate
			$best_room_rate = VikBooking::getBestRoomRate($website_rates, $pdef_rplan);
			//get the charge/discount value for the OTAs rates from the Bulk Rates Cache of VCM
			$otas_rates_val = VikBooking::getOtasRatesVal($best_room_rate, true);

			$otas_rmod = '';
			$otas_rmodpcent = 0;
			$otas_rmodval = 0;
			$otas_rmod_channels = array();
			if (!empty($otas_rates_val)) {
				if (is_array($otas_rates_val)) {
					$otas_rmod_channels = $otas_rates_val;
					$use_rates_val = $otas_rates_val[0];
				} else {
					// string
					$use_rates_val = $otas_rates_val;
				}
				$otas_rmod = substr($use_rates_val, 0, 1); // + or - (charge or discount)
				$otas_rmodpcent = substr($use_rates_val, -1) == '%' ? 1 : 0;
				$otas_rmodval = (float)($otas_rmodpcent > 0 ? substr($use_rates_val, 1, (strlen($use_rates_val) - 2)) : substr($use_rates_val, 1, (strlen($use_rates_val) - 1)));
			}
			if (!count($best_room_rate)) {
				// nothing to parse
				echo json_encode(array('e4j.error' => 'no rates'));
				exit;
			}
			// build the response
			$final_cost = $pshow_tax > 0 ? ($best_room_rate['cost'] + $best_room_rate['taxes']) : $best_room_rate['cost'];
			$rates_resp = array(
				'website' => VikBooking::numberFormat($final_cost)
			);
			if (count($channels_map)) {
				$rates_resp['channels'] = array();
			}
			foreach ($channels_map as $ch) {
				$ch_final_cost = $final_cost;

				/**
				 * Check if an alteration for this channel has been specified.
				 * 
				 * @since 	1.15.0 (J) - 1.5.0 (WP)
				 */
				$use_otas_rmod = $otas_rmod;
				$use_otas_rmodpcent = $otas_rmodpcent;
				$use_otas_rmodval = $otas_rmodval;
				if (is_array($otas_rmod_channels) && isset($otas_rmod_channels[$ch['id']])) {
					$use_rates_val = $otas_rmod_channels[$ch['id']];
					$use_otas_rmod = substr($use_rates_val, 0, 1); //+ or - (charge or discount)
					$use_otas_rmodpcent = substr($use_rates_val, -1) == '%' ? 1 : 0;
					$use_otas_rmodval = (float)($use_otas_rmodpcent > 0 ? substr($use_rates_val, 1, (strlen($use_rates_val) - 2)) : substr($use_rates_val, 1, (strlen($use_rates_val) - 1)));
				}

				if (!empty($use_otas_rmod)) {
					if ($use_otas_rmod == '+') {
						// charge
						if ($use_otas_rmodpcent > 0) {
							// percentage
							$ch_final_cost = $ch_final_cost * (100 + $use_otas_rmodval) / 100;
						} else {
							// absolute
							$ch_final_cost += $use_otas_rmodval * (!empty($best_room_rate['days']) ? $best_room_rate['days'] : 1);
						}
					} else {
						// discount (must be a fool)
						if ($use_otas_rmodpcent > 0) {
							// percentage
							$ch_final_cost = $ch_final_cost / (($use_otas_rmodval / 100) + 1);
						} else {
							// absolute
							$ch_final_cost -= $use_otas_rmodval * (!empty($best_room_rate['days']) ? $best_room_rate['days'] : 1);
						}
					}
				}

				$rates_resp['channels'][$ch['id']] = VikBooking::numberFormat($ch_final_cost);
			}
			// output the response
			echo json_encode($rates_resp);
			exit;
		}

		// proceed with the standard request (that will exit the process)
		TACVBO::tac_av_l();
	}

	/**
	 * Front-end authentication for the operators
	 * through their authentication code.
	 *
	 * @since 	1.11
	 */
	public function operatorlogin()
	{
		/**
		 * Secure the login form token.
		 */
		if (!JSession::checkToken()) {
			// missing CSRF-proof token
			throw new Exception('The security token did not match.', 403);
		}

		$app = JFactory::getApplication();
		$pauthcode = VikRequest::getString('authcode', '', 'request');
		$pitemid = VikRequest::getInt('Itemid', '', 'request');
		/**
		 * We add "&auth=1" to the query string just to avoid caching for a redirect to the same login page URI.
		 * 
		 * @since 	September 9th 2020
		 */
		$goto = JRoute::rewrite('index.php?option=com_vikbooking&view=operators&auth=1'.(!empty($pitemid) ? '&Itemid='.$pitemid : ''), false);

		if (empty($pauthcode) || !VikBooking::getOperatorInstance()->authOperator($pauthcode)) {
			// print warning message
			VikError::raiseWarning('', JText::translate('VBOOPERINVAUTHCODE'));
		}

		$app->redirect($goto);
	}

	/**
	 * Front-end logout for the operators.
	 *
	 * @since 	1.11
	 */
	public function operatorlogout()
	{
		$app = JFactory::getApplication();
		$pitemid = VikRequest::getInt('Itemid', '', 'request');
		$goto = JRoute::rewrite('index.php?option=com_vikbooking&view=operators'.(!empty($pitemid) ? '&Itemid='.$pitemid : ''), false);

		VikBooking::getOperatorInstance()->logoutOperator();

		$app->redirect($goto);
	}

	/**
	 * Front-end Pre Check-in submit of the guests details.
	 *
	 * @since 	1.12
	 */
	public function storeprecheckin()
	{
		if (!JSession::checkToken()) {
			// missing CSRF-proof token
			VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
		}

		$dbo 	 = JFactory::getDbo();
		$app 	 = JFactory::getApplication();

		$sid 	 = $app->input->getString('sid', '');
		$ts 	 = $app->input->getString('ts', '');
		$pguests = $app->input->get('guests', [], 'array');
		$pitemid = $app->input->getInt('Itemid', 0);

		$q = "SELECT `o`.* FROM `#__vikbooking_orders` AS `o` WHERE (`o`.`sid`=" . $dbo->quote($sid) . " OR `o`.`idorderota`=" . $dbo->quote($sid) . ") AND `o`.`ts`=" . $dbo->quote($ts) . " AND `o`.`status`='confirmed';";
		$dbo->setQuery($q);
		$order = $dbo->loadAssoc();
		if (!$order) {
			throw new Exception('Booking not found', 404);
		}

		$q = "SELECT `or`.`idroom`,`or`.`adults`,`or`.`children`,`or`.`idtar`,`or`.`optionals`,`or`.`childrenage`,`or`.`t_first_name`,`or`.`t_last_name`,`or`.`roomindex`,`or`.`pkg_id`,`or`.`pkg_name`,`or`.`cust_cost`,`or`.`cust_idiva`,`or`.`extracosts`,`or`.`room_cost`,`or`.`otarplan`,`r`.`id` AS `r_reference_id`,`r`.`name`,`r`.`img`,`r`.`idcarat`,`r`.`fromadult`,`r`.`toadult` FROM `#__vikbooking_ordersrooms` AS `or`,`#__vikbooking_rooms` AS `r` WHERE `or`.`idorder`=".(int)$order['id']." AND `or`.`idroom`=`r`.`id` ORDER BY `or`.`id` ASC;";
		$dbo->setQuery($q);
		$orderrooms = $dbo->loadAssocList();
		if (!$orderrooms) {
			throw new Exception('No rooms found', 404);
		}

		// access the customer record
		$customer = VikBooking::getCPinInstance()->getCustomerFromBooking($order['id']);

		$q = "SELECT * FROM `#__vikbooking_customers_orders` WHERE `idorder`=".(int)$order['id'].";";
		$dbo->setQuery($q);
		$custorder = $dbo->loadAssoc();
		if (!$custorder) {
			throw new Exception('No customer found', 404);
		}

		// booking details page
		$goto = JRoute::rewrite('index.php?option=com_vikbooking&view=booking&sid=' . $order['sid'] . '&ts=' . $order['ts'] . (!empty($pitemid) ? '&Itemid=' . $pitemid : ''), false);
		if (empty($order['sid']) && !empty($order['idorderota'])) {
			// booking details page for OTA bookings
			$goto = JRoute::rewrite('index.php?option=com_vikbooking&view=booking&sid=' . $order['idorderota'] . '&ts=' . $order['ts'] . (!empty($pitemid) ? '&Itemid=' . $pitemid : ''), false);
		}

		// make sure pre-checkin is allowed
		$precheckin = VikBooking::precheckinEnabled();
		if ($precheckin) {
			// make sure the limit of days in advance is reflected
			$precheckin_mind = VikBooking::precheckinMinOffset();
			if ($precheckin_mind < 0) {
				// validation made prior to check-out date and time
				$precheckin = time() <= strtotime("{$precheckin_mind} days 23:59:59", $order['checkout']);
			} else {
				// classic validation prior to check-in date and time
				$precheckin_lim_ts = strtotime("+{$precheckin_mind} days 00:00:00");
				$precheckin = ($precheckin_lim_ts <= $order['checkin'] || ($precheckin_mind === 1 && time() <= $order['checkin']));
			}
		}
		if (!$precheckin) {
			// raise error and redirect in case of website or OTA booking
			VikError::raiseWarning('', 'Pre-checkin not allowed at this time');
			$app->redirect($goto);
			exit;
		}

		// build guest details
		$guests_details = array();

		// list of keys for the guests details collected via front-end
		$front_keys = array();
		
		foreach ($pguests as $ind => $adults) {
			foreach ($adults as $aduind => $details) {
				foreach ($details as $detkey => $detval) {
					if (!in_array($detkey, $front_keys)) {
						// push the key of the guest details for later comparison
						array_push($front_keys, $detkey);
					}
					if (strlen($detval)) {
						// push value only if not empty
						if (!isset($guests_details[$ind])) {
							$guests_details[$ind] = array();
						}
						if (!isset($guests_details[$ind][$aduind])) {
							$guests_details[$ind][$aduind] = array();
						}
						$guests_details[$ind][$aduind][$detkey] = $detval;
					}
				}
			}
		}

		/**
		 * Compare the current data collected to the back-end pax_data in case there are some
		 * fields dedicated to just the back-end for the admins (like extra_notes), and merge.
		 */
		$curpaxdata = json_decode($custorder['pax_data'], true);
		if (is_array($curpaxdata) && count($curpaxdata)) {
			foreach ($curpaxdata as $ind => $adults) {
				if (!isset($guests_details[$ind])) {
					// current pax data include a room not present, set it to not lose it
					$guests_details[$ind] = $adults;
				}
				foreach ($adults as $aduind => $details) {
					if (!isset($guests_details[$ind][$aduind])) {
						// current pax data include a guest not present, set it to not lose it
						$guests_details[$ind][$aduind] = $details;
					}
					foreach ($details as $detkey => $detval) {
						if (!in_array($detkey, $front_keys)) {
							// merge this key probably reserved to the back-end
							$guests_details[$ind][$aduind][$detkey] = $detval;
						}
					}
				}
			}
		}
		
		// update checkin information
		$q = "UPDATE `#__vikbooking_customers_orders` SET `pax_data`=".$dbo->quote(json_encode($guests_details))." WHERE `id`=".(int)$custorder['id'].";";
		$dbo->setQuery($q);
		$dbo->execute();

		// Booking History
		VikBooking::getBookingHistoryInstance()->setBid($order['id'])->store('PC');

		/**
		 * Invoke the callback on the pax registration driver.
		 * 
		 * @since 	1.17.5 (J) - 1.7.5 (WP)
		 */
		VBOCheckinPax::callbackPrecheckinDataStored(
			VBOFactory::getConfig()->getString('checkindata', 'basic'),
			(array) $guests_details,
			(array) $order,
			(array) $customer
		);

		// print success message and redirect
		$app->enqueueMessage(JText::translate('VBOSUBMITPRECHECKINTNKS'));
		$app->redirect($goto);
	}

	/**
	 * Upsell extra services/options.
	 *
	 * @since 	1.13 (J) - 1.3.0 (WP)
	 */
	public function upsellextras()
	{
		$dbo 	 = JFactory::getDbo();
		$app 	 = JFactory::getApplication();
		$sid 	 = VikRequest::getString('sid', '', 'request');
		$ts 	 = VikRequest::getString('ts', '', 'request');
		$pitemid = VikRequest::getInt('Itemid', 0, 'request');
		$paddopt = VikRequest::getVar('addopt', array());

		if (!$paddopt) {
			throw new Exception('No extra services selected', 404);
		}

		// find the involved reservation, direct or OTA
		$q = "SELECT `o`.* FROM `#__vikbooking_orders` AS `o` WHERE (`o`.`sid`=" . $dbo->quote($sid) . " OR `o`.`idorderota`=" . $dbo->quote($sid) . ") AND `o`.`ts`=" . $dbo->quote($ts) . ";";
		$dbo->setQuery($q);
		$order = $dbo->loadAssoc();
		if (!$order) {
			throw new Exception('Booking not found', 404);
		}

		// obtain the involved rooms (include room "params")
		$q = "SELECT `or`.*,`r`.`name` AS `room_name`,`r`.`params` AS `room_params` FROM `#__vikbooking_ordersrooms` AS `or`,`#__vikbooking_rooms` AS `r` WHERE `or`.`idorder`=".(int)$order['id']." AND `or`.`idroom`=`r`.`id` ORDER BY `or`.`id` ASC;";
		$dbo->setQuery($q);
		$orderrooms = $dbo->loadAssocList();
		if (!$orderrooms) {
			throw new Exception('No rooms found', 404);
		}

		// availability helper
		$av_helper = VikBooking::getAvailabilityInstance();

		// room stay dates in case of split stay
		$room_stay_dates = [];
		if ($order['split_stay']) {
			if ($order['status'] == 'confirmed') {
				$room_stay_dates = $av_helper->loadSplitStayBusyRecords($order['id']);
			} else {
				$room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $order['id'], []);
			}
		}

		// load all valid and existing options as a security measure
		$alloptions = [];
		$q = "SELECT * FROM `#__vikbooking_optionals`;";
		$dbo->setQuery($q);
		$records = $dbo->loadAssocList();
		if (!$records) {
			throw new Exception('No options found', 404);
		}
		foreach ($records as $v) {
			$alloptions[$v['id']] = $v;
		}

		/**
		 * Custom check-in/out times due to late check-out/early check-in options or custom listing settings.
		 * 
		 * @since 	1.17.2 (J) - 1.7.2 (WP)
		 * @since 	1.18.3 (J) - 1.8.3 (WP) added support to check-in/out times at listing-level.
		 */
		$custom_checkinout = [];
		if (count($orderrooms) === 1) {
			$listing_custom_checkin = VikBooking::getRoomParam('checkin', ($orderrooms[(key($orderrooms))]['room_params'] ?? ''));
			$listing_custom_checkout = VikBooking::getRoomParam('checkout', ($orderrooms[(key($orderrooms))]['room_params'] ?? ''));
			if ($listing_custom_checkin) {
				// set listing-level check-in time in seconds
				$listing_custom_checkin_parts = explode(':', $listing_custom_checkin);
				$listing_custom_checkin = (intval($listing_custom_checkin_parts[0]) * 3600) + (intval($listing_custom_checkin_parts[1]) * 60);
			}
			if ($listing_custom_checkout) {
				// set listing-level check-out time in seconds
				$listing_custom_checkout_parts = explode(':', $listing_custom_checkout);
				$listing_custom_checkout = (intval($listing_custom_checkout_parts[0]) * 3600) + (intval($listing_custom_checkout_parts[1]) * 60);
			}
			if ($listing_custom_checkin || $listing_custom_checkout) {
				// set listing-level check-in/out times in seconds
				$custom_checkinout = [
					(int) $listing_custom_checkin,
					(int) $listing_custom_checkout,
				];
			}
		}

		/**
		 * Gather the damage deposit options to be paid separately.
		 * 
		 * @since 	1.18.6 (J) - 1.8.6 (WP)
		 */
		$separate_dd_options = [];

		// build the extras booked
		$extras_booked = [];
		foreach ($orderrooms as $kor => $or) {
			if (!isset($paddopt[$kor]) || !$paddopt[$kor]) {
				continue;
			}

			// determine proper nights of stay
			$room_stay_nights = $order['days'];
			if ($order['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
				$room_stay_checkin  = !empty($room_stay_dates[$kor]['checkin_ts']) ? $room_stay_dates[$kor]['checkin_ts'] : $room_stay_dates[$kor]['checkin'];
				$room_stay_checkout = !empty($room_stay_dates[$kor]['checkout_ts']) ? $room_stay_dates[$kor]['checkout_ts'] : $room_stay_dates[$kor]['checkout'];
				$room_stay_nights 	= $av_helper->countNightsOfStay($room_stay_checkin, $room_stay_checkout);
			}

			$extraoptstr = '';
			foreach ($paddopt[$kor] as $optid => $quant) {
				if (strpos($or['optionals'], $optid . ':') === 0 || strpos($or['optionals'], ';' . $optid . ':') > 0) {
					// this option has already been booked, skip it
					continue;
				}
				if (!isset($alloptions[$optid])) {
					// this option ID does not exist, skip it
					continue;
				}
				$extraoptstr .= $optid . ':' . (int)$quant . ';';

				// option params
				$opt_params = !empty($alloptions[$optid]['oparams']) ? (array) json_decode($alloptions[$optid]['oparams'], true) : [];

				/**
				 * Custom check-in/out times due to late check-out/early check-in options.
				 * 
				 * @since 	1.17.2 (J) - 1.7.2 (WP)
				 */
				if (($opt_params['custom_checkinout'] ?? 0) && (($opt_params['set_checkin'] ?? 0) || ($opt_params['set_checkout'] ?? 0))) {
					$custom_checkinout = [
						($opt_params['set_checkin'] ?? 0),
						($opt_params['set_checkout'] ?? 0),
					];
				}

				/**
				 * Check if this is a damage deposit with a separate payment window.
				 * 
				 * @since 	1.18.6 (J) - 1.8.6 (WP)
				 */
				if (($opt_params['damagedep'] ?? 0) && !empty($opt_params['damagedep_settings']['paywhen'])) {
					// damage deposit option with separate payment defined
					$future_payable_dd = true;
					if (!empty($opt_params['damagedep_settings']['bmaxlos']) && ($order['days'] ?? 1) > $opt_params['damagedep_settings']['bmaxlos']) {
						// maximum nights of stay validation failed
						$future_payable_dd = false;
					} elseif (empty($opt_params['damagedep_settings']['payid'])) {
						// separate payment method ID validation failed
						$future_payable_dd = false;
					}
					if ($future_payable_dd === true) {
						// push damage deposit option ID to be paid separately
						$separate_dd_options[] = (int) $optid;
					}
				}

				// push option booked
				$extras_booked[] = [
					'id'        => $optid,
					'idroom'    => $or['idroom'],
					'name'      => $alloptions[$optid]['name'],
					'quant'     => $quant,
					'room_cost' => (!empty($or['cust_cost']) ? $or['cust_cost'] : $or['room_cost']),
					'room_name' => $or['room_name'],
					'optcost'   => $alloptions[$optid]['cost'],
					'adults'    => $or['adults'],
					'children'  => $or['children'],
					'nights'    => $room_stay_nights,
				];
			}

			// update options for this room record
			$newoptstr = $or['optionals'] . $extraoptstr;
			$q = "UPDATE `#__vikbooking_ordersrooms` SET `optionals`=" . $dbo->quote($newoptstr) . " WHERE `id`={$or['id']};";
			$dbo->setQuery($q);
			$dbo->execute();
		}

		// increase booking total amount and build event log for the history
		$currency  = VikBooking::getCurrencySymb();
		$totrooms  = count($orderrooms);
		$increase  = 0;
		$future_dd = 0;
		$add_tax   = 0;
		$extraslog = [];
		foreach ($extras_booked as $extra) {
			$o = $alloptions[(int)$extra['id']];
			if ((int)$o['pcentroom']) {
				// make sure we have a cost for the room, or we should skip this type of option for "incomplete" bookings
				if (empty($extra['room_cost'])) {
					continue;
				}
				$o['cost'] = ($extra['room_cost'] * $o['cost'] / 100);
			}
			$optcost = intval($o['perday']) == 1 ? ($o['cost'] * $extra['nights']) : $o['cost'];
			if (!empty($o['maxprice']) && $o['maxprice'] > 0 && $optcost > $o['maxprice']) {
				$optcost = $o['maxprice'];
			}
			if ($o['perperson'] == 1) {
				$optcost = $optcost * $extra['adults'];
			}
			$optcost *= $extra['quant'];

			/**
			 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
			 * 
			 * @since 	1.17.7 (J) - 1.7.7 (WP)
			 */
			$custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$optcost, &$o, $order, $extra]);
			if ($custom_calculation) {
				$optcost = (float) $custom_calculation[0];
			}

			$floatoptprice = VikBooking::sayOptionalsPlusIva($optcost, $o['idiva']);
			$netoptprice   = VikBooking::sayOptionalsMinusIva($optcost, $o['idiva']);
			$increase += $floatoptprice;
			$add_tax  += $floatoptprice - $netoptprice;
			array_push($extraslog, ($totrooms > 1 ? $extra['room_name'] . ': ' : '') . $extra['name'] . ($extra['quant'] > 1 ? ' (x' . $extra['quant'] . ')' : '') . ' ' . $currency . ' ' . VikBooking::numberFormat($floatoptprice));

			if (in_array((int) $extra['id'], $separate_dd_options)) {
				// this is a damage deposit option that will be paid separately
				$future_dd += $floatoptprice;
			}
		}

		$newtotbooking = $order['total'] + $increase;
		$new_tot_taxes = $order['tot_taxes'] + $add_tax;

		/**
		 * Important: the 'paymcount' should be increased only if the status is
		 * "confirmed" or no rooms may be occupied when receiving a payment.
		 */
		$q = $dbo->getQuery(true)
			->update($dbo->qn('#__vikbooking_orders'))
			->set($dbo->qn('total') . ' = ' . $dbo->q($newtotbooking))
			->set($dbo->qn('paymcount') . ' = ' . ($order['status'] == 'confirmed' && (int)$order['paymcount'] < 1 ? '1' : $order['paymcount']))
			->set($dbo->qn('tot_taxes') . ' = ' . $dbo->q($new_tot_taxes))
			->set($dbo->qn('payable') . ' = ' . $dbo->q(((float)$order['payable'] + $increase - $future_dd)))
			->where($dbo->qn('id') . ' = ' . (int) $order['id']);

		/**
		 * Custom check-in/out times due to late check-out/early check-in options.
		 * 
		 * @since 	1.17.2 (J) - 1.7.2 (WP)
		 */
		if ($custom_checkinout) {
			$checkin_info = getdate($order['checkin']);
			$checkout_info = getdate($order['checkout']);
			// overwrite check-in and/or check-out timestamp(s)
			if ($custom_checkinout[0] >= 3600) {
				// overwrite check-in timestamp
				$time_hours = floor($custom_checkinout[0] / 3600);
				$time_minutes = floor(($custom_checkinout[0] - ($time_hours * 3600)) / 60);
				$new_booking_checkin = mktime($time_hours, $time_minutes, 0, $checkin_info['mon'], $checkin_info['mday'], $checkin_info['year']);
				// update db record field
				$q->set($dbo->qn('checkin') . ' = ' . $dbo->q($new_booking_checkin));
			}
			if ($custom_checkinout[1] >= 3600) {
				// overwrite check-out timestamp
				$time_hours = floor($custom_checkinout[1] / 3600);
				$time_minutes = floor(($custom_checkinout[1] - ($time_hours * 3600)) / 60);
				$new_booking_checkout = mktime($time_hours, $time_minutes, 0, $checkout_info['mon'], $checkout_info['mday'], $checkout_info['year']);
				// update db record field
				$q->set($dbo->qn('checkout') . ' = ' . $dbo->q($new_booking_checkout));
			}
		}

		// update booking record on db
		$dbo->setQuery($q);
		$dbo->execute();

		// Booking History
		VikBooking::getBookingHistoryInstance($order['id'])->store('UE', implode("\n", $extraslog));

		/**
		 * Trigger event to allow third-party plugins to choose whether upselling notifications should be sent.
		 * 
		 * @since 	1.17.2 (J) - 1.7.2 (WP)
		 */
		$send_notifications = true;
		$should_send = VBOFactory::getPlatform()->getDispatcher()->filter('onUpsellingReceivedShouldSendNotifications', [$order]);
		if (is_array($should_send) && in_array(false, $should_send, true)) {
			$send_notifications = false;
		}

		if ($send_notifications) {
			// send email notification to guest and admin
			VikBooking::sendBookingEmail($order['id'], array('guest', 'admin'));
		}

		$goto = JRoute::rewrite('index.php?option=com_vikbooking&view=booking&sid=' . (empty($order['sid']) && !empty($order['idorderota']) ? $order['idorderota'] : $order['sid']) . '&ts=' . $order['ts'] . (!empty($pitemid) ? '&Itemid=' . $pitemid : ''), false);
		$app->enqueueMessage(JText::translate('VBOUPSELLRESULTOK'));
		$app->redirect($goto);
		$app->close();
	}

	/**
	 * Submits a new review.
	 *
	 * @since 	1.3.0
	 */
	public function sendreview()
	{
		$dbo 	 	= JFactory::getDbo();
		$app 	 	= JFactory::getApplication();
		$vbo_tn 	= VikBooking::getTranslator();
		$sid 	 	= VikRequest::getString('sid', '', 'request');
		$ts 	 	= VikRequest::getString('ts', '', 'request');
		$ratingmess = VikRequest::getString('ratingmess', '', 'request');
		$rating 	= VikRequest::getVar('rating', array(), 'request', 'array');
		$pitemid 	= VikRequest::getInt('Itemid', 0, 'request');

		$q = "SELECT `o`.* FROM `#__vikbooking_orders` AS `o` WHERE (`o`.`sid`=" . $dbo->quote($sid) . " OR `o`.`idorderota`=" . $dbo->quote($sid) . ") AND `o`.`ts`=" . $dbo->quote($ts) . " AND `o`.`status`='confirmed';";
		$dbo->setQuery($q);
		$dbo->execute();
		if (!$dbo->getNumRows()) {
			throw new Exception('Booking not found', 404);
		}
		$order = $dbo->loadAssoc();

		// make sure a review can be left for this booking
		if (!VikBooking::canBookingBeReviewed($order)) {
			throw new Exception('Cannot leave a review at this time', 403);
		}

		$orderrooms = array();
		$q = "SELECT `or`.`idroom`,`or`.`adults`,`or`.`children`,`or`.`idtar`,`or`.`optionals`,`or`.`childrenage`,`or`.`t_first_name`,`or`.`t_last_name`,`or`.`roomindex`,`or`.`pkg_id`,`or`.`pkg_name`,`or`.`cust_cost`,`or`.`cust_idiva`,`or`.`extracosts`,`or`.`room_cost`,`or`.`otarplan`,`r`.`id` AS `r_reference_id`,`r`.`name`,`r`.`img`,`r`.`idcarat`,`r`.`fromadult`,`r`.`toadult` FROM `#__vikbooking_ordersrooms` AS `or`,`#__vikbooking_rooms` AS `r` WHERE `or`.`idorder`=".(int)$order['id']." AND `or`.`idroom`=`r`.`id` ORDER BY `or`.`id` ASC;";
		$dbo->setQuery($q);
		$dbo->execute();
		if (!$dbo->getNumRows()) {
			throw new Exception('No rooms found', 404);
		}
		$orderrooms = $dbo->loadAssocList();

		// get customer information
		$customer = VikBooking::getCPinIstance()->getCustomerFromBooking($order['id']);

		// booking details page
		$goto = JRoute::rewrite('index.php?option=com_vikbooking&view=booking&sid=' . $order['sid'] . '&ts=' . $order['ts'] . (!empty($pitemid) ? '&Itemid=' . $pitemid : ''), false);
		if (empty($order['sid']) && !empty($order['idorderota'])) {
			// booking details page for OTA bookings
			$goto = JRoute::rewrite('index.php?option=com_vikbooking&view=booking&sid=' . $order['idorderota'] . '&ts=' . $order['ts'] . (!empty($pitemid) ? '&Itemid=' . $pitemid : ''), false);
		}

		// reviews settings
		$gr_approval = VikBooking::guestReviewsApproval();
		$gr_type 	 = VikBooking::guestReviewsType();
		$gr_services = VikBooking::guestReviewsServices();
		$rawservices = $gr_services;
		$vbo_tn->translateContents($gr_services, '#__vikbooking_greview_service');

		// make sure all ratings are not empty
		if ($gr_type == 'global') {
			if (empty($rating[0]) || intval($rating[0]) < 1 || intval($rating[0]) > 5) {
				// no or invalid single-rating received
				VikError::raiseWarning('', 'Please rate your experience to leave a review.');
				$app->redirect($goto);
				exit;
			}
		} else {
			if (count($gr_services) != count($rating) || !count($rating)) {
				// something is missing
				VikError::raiseWarning('', 'Please rate your experience for all services');
				$app->redirect($goto);
				exit;
			}
			// make sure all ratings are valid
			foreach ($rating as $k => $score) {
				if (empty($score) || intval($score) < 1 || intval($score) > 5) {
					// no or invalid rating received for this service
					VikError::raiseWarning('', 'Please rate your experience to leave a review (missing ' . (isset($gr_services[$k]) ? $gr_services[$k]['service_name'] : '-----') . ').');
					$app->redirect($goto);
					exit;
				}
			}
		}

		// average review score (in base 10) and services map
		$avg_score = 0;
		$serv_scores = array();

		// gather the information
		foreach ($rating as $k => $score) {
			// rating in base 10
			$score = ((int)$score * 2);
			//
			$avg_score += $score;
			if ($gr_type == 'service' && isset($gr_services[$k]) && !empty($gr_services[$k]['service_name'])) {
				$skey = $gr_services[$k]['service_name'];
				$serv_scores[$skey] = $score;
			}
		}
		// this will be the review_score
		$avg_score = round(($avg_score / count($rating)), 2);

		// build review content object
		$review_content = new stdClass;

		// creation date
		$review_content->created_timestamp = date('Y-m-d H:i:s');

		// scoring per service (if any)
		$review_content->scoring = new stdClass;
		if (count($serv_scores)) {
			$counter = 0;
			foreach ($serv_scores as $snametranx => $servscore) {
				// we build the object with the original names of the services, as they could have been translated
				$origskey = $rawservices[$counter]['service_name'];
				$review_content->scoring->{$origskey} = $servscore;
				$counter++;
			}
		}
		// scoring total value ("review_score" is a protected key) is added no matter of the review type (service/global)
		$review_content->scoring->review_score = $avg_score;

		// reviewer information
		$review_content->reviewer = new stdClass;
		$customer_name = '';
		if ($customer) {
			$review_content->reviewer->name = $customer['first_name'];
			$review_content->reviewer->country_code = $customer['country'];
			$customer_name = $customer['first_name'] . ' ' . $customer['last_name'];
		} else {
			$revuname = '';
			if (!empty($order['custdata'])) {
				$uinfos = explode("\n", $order['custdata']);
				$first_info = explode(':', $uinfos[0]);
				if (count($first_info) > 1) {
					unset($first_info[0]);
					$revuname = implode(':', $first_info);
				} else {
					$revuname = trim($first_info[0]);
				}
			}
			$review_content->reviewer->name = $revuname;
			$review_content->reviewer->country_code = $order['country'];
			$customer_name = $revuname;
		}

		// maximum 2000 chars for the message review to avoid spammers
		if (!empty($ratingmess) && strlen($ratingmess) > 2000) {
			$ratingmess = substr($ratingmess, 0, 2000);
		}

		// review message
		$review_content->content = new stdClass;
		$review_content->content->message = !empty($ratingmess) ? $ratingmess : null;

		// null reply
		$review_content->reply = null;

		// check if multiple accounts to find the property name
		$property_name = null;
		$has_multiaccounts = false;
		$multi_map = array();
		$q = "SELECT * FROM `#__vikchannelmanager_roomsxref`;";
		$dbo->setQuery($q);
		$dbo->execute();
		if ($dbo->getNumRows()) {
			$xref_data = $dbo->loadAssocList();
			foreach ($xref_data as $xref) {
				if (empty($xref['prop_params'])) {
					continue;
				}
				if (!isset($multi_map[$xref['idchannel']])) {
					$multi_map[$xref['idchannel']] = array();
				}
				if (!isset($multi_map[$xref['idchannel']][$xref['prop_params']])) {
					$multi_map[$xref['idchannel']][$xref['prop_params']] = 0;
				}
				$multi_map[$xref['idchannel']][$xref['prop_params']]++;
			}
			foreach ($multi_map as $ch_id => $ch_params) {
				if (count($ch_params) > 1) {
					$has_multiaccounts = true;
					break;
				}
			}
		}
		if ($has_multiaccounts && (int)$order['roomsnum'] === 1) {
			// find the category name of the room booked (if any, and if one room booked)
			$q = "SELECT `idcat` FROM `#__vikbooking_rooms` WHERE `id`={$orderrooms[0]['idroom']};";
			$dbo->setQuery($q);
			$dbo->execute();
			if ($dbo->getNumRows()) {
				$allcats = $dbo->loadResult();
				if (!empty($allcats)) {
					$parts = explode(';', $allcats);
					if (count($parts) === 2) {
						// just one category, get the name of it
						$property_name = VikBooking::getCategoryName($parts[0]);
					}
				}
			}
			if (empty($property_name)) {
				// category not found, get the room name
				$property_name = $orderrooms[0]['name'];
			}
		}

		// create record
		$review_record = new stdClass;
		$review_record->review_id = -1;
		$review_record->prop_first_param = null;
		$review_record->prop_name = $property_name;
		$review_record->channel = null;
		$review_record->uniquekey = 0;
		$review_record->idorder = $order['id'];
		$review_record->dt = JFactory::getDate()->toSql(true);
		$review_record->customer_name = $customer_name;
		$review_record->lang = JFactory::getLanguage()->getTag();
		$review_record->score = $avg_score;
		$review_record->country = $order['country'];
		$review_record->content = json_encode($review_content);
		$review_record->published = ($gr_approval == 'auto' ? 1 : 0);

		// insert review
		if ($dbo->insertObject('#__vikchannelmanager_otareviews', $review_record, 'id')) {
			$app->enqueueMessage(JText::translate('VBOTHANKSREVIEWLEFT'));
			// Booking History
			VikBooking::getBookingHistoryInstance()->setBid($order['id'])->store('GR');
		} else {
			VikError::raiseWarning('', JText::translate('VBOREVIEWGENERROR'));
		}

		// update global score for website and this property (if multiple accounts)
		$globscore_id = null;
		$q = "SELECT `id` FROM `#__vikchannelmanager_otascores` WHERE `channel` IS NULL AND " . (is_null($property_name) ? '`prop_name` IS NULL' : '`prop_name`=' . $dbo->quote($property_name));
		$dbo->setQuery($q, 0, 1);
		$dbo->execute();
		if ($dbo->getNumRows()) {
			$globscore_id = $dbo->loadResult();
		}
		// select score for all reviews for the website and this account
		$services_scores = array();
		$services_revscount = array();
		$revs_count = 0;
		$super_tot = 0;
		$q = "SELECT `score`,`content` FROM `#__vikchannelmanager_otareviews` WHERE `channel` IS NULL AND " . (is_null($property_name) ? '`prop_name` IS NULL' : '`prop_name`=' . $dbo->quote($property_name)) . ";";
		$dbo->setQuery($q);
		$dbo->execute();
		if ($dbo->getNumRows()) {
			$all_scores = $dbo->loadAssocList();
			$revs_count += count($all_scores);
			foreach ($all_scores as $s) {
				$super_tot += $s['score'];
				// check if scores were given per service
				$s['content'] = json_decode($s['content'], true);
				if (isset($s['content']['scoring']) && count($s['content']['scoring']) > 1) {
					// review was left for services
					foreach ($s['content']['scoring'] as $sname => $sval) {
						if (!isset($services_scores[$sname])) {
							$services_scores[$sname] = 0;
							$services_revscount[$sname] = 0;
						}
						$services_scores[$sname] += $sval;
						$services_revscount[$sname]++;
					}
				}
			}
		}
		// global average score
		$revs_count = $revs_count > 0 ? $revs_count : 1;
		$glob_avg_score = ($super_tot / $revs_count);
		// services average score
		$glob_servs_avg_score = array();
		foreach ($services_scores as $sname => $val) {
			$services_revscount[$sname] = isset($services_revscount[$sname]) && $services_revscount[$sname] > 0 ? $services_revscount[$sname] : 1;
			$glob_servs_avg_score[$sname] = ($val / $services_revscount[$sname]);
		}
		
		// build global score content
		$glob_score_content = new stdClass;
		$glob_score_content->review_score = new stdClass;
		$glob_score_content->review_score->score = $glob_avg_score;
		$glob_score_content->review_score->review_count = $revs_count;
		foreach ($glob_servs_avg_score as $sname => $val) {
			$glob_score_content->{$sname} = new stdClass;
			$glob_score_content->{$sname}->score = $val;
			$glob_score_content->{$sname}->review_count = $services_revscount[$sname];
		}
		
		// build global score object
		$glob_score_obj = new stdClass;
		if (!empty($globscore_id)) {
			$glob_score_obj->id = $globscore_id;
		}
		$glob_score_obj->prop_first_param = null;
		$glob_score_obj->prop_name = $property_name;
		$glob_score_obj->channel = null;
		$glob_score_obj->uniquekey = 0;
		$glob_score_obj->last_updated = JFactory::getDate()->toSql(true);
		$glob_score_obj->score = round($glob_avg_score, 2);
		$glob_score_obj->content = json_encode($glob_score_content);

		// update or create global score
		if (!empty($globscore_id)) {
			$dbo->updateObject('#__vikchannelmanager_otascores', $glob_score_obj, 'id');
		} else {
			$dbo->insertObject('#__vikchannelmanager_otascores', $glob_score_obj, 'id');
		}

		// redirect to main view
		$app->redirect($goto);
	}

	/**
	 * AJAX task to get one monthly availability calendar of the room details page.
	 *
	 * @since 	1.13.5
	 */
	public function get_avcalendars_data()
	{
		$dbo 	 	= JFactory::getDbo();
		$rid 		= VikRequest::getInt('rid', 0, 'request');
		$direction 	= VikRequest::getString('direction', 'next', 'request');
		$fromdt 	= VikRequest::getString('fromdt', '', 'request');
		$nextdt 	= VikRequest::getString('nextdt', '', 'request');
		$prevdt 	= VikRequest::getString('prevdt', '', 'request');

		// make sure vars are not empty
		if (empty($rid) || empty($direction) || empty($fromdt) || empty($nextdt) || empty($prevdt)) {
			/**
			 * Search engines may follow this endpoint, so we have to exit with HTTP status code 200
			 * 
			 * @since 	1.16.0 (J) - 1.6.0 (WP)
			 */
			VBOHttpDocument::getInstance()->json(['Invalid request variables']);
		}

		// date format
		$vbo_df = VikBooking::getDateFormat();
		if ($vbo_df == "%d/%m/%Y") {
			$vbo_df = 'd/m/Y';
		} elseif ($vbo_df == "%m/%d/%Y") {
			$vbo_df = 'm/d/Y';
		} else {
			$vbo_df = 'Y/m/d';
		}

		// configuration settings
		$numcalendars = VikBooking::numCalendars();
		$showpartlyres = VikBooking::showPartlyReserved();
		$showcheckinoutonly = VikBooking::showStatusCheckinoutOnly();
		$usepricecal = false;
		$inonout_allowed = true;
		$timeopst = VikBooking::getTimeOpenStore();
		if (is_array($timeopst)) {
			if ($timeopst[0] < $timeopst[1]) {
				// check-in not allowed on a day where there is already a check out (no arrivals/depatures on the same day)
				$inonout_allowed = false;
			}
		}

		// week-days ordering
		$firstwday = (int)VikBooking::getFirstWeekDay();
		$days_labels = array(
			JText::translate('VBSUN'),
			JText::translate('VBMON'),
			JText::translate('VBTUE'),
			JText::translate('VBWED'),
			JText::translate('VBTHU'),
			JText::translate('VBFRI'),
			JText::translate('VBSAT')
		);
		$days_indexes = array();
		for ($i = 0; $i < 7; $i++) {
			$days_indexes[$i] = (6 - ($firstwday - $i) + 1) % 7;
		}

		// first day timestamp of month to read in case of forward navigation
		$start_ts = strtotime($fromdt);
		$start_info = getdate($start_ts);
		if (!$start_ts || !$start_info) {
			VBOHttpDocument::getInstance()->close(500, 'Invalid date provided');
		}
		// backward navigation
		if ($direction == 'prev') {
			// we need to get the previous month
			$start_ts = mktime(0, 0, 0, ($start_info['mon'] - 1), 1, $start_info['year']);
			$start_info = getdate($start_ts);
			if (!$start_ts || !$start_info) {
				VBOHttpDocument::getInstance()->close(500, 'Invalid date calculated');
			}
		}
		
		// make sure minimum date is respected
		$min_lim_ts = mktime(0, 0, 0, date('n'), 1, date('Y'));
		if ($start_ts < $min_lim_ts) {
			VBOHttpDocument::getInstance()->close(500, 'Dates in the past not allowed');
		}

		// check the current next and prev dates to help the next AJAX navigations
		$nextnav_ts = strtotime($nextdt);
		$nextnav_info = getdate($nextnav_ts);
		if (!$nextnav_ts || !$nextnav_info) {
			VBOHttpDocument::getInstance()->close(500, 'Invalid next navigation date provided');
		}
		$prevnav_ts = strtotime($prevdt);
		$prevnav_info = getdate($prevnav_ts);
		if (!$prevnav_ts || !$prevnav_info) {
			VBOHttpDocument::getInstance()->close(500, 'Invalid prev navigation date provided');
		}

		// make sure maximum date is respected
		$max_months_future = 12;
		$max_date_future  = VikBooking::getMaxDateFuture($rid);
		if (!empty($max_date_future)) {
			$numlim = (int)substr($max_date_future, 1, (strlen($max_date_future) - 2));
			$numlim = $numlim < 1 ? 1 : $numlim;
			$quantlim = substr($max_date_future, -1, 1);
			if ($quantlim == 'm' || $quantlim == 'y') {
				$max_months_future = $numlim * ($quantlim == 'm' ? 1 : 12);
				$max_ts_future = strtotime("+{$max_months_future} months");
				$max_info = getdate($max_ts_future);
				$max_endts_future = mktime(23, 59, 59, $max_info['mon'], date('t', $max_info[0]), $max_info['year']);
				if ($start_ts > $max_endts_future) {
					VBOHttpDocument::getInstance()->close(500, 'Maximum date in the future exceeded');
				}
			}
		}

		// get global property closing dates
		$cal_closing_dates = VikBooking::parseJsClosingDates();
		if (count($cal_closing_dates)) {
			foreach ($cal_closing_dates as $ccdk => $ccdv) {
				if (!(count($ccdv) == 2)) {
					continue;
				}
				$cal_closing_dates[$ccdk][0] = strtotime($ccdv[0]);
				$cal_closing_dates[$ccdk][1] = strtotime($ccdv[1]);
			}
		}

		// load room details
		$q = "SELECT * FROM `#__vikbooking_rooms` WHERE `id`={$rid}";
		$dbo->setQuery($q, 0, 1);
		$dbo->execute();
		if (!$dbo->getNumRows()) {
			VBOHttpDocument::getInstance()->close(404, 'Room not found');
		}
		$room_details = $dbo->loadAssoc();

		// get the future busy records
		$today_ts = mktime(0, 0, 0, date('n'), date('j'), date('Y'));
		$previousdayclass = '';

		$q = "SELECT * FROM `#__vikbooking_busy` WHERE `idroom`={$room_details['id']} AND `checkout`>={$start_ts};";
		$dbo->setQuery($q);
		$busy = $dbo->loadAssocList();

		// empty day element
		$empty_elem = new stdClass;
		$empty_elem->type = 'placeholder';
		$empty_elem->cont = '&nbsp;';

		// build response container
		$calendars = array();

		// prepare calendar object
		$calendar = new stdClass;
		$calendar->ts = $start_info[0];
		$calendar->mon = $start_info['mon'];
		$calendar->mday = $start_info['mday'];
		$calendar->year = $start_info['year'];
		$calendar->month = VikBooking::sayMonth($start_info['mon']);
		$calendar->wdays = array();
		for ($i = 0; $i < 7; $i++) {
			$d_ind = ($i + $firstwday) < 7 ? ($i + $firstwday) : ($i + $firstwday - 7);
			array_push($calendar->wdays, $days_labels[$d_ind]);
		}

		// build the calendar rows by looping over the days of this month
		$calendar->rows = array();

		// the row will contain all the placeholders and real days (7 elements at most)
		$row = array();
		$d_count = 0;

		// first, we push empty dates placeholders for printing the first table cells (cells before the 1st of the month)
		for ($i = 0, $n = $days_indexes[$start_info['wday']]; $i < $n; $i++, $d_count++) {
			// push fake-day element (placeholder)
			array_push($row, $empty_elem);
		}

		// start looping
		$loop_end_month = $start_info['mon'];
		while ($start_info['mon'] == $loop_end_month) {
			if ($d_count > 6) {
				// push the current row
				array_push($calendar->rows, $row);

				// start a new row and reset cells counter
				$row = array();
				$d_count = 0;
			}

			// build real-day element
			$elem = new stdClass;
			$elem->type = 'day';
			$elem->cont = $start_info['mday'] < 10 ? "0{$start_info['mday']}" : $start_info['mday'];
			$elem->dt 	= date($vbo_df, $start_info[0]);
			$elem->ymd 	= date('Y-m-d', $start_info[0]);
			$elem->ts 	= $start_info[0];
			$elem->class = 'vbtdfree';
			$elem->past_class = $start_info[0] < $today_ts ? ' vbtdpast' : '';

			// check whether this day has got bookings
			$totfound = 0;
			$ischeckinday = false;
			$ischeckoutday = false;
			foreach ($busy as $b) {
				$info_in = getdate($b['checkin']);
				$checkin_ts = mktime(0, 0, 0, $info_in['mon'], $info_in['mday'], $info_in['year']);
				$info_out = getdate($b['checkout']);
				$checkout_ts = mktime(0, 0, 0, $info_out['mon'], $info_out['mday'], $info_out['year']);
				if ($start_info[0] >= $checkin_ts && $start_info[0] == $checkout_ts) {
					$ischeckoutday = true;
				}
				if ($start_info[0] >= $checkin_ts && $start_info[0] < $checkout_ts) {
					$totfound++;
					if ($start_info[0] == $checkin_ts) {
						$ischeckinday = true;
					}
				}
			}
			if ($totfound >= $room_details['units']) {
				$elem->class = "vbtdbusy";
				if ($ischeckinday && $showcheckinoutonly && !$usepricecal && $inonout_allowed && $previousdayclass != "vbtdbusy" && $previousdayclass != "vbtdbusy vbtdbusyforcheckin") {
					$elem->class = "vbtdbusy vbtdbusyforcheckin";
				} elseif ($ischeckinday && !$usepricecal && !$inonout_allowed && $previousdayclass != "vbtdbusy" && $previousdayclass != "vbtdbusy vbtdbusyforcheckin") {
					// check-out not allowed on a day where someone is already checking-in
					$elem->class = "vbtdbusy";
				}
			} elseif ($totfound > 0) {
				if ($showpartlyres) {
					$elem->class = "vbtdwarning";
				}
			} else {
				if ($ischeckoutday && !$usepricecal && $showcheckinoutonly && $inonout_allowed && !($room_details['units'] > 1)) {
					$elem->class = "vbtdbusy vbtdbusyforcheckout";
				} elseif ($ischeckoutday && !$usepricecal && !$inonout_allowed && !($room_details['units'] > 1)) {
					$elem->class = "vbtdbusy";
				}
			}

			// check global closing dates
			if (count($cal_closing_dates)) {
				foreach ($cal_closing_dates as $closed_interval) {
					if ($start_info[0] >= $closed_interval[0] && $start_info[0] <= $closed_interval[1]) {
						$elem->class = "vbtdbusy";
						break;
					}
				}
			}

			// push cell element and increase counter
			array_push($row, $elem);
			$d_count++;

			// update previous day class
			$previousdayclass = $elem->class;

			// go to next day
			$start_info = getdate(mktime(0, 0, 0, $start_info['mon'], ($start_info['mday'] + 1), $start_info['year']));
		}

		if (count($row)) {
			// the last row still need to be pushed in the rows
			for ($i = $d_count; $i <= 6; $i++) {
				// fill last empty days
				array_push($row, $empty_elem);
			}

			// push ending row
			array_push($calendar->rows, $row);
		}

		// push this month's calendar object
		array_push($calendars, $calendar);

		// check whether next request can navigate forward
		$can_nav_next = false;
		if ($direction == 'prev') {
			// we went prev, so we got to be able to go next
			$can_nav_next = true;
		} elseif ($direction == 'next' && strtotime("+{$max_months_future} months") > $start_info[0]) {
			// went next and max future date is still greater than next hypothetical month
			$can_nav_next = true;
		}

		// check whether next request can navigate backward
		$can_nav_prev = false;
		if ($direction == 'next') {
			// we went next, so we got to be able to go prev
			$can_nav_prev = true;
		} elseif ($direction == 'prev' && $min_lim_ts < mktime(0, 0, 0, ($start_info['mon'] - 1), 1, $start_info['year'])) {
			// went prev and first day of today's month is less than (not equal to) the last month just rendered
			$can_nav_prev = true;
		}

		// calculate next and prev dates for the next navigations to help the AJAX request
		$next_ymd = null;
		if ($can_nav_next) {
			if ($direction == 'next') {
				// next forward navigation will start from the ne month
				$next_ymd = date('Y-m-d', $start_info[0]);
			} else {
				// we add the total number of calendars to the month after the one lastly displayed
				$next_ymd = date('Y-m-d', strtotime("+" . ($numcalendars - 1) . " months", $start_info[0]));
			}
		}
		$prev_ymd = null;
		if ($can_nav_prev) {
			if ($direction == 'prev') {
				// next backward navigation will start from the last month we just displayed
				$prev_ymd = date('Y-m-d', mktime(0, 0, 0, ($start_info['mon'] - 1), 1, $start_info['year']));
			} else {
				// we add the total number of calendars to the month after the one lastly displayed
				$prev_ymd = date('Y-m-d', strtotime("-{$numcalendars} months", $start_info[0]));
			}
		}

		// build response object
		$response = new stdClass;
		$response->calendars 	= $calendars;
		$response->can_nav_next = $can_nav_next;
		$response->can_nav_prev = $can_nav_prev;
		$response->next_ymd 	= $next_ymd;
		$response->prev_ymd 	= $prev_ymd;

		echo json_encode($response);
		exit;
	}

	/**
	 * AJAX request for adding a new room-day note from the front-end tableaux.
	 * 
	 * @return 	void
	 * 
	 * @since 	1.13.5
	 */
	public function add_roomdaynote()
	{
		if (!JSession::checkToken()) {
			// missing CSRF-proof token
			VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
		}

		$dt 	 = VikRequest::getString('dt', '', 'request');
		$idroom  = VikRequest::getInt('idroom', 0, 'request');
		$subunit = VikRequest::getInt('subunit', 0, 'request');
		$type 	 = VikRequest::getString('type', '', 'request');
		$type 	 = empty($type) ? 'custom' : $type;
		$name 	 = VikRequest::getString('name', '', 'request');
		$descr 	 = VikRequest::getString('descr', '', 'request');
		$cdays   = VikRequest::getInt('cdays', 0, 'request');
		$cdays 	 = $cdays < 0 ? 0 : $cdays;
		$cdays 	 = $cdays > 365 ? 365 : $cdays;
		if (empty($idroom) || empty($dt) || !strtotime($dt)) {
			echo 'e4j.error.1';
			exit;
		}

		// we put the operator name in the description (if available)
		$operator = VikBooking::getOperatorInstance()->getOperatorAccount();
		if ($operator !== false) {
			$oper_signature = "({$operator['first_name']} {$operator['last_name']})";
			$descr = empty($descr) ? $oper_signature : $descr . " \n" . $oper_signature;
		}

		// reload end date
		$end_date = $dt;
		
		// build critical date object
		$new_note = array(
			'name'  => $name,
			'type'  => $type,
			'descr' => $descr,
		);

		// get object
		$notes  = VikBooking::getCriticalDatesInstance();

		// store the notes for all consecutive dates
		for ($i = 0; $i <= $cdays; $i++) {
			$store_dt = $dt;
			if ($i > 0) {
				$dt_info = getdate(strtotime($store_dt));
				$store_dt = date('Y-m-d', mktime(0, 0, 0, $dt_info['mon'], ($dt_info['mday'] + $i), $dt_info['year']));
				$end_date = $store_dt;
			}
			$result = $notes->storeDayNote($new_note, $store_dt, $idroom, $subunit);
			if (!$result) {
				echo 'e4j.error.2';
				exit;
			}
		}

		// reload all room day notes for this day for the AJAX response
		$all_notes = $notes->loadRoomDayNotes($dt, $end_date, $idroom, $subunit);

		if (!$all_notes || !count($all_notes)) {
			// no notes found even after storing it
			echo 'e4j.error.3';
			exit;
		}

		echo json_encode($all_notes);
		exit;
	}

	/**
	 * AJAX endpoint to upload customer documents during the pre-checkin.
	 * 
	 * @since 	1.14 (J) - 1.4.0 (WP)
	 * @since 	1.18.6 (J) - 1.8.6 (WP) added support for MRZ detection through Channel Manager.
	 */
	public function precheckin_upload_docs()
	{
		$app   = JFactory::getApplication();
		$dbo   = JFactory::getDbo();
		$input = $app->input;

		if (!JSession::checkToken()) {
			// missing CSRF-proof token
			VBOHttpDocument::getInstance($app)->close(403, JText::translate('JINVALID_TOKEN'));
		}

		// gather request values
		$order_sid = $input->getString('sid', '');
		$order_ts  = $input->getString('ts', '');
		$use_mrz   = $input->getBool('mrz', false);
		/**
		 * This is a simple file uploading process, but if
		 * we wanted to automatically update the pax_data,
		 * the request vars below would be necessary.
		 */
		$room_index  = $input->getInt('room_index', 0);
		$guest_index = $input->getInt('guest_index', 0);
		$pax_index   = $input->getString('pax_index', '');

		if (empty($order_sid) || empty($order_ts)) {
			VBOHttpDocument::getInstance($app)->close(404, 'Missing booking details');
		}

		$q = "SELECT `o`.*,(SELECT SUM(`or`.`adults`) FROM `#__vikbooking_ordersrooms` AS `or` WHERE `or`.`idorder`=`o`.`id`) AS `tot_adults` FROM `#__vikbooking_orders` AS `o` WHERE (`o`.`sid`=" . $dbo->quote($order_sid) . " OR `o`.`idorderota`=" . $dbo->quote($order_sid) . ") AND `o`.`ts`=" . $dbo->quote($order_ts) . " AND `o`.`status`='confirmed';";
		$dbo->setQuery($q);
		$order = $dbo->loadAssoc();
		if (!$order) {
			VBOHttpDocument::getInstance($app)->close(404, 'Booking not found');
		}

		$customer = array();
		$q = "SELECT `c`.*,`co`.`idorder`,`co`.`signature`,`co`.`pax_data`,`co`.`comments`,`co`.`checkindoc` FROM `#__vikbooking_customers` AS `c` LEFT JOIN `#__vikbooking_customers_orders` `co` ON `c`.`id`=`co`.`idcustomer` WHERE `co`.`idorder`=".$order['id'].";";
		$dbo->setQuery($q);
		$customer = $dbo->loadObject();
		if (!$customer) {
			// one customer must be assigned to this booking for the pre-checkin
			VBOHttpDocument::getInstance($app)->close(404, 'No customers associated to this booking');
		}

		// make sure pre-checkin is allowed
		$precheckin = VikBooking::precheckinEnabled();
		if ($precheckin) {
			// make sure the limit of days in advance is reflected
			$precheckin_mind = VikBooking::precheckinMinOffset();
			if ($precheckin_mind < 0) {
				// validation made prior to check-out date and time
				$precheckin = time() <= strtotime("{$precheckin_mind} days 23:59:59", $order['checkout']);
			} else {
				// classic validation prior to check-in date and time
				$precheckin_lim_ts = strtotime("+{$precheckin_mind} days 00:00:00");
				$precheckin = ($precheckin_lim_ts <= $order['checkin'] || ($precheckin_mind === 1 && time() <= $order['checkin']));
			}
		}
		if (!$precheckin) {
			VBOHttpDocument::getInstance($app)->close(403, 'Pre-checkin not allowed at this time');
		}

		// get uploaded files array (use "raw" to avoid filtering the file to upload)
		$files = $input->files->get('docs', array(), 'raw');
		if (!$files) {
			VBOHttpDocument::getInstance($app)->close(500, 'No files to be uploaded');
		}

		if (isset($files['name'])) {
			// we have a single associative array, we need to push it within a list,
			// because the upload iterates the $files array
			$files = [$files];
		}

		// fetch documents folder path
		$dirpath = VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR;

		// check if we have a valid directory
		if (empty($customer->docsfolder) || !is_dir($dirpath . $customer->docsfolder)) {
			// randomize string
			$customer->seed = uniqid();

			// create blocks for hashed folder
			$parts = [
				$customer->first_name,
				$customer->last_name,
				md5(serialize($customer)),
			];

			// join fetched parts
			$customer->docsfolder = strtolower(implode('-', array_filter($parts)));

			if (strlen($customer->docsfolder) < 16) {
				VBOHttpDocument::getInstance($app)->close(400, 'Possible security breach. Please specify as many details as possible.');
			}

			// create a folder for this customer
			$created = JFolder::create($dirpath . $customer->docsfolder);

			if (!$created) {
				VBOHttpDocument::getInstance($app)->close(403, sprintf('Unable to create the folder [%s]', $dirpath . $customer->docsfolder));
			}

			unset($customer->seed);

			// update customer docs folder
			$record = new stdClass;
			$record->id = $customer->id;
			$record->docsfolder = $customer->docsfolder;
			$dbo->updateObject('#__vikbooking_customers', $record, 'id');
		}

		// prepare the response with the uploaded-file objects
		$response = [
			'uploads' => [],
		];
		$upload_err = null;

		// compose prefix for all files uploaded (must end with an underscrore for View's compatibility)
		$file_prefix = str_replace(' ', '-', JText::translate('VBOPRECHECKIN')) . '_';

		try {
			
			foreach ($files as $file) {
				// sanitize file name
				if (!empty($file['name'])) {
					// replace quotes and pipe, which is the separator in pax_data
					$file['name'] = str_replace(array("'", '"', '|'), '', $file['name']);
				}
				if (empty($file['name'])) {
					continue;
				}
				// always prepend "pre check-in" to the original file name
				$file['name'] = strtolower($file_prefix . $file['name']);
				// try to upload the file
				$result = VikBooking::uploadFileFromRequest($file, $dirpath . $customer->docsfolder, 'png,jpg,jpeg,bmp,heic,zip,rar,pdf,doc,docx,rtf,odt,pages,xls,xlsx,csv,ods,numbers,txt,md');
				// set a valid URL for the uploaded file
				$result->url = str_replace(DIRECTORY_SEPARATOR, '/', str_replace(VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR, VBO_CUSTOMERS_URI, $result->path));
				// push uploaded file
				array_push($response['uploads'], $result);
			}

		} catch (Exception $e) {
			// do nothing, but catch the error
			$upload_err = $e;
		}

		if (!$response['uploads']) {
			// raise an error
			if ($upload_err instanceof Exception) {
				VBOHttpDocument::getInstance($app)->close($upload_err->getCode() ?: 500, $upload_err->getMessage());
			}
			VBOHttpDocument::getInstance($app)->close(500, 'No files could actually be uploaded');
		}

		if ($use_mrz) {
			// file upload completed, detect MRZ codes from uploaded documents
			$mrzImageUrls = array_values(array_filter(array_map(function($uploaded) {
				return $uploaded->url ?? '';
			}, $response['uploads'])));

			// set response properties for MRZ detection result
			$response['mrz'] = [
				'data'     => null,
				'raw'      => null,
				'verified' => null,
				'error'    => null,
			];

			try {
				// let the AI model service analyse the uploaded documents
				$mrzResult = (new VCMAiModelService)->mrzDetection($mrzImageUrls);

				// let the current pax-fields data collector parse the extracted MRZ data
				$mrzMappedFields = VBOCheckinPax::getInstance()
					->getMRZMapper()
					->setVerified((bool) ($mrzResult->valid ?? false))
					->mapDetectedProperties((array) ($mrzResult->data ?? null))
					->getMappedFields();

				// set MRZ validation result within the response
				$response['mrz']['data']     = $mrzMappedFields;
				$response['mrz']['raw']      = ($mrzResult->data ?? null);
				$response['mrz']['verified'] = (bool) $mrzResult->valid ?? false;
			} catch (Exception $e) {
				// catch the error message
				$response['mrz']['error'] = $e->getMessage();
			} catch (Throwable $e) {
				// catch the PHP error
				$response['mrz']['error'] = sprintf('Fatal error caught: %s', $e->getMessage());
			}
		}

		// send response to output
		VBOHttpDocument::getInstance($app)->json($response);
	}

	/**
	 * AJAX endpoint to submit an inquiry/information request.
	 * 
	 * @since 	1.15.0 (J) - 1.5.0 (WP)
	 */
	public function submit_inquiry()
	{
		if (!JSession::checkToken()) {
			// missing CSRF-proof token
			VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
		}

		$dbo 	= JFactory::getDbo();
		$app 	= JFactory::getApplication();
		$input  = $app->input;

		// get request values
		$checkin_dt  = $input->getString('checkindate', '');
		$checkin_h   = $input->getInt('checkinh', 0);
		$checkin_m   = $input->getInt('checkinm', 0);
		$checkout_dt = $input->getString('checkoutdate', '');
		$checkout_h  = $input->getInt('checkouth', 0);
		$checkout_m  = $input->getInt('checkoutm', 0);
		$categories  = $input->getString('categories', '');
		$roomsnum  	 = $input->getInt('roomsnum', 1);
		$adults 	 = $input->get('adults', array(), 'int');
		$children 	 = $input->get('children', array(), 'int');
		$inquiry 	 = $input->get('inquiry', array(), 'raw');
		$ulang 		 = $input->getString('ulang', '');

		$timeopst = VikBooking::getTimeOpenStore();
		if (empty($checkin_h) && empty($checkout_h) && is_array($timeopst)) {
			$opent = VikBooking::getHoursMinutes($timeopst[0]);
			$closet = VikBooking::getHoursMinutes($timeopst[1]);
			$checkin_h  = $opent[0];
			$checkin_m  = $opent[1];
			$checkout_h = $closet[0];
			$checkout_m = $closet[1];
		}

		// compose the stay dates
		$checkin_ts  = VikBooking::getDateTimestamp($checkin_dt, $checkin_h, $checkin_m);
		$checkout_ts = VikBooking::getDateTimestamp($checkout_dt, $checkout_h, $checkout_m);

		if (empty($checkin_dt) || empty($checkout_dt) || empty($checkin_ts) || empty($checkout_ts)) {
			// invalid dates
			VBOHttpDocument::getInstance()->close(400, JText::translate('VBINVALIDDATES'));
		}

		// validate guests
		if (!is_array($adults) || !count($adults)) {
			// invalid adults
			VBOHttpDocument::getInstance()->close(400, JText::translate('VBINCONGRDATA'));
		}

		// prepare customer information
		$res_custdata  = array();
		$t_first_name  = '';
		$t_last_name   = '';
		$guest_email   = '';
		$guest_phone   = '';
		$guest_country = '';
		$guest_extras  = array();
		$guest_custom  = array();
		foreach ($inquiry as $info_type => $info_vals) {
			if (empty($info_type) || $info_type == 'checkbox') {
				// we ignore any checkbox information
				continue;
			}
			foreach ($info_vals as $info_val) {
				if (!is_scalar($info_val) || !strlen($info_val)) {
					// empty or invalid field
					continue;
				}
				if ($info_type == 'nominative') {
					if (empty($t_first_name)) {
						$t_first_name = $info_val;
						$res_custdata[JText::translate('VBNAME')] = $info_val;
					} else {
						$t_last_name = $info_val;
						$res_custdata[JText::translate('VBLNAME')] = $info_val;
					}
				} elseif ($info_type == 'email') {
					$guest_email = $info_val;
					$res_custdata[JText::translate('ORDER_EMAIL')] = $info_val;
				} elseif ($info_type == 'phone') {
					$guest_phone = $info_val;
					$res_custdata[JText::translate('ORDER_PHONE')] = $info_val;
				} elseif ($info_type == 'country') {
					$guest_country = $info_val;
					$res_custdata[JText::translate('ORDER_STATE')] = $info_val;
				} elseif ($info_type == 'city') {
					$guest_extras['city'] = $info_val;
					$res_custdata[JText::translate('ORDER_CITY')] = $info_val;
				} else {
					// we treat this as a custom data field
					$guest_custom[$info_type] = $info_val;
					// inject reservation custdata string for this value
					if ($info_type == 'special_requests') {
						$res_custdata[JText::translate('ORDER_SPREQUESTS')] = $info_val;
					} else {
						$readable_ftype = ucwords(str_replace(array('_', '-'), ' ', $info_type));
						$res_custdata[$readable_ftype] = $info_val;
					}
				}
			}
		}

		// validate fields
		if (!count($res_custdata)) {
			// we received no filled information
			VBOHttpDocument::getInstance()->close(400, JText::translate('VBINCONGRDATA'));
		}

		// build the reservation raw-text for the customer data
		$res_custdata_str = VikBooking::buildCustData($res_custdata, "\n");

		// store the customer record as first thing
		$cpin = VikBooking::getCPinIstance();
		$cpin->setCustomerExtraInfo($guest_extras);
		$cpin->saveCustomerDetails($t_first_name, $t_last_name, $guest_email, $guest_phone, $guest_country, array());

		// build customer object for the inquiry reservation
		$customer = new stdClass;
		$customer->name = $t_first_name;
		$customer->lname = $t_last_name;
		$customer->email = $guest_email;
		$customer->phone = $guest_phone;
		$customer->country = $guest_country;
		$customer->lang = $ulang;
		$customer->custdata = $res_custdata_str;
		$customer->adminnotes = isset($guest_custom['special_requests']) ? $guest_custom['special_requests'] : '';
		/**
		 * Always append the originally selected stay dates and guest party to the
		 * administrator notes string so that they can be accessed all the times.
		 */
		$customer->adminnotes .= !empty($customer->adminnotes) ? "\n" : '';
		$customer->adminnotes .= JText::translate('VBPICKUP') . ': ' . $checkin_dt . "\n";
		$customer->adminnotes .= JText::translate('VBRETURN') . ': ' . $checkout_dt . "\n";
		$customer->adminnotes .= JText::translate('VBFORMADULTS') . ': ' . array_sum($adults) . "\n";
		$customer->adminnotes .= JText::translate('VBFORMCHILDREN') . ': ' . array_sum($children) . "\n";

		// prepare response object
		$response = new stdClass;
		$response->status = 0;
		$response->error  = '';

		// invoke availability helper class
		$av_helper = VikBooking::getAvailabilityInstance();

		// turn flag on to ignore restrictions, as this is an inquiry and we must allocate the booking
		$av_helper->ignoreRestrictions(true);

		// increase the default number of back and forth days for alternative date suggestions
		$av_helper->setBackForthDays(90);

		// set stay dates
		$av_helper->setStayDates($checkin_dt, $checkout_dt);

		// set room parties, but we expect to always have one room for the inquiry
		foreach ($adults as $k => $num_adults) {
			$num_children = isset($children[$k]) ? $children[$k] : 0;
			$av_helper->setRoomParty($num_adults, $num_children);
		}

		// load available room rates
		$room_rates = $av_helper->getRates();

		// check if availability errors occurred
		$has_av_error  = strlen($av_helper->getError());
		$av_error_code = $av_helper->getErrorCode();

		// count total fitting records
		$tot_records = is_array($room_rates) && !$has_av_error ? count($room_rates) : 0;

		// build history extra data base object
		$ymd_stay_dates  = $av_helper->getStayDates();
		$hist_extra_data = new stdClass;
		$hist_extra_data->checkin_date  = $ymd_stay_dates[0];
		$hist_extra_data->checkout_date = $ymd_stay_dates[1];
		$hist_extra_data->adults = array_sum($adults);
		$hist_extra_data->children = array_sum($children);

		if ($tot_records > 0) {
			// we can create an inquiry pending reservation for a room rate
			$inquiry_res_id = 0;
			foreach ($room_rates as $rid => $rates) {
				foreach ($rates as $room_rplan) {
					if (!is_array($room_rplan) || !isset($room_rplan['idroom'])) {
						continue;
					}
					// we grab the cheapest room and cheapest rate plan (the first room-rate plan)
					$inquiry_res_id = $av_helper->createInquiryReservation($room_rplan, $customer);
					break;
				}
				// make sure to create just one inquiry reservation for the cheapest room available
				if ($inquiry_res_id) {
					break;
				}
			}

			if ($inquiry_res_id) {
				// assign booking to customer
				$cpin->saveCustomerBooking($inquiry_res_id);

				// send email notification to admin
				VikBooking::sendBookingEmail($inquiry_res_id, array('admin'));

				// trigger SMS sending
				VikBooking::sendBookingSMS($inquiry_res_id);

				// booking history (set inquiry availability type to 1)
				$hist_extra_data->av_type = 1;
				VikBooking::getBookingHistoryInstance()->setBid($inquiry_res_id)->setExtraData($hist_extra_data)->store('IR', $customer->adminnotes);
			}

			// update the response status no matter what
			$response->status = 1;

			// output response and terminate the request
			VBOHttpDocument::getInstance()->json($response);
		}

		// rely on suggestions in case of no rooms available
		if (!is_array($room_rates) || $has_av_error) {
			// try to get the suggestions when no availability
			list($alternative_dates, $alternative_parties) = $av_helper->findSuggestions();

			$inquiry_res_id = 0;
			$alt_suggestion = '';

			if (count($alternative_dates)) {
				$inquiry_res_id = $av_helper->allocateAltDatesInquiry($alternative_dates, $customer);
				$alt_suggestion = JText::translate('VBO_ALT_DATES_INQ');
				// set inquiry availability type to 2 for alternative dates
				$hist_extra_data->av_type = 2;
			} elseif (count($alternative_parties)) {
				$inquiry_res_id = $av_helper->allocateAltPartyInquiry($alternative_parties, $customer);
				$alt_suggestion = JText::translate('VBO_ALT_PARTY_INQ');
				// set inquiry availability type to 3 for alternative party
				$hist_extra_data->av_type = 3;
			}

			if ($inquiry_res_id) {
				// assign booking to customer
				$cpin->saveCustomerBooking($inquiry_res_id);

				// send email notification to admin
				VikBooking::sendBookingEmail($inquiry_res_id, array('admin'));

				// trigger SMS sending
				VikBooking::sendBookingSMS($inquiry_res_id);

				// booking history
				$history_obj = VikBooking::getBookingHistoryInstance()->setBid($inquiry_res_id);
				// store first history record
				$history_obj->store('IR', $customer->adminnotes);
				// store history record mentioning the suggestion used
				$history_obj->setExtraData($hist_extra_data)->store('IR', $alt_suggestion);
			}

			// update the response status no matter what
			$response->status = 1;

			// output response and terminate the request
			VBOHttpDocument::getInstance()->json($response);
		}

		/**
		 * If we reach this point it means that no rooms were available and no rooms/dates could be
		 * suggested as an alternative party. Therefore, we allocate the reservation on a "dummy room".
		 */
		$all_rooms = $av_helper->loadRooms();
		if (count($all_rooms)) {
			// grab the first "dummy room"
			$dummy_room_id = key($all_rooms);
			$room_rplan = array(
				'idroom' => $dummy_room_id,
			);

			// create inquiry reservation for a dummy room
			$inquiry_res_id = $av_helper->createInquiryReservation($room_rplan, $customer);
			$alt_suggestion = JText::translate('VBO_ALT_DUMMY_INQ');
			// set inquiry availability type to 4 for "dummy room"
			$hist_extra_data->av_type = 4;

			if ($inquiry_res_id) {
				// assign booking to customer
				$cpin->saveCustomerBooking($inquiry_res_id);

				// send email notification to admin
				VikBooking::sendBookingEmail($inquiry_res_id, array('admin'));

				// trigger SMS sending
				VikBooking::sendBookingSMS($inquiry_res_id);

				// booking history
				$history_obj = VikBooking::getBookingHistoryInstance()->setBid($inquiry_res_id);
				// store first history record
				$history_obj->store('IR', $customer->adminnotes);
				// store history record mentioning the suggestion used
				$history_obj->setExtraData($hist_extra_data)->store('IR', $alt_suggestion);

				// update the response status
				$response->status = 1;

				// output response and terminate the request
				VBOHttpDocument::getInstance()->json($response);
			}
		}

		// if not even a dummy room could be allocated, it means Vik Booking isn't set up
		$response->error = $av_helper->explainErrorCode();
		if (empty($response->error)) {
			$response->error = 'No rooms have been configured on this site yet';
		}

		// output response and terminate the request
		VBOHttpDocument::getInstance()->json($response);
	}

	/**
	 * AJAX endpoint to load the states of a given country.
	 * 
	 * @return 	void
	 * 
	 * @since 	1.16.0 (J) - 1.6.0 (WP)
	 */
	public function states_load_from_country()
	{
		$dbo = JFactory::getDbo();

		$id_country 	= VikRequest::getInt('id_country', 0, 'request');
		$country_3_code = VikRequest::getString('country_3_code', '', 'request');
		$country_2_code = VikRequest::getString('country_2_code', '', 'request');
		$country_name 	= VikRequest::getString('country_name', '', 'request');

		if (empty($id_country) && empty($country_3_code) && empty($country_2_code) && empty($country_name)) {
			VBOHttpDocument::getInstance()->close(500, 'Missing country identifier');
		}

		if (!empty($id_country)) {
			$q = "SELECT * FROM `#__vikbooking_states` WHERE `id_country`=" . $id_country;
			$dbo->setQuery($q);
			$dbo->execute();
			if (!$dbo->getNumRows()) {
				// no records found for this country
				VBOHttpDocument::getInstance()->json([]);
			}
			// output the JSON encoded list of states found
			VBOHttpDocument::getInstance()->json($dbo->loadAssocList());
		}

		// find country ID by name or code
		$field_name = $dbo->qn('country_name');
		$field_value = $country_name;
		if (!empty($country_3_code)) {
			$field_name = $dbo->qn('country_3_code');
			$field_value = $country_3_code;
		}
		if (!empty($country_2_code)) {
			$field_name = $dbo->qn('country_2_code');
			$field_value = $country_2_code;
		}

		$q = "SELECT `id` FROM `#__vikbooking_countries` WHERE {$field_name}=" . $dbo->quote($field_value);
		$dbo->setQuery($q, 0, 1);
		$dbo->execute();
		if (!$dbo->getNumRows()) {
			// country not found
			VBOHttpDocument::getInstance()->close(404, sprintf('Country [%s] not found', $field_value));
		}

		$id_country = $dbo->loadResult();

		$q = "SELECT * FROM `#__vikbooking_states` WHERE `id_country`=" . $id_country;
		$dbo->setQuery($q);
		$dbo->execute();
		if (!$dbo->getNumRows()) {
			// no records found for this country
			VBOHttpDocument::getInstance()->json([]);
		}
		// output the JSON encoded list of states found
		VBOHttpDocument::getInstance()->json($dbo->loadAssocList());
	}

	/**
	 * AJAX endpoint to perform the room upgrade operation.
	 * 
	 * @return 	void
	 * 
	 * @since 	1.16.0 (J) - 1.6.0 (WP)
	 */
	public function upgrade_room()
	{
		$dbo = JFactory::getDbo();

		$bid = VikRequest::getInt('bid', 0, 'request');
		$sid = VikRequest::getString('sid', '', 'request');
		$ts = VikRequest::getString('ts', '', 'request');
		$room_index = VikRequest::getInt('room_index', 0, 'request');
		$room_id = VikRequest::getInt('room_id', 0, 'request');

		if (empty($room_id)) {
			VBOHttpDocument::getInstance()->close(500, 'Invalid data provided');
		}

		$q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`={$bid} AND (`sid`=" . $dbo->q($sid) . " OR `idorderota`=" . $dbo->q($sid) . ") AND `ts`=" . $dbo->q($ts) . " AND `status`='confirmed'";
		$dbo->setQuery($q, 0, 1);
		$dbo->execute();
		if (!$dbo->getNumRows()) {
			VBOHttpDocument::getInstance()->close(404, JText::translate('VBORDERNOTFOUND'));
		}

		$booking = $dbo->loadAssoc();

		$booking_rooms = VikBooking::loadOrdersRoomsData($booking['id']);
		if (!$booking_rooms || !isset($booking_rooms[$room_index])) {
			VBOHttpDocument::getInstance()->close(404, JText::translate('VBORDERNOTFOUND'));
		}

		// access the room helper object
		$room_helper = VBORoomHelper::getInstance([
			'booking' => $booking,
			'rooms'   => $booking_rooms,
		]);

		// load the upgrade options for this booking
		$upgrade_options = $room_helper->getUpgradeOptions();

		if (!$upgrade_options || !isset($upgrade_options['upgrade'][$room_index]) || !isset($upgrade_options['upgrade'][$room_index]['r_costs'][$room_id])) {
			// the room for the upgrade is not available
			VBOHttpDocument::getInstance()->close(500, 'Could not upgrade to the selected room. Please reload the page and try again');
		}

		$upgrade_room_rate = $upgrade_options['upgrade'][$room_index]['r_costs'][$room_id];

		// calculate the cost difference, if any
		$current_room_cost  = $booking_rooms[$room_index]['cust_cost'] > 0 ? (float)$booking_rooms[$room_index]['cust_cost'] : (float)$booking_rooms[$room_index]['room_cost'];
		$upgrade_room_cost  = $current_room_cost;
		$upgrade_difference = 0;
		$upgtax_difference  = 0;

		if ($current_room_cost < $upgrade_room_rate['upgrade_cost']) {
			// we update the total booking amount only if the upgrade room is more expensive
			$upgrade_room_cost  = $upgrade_room_rate['upgrade_cost'];
			$upgrade_difference = $upgrade_room_rate['upgrade_cost'] - $current_room_cost;
			// handle taxes
			$current_tariff_data = VBORoomHelper::getInstance()->getTariffData($booking_rooms[$room_index]['idtar']);
			if ($current_tariff_data) {
				// current room tax
				$current_cost_plus_tax = VikBooking::sayCostPlusIva($current_room_cost, $current_tariff_data['idprice']);
				$current_cost_minus_tax = VikBooking::sayCostMinusIva($current_room_cost, $current_tariff_data['idprice']);
				$current_room_tax = $current_cost_plus_tax - $current_cost_minus_tax;
				// upgrade room tax
				$upgrade_cost_plus_tax = VikBooking::sayCostPlusIva($upgrade_room_rate['upgrade_cost'], $upgrade_room_rate['idprice']);
				$upgrade_cost_minus_tax = VikBooking::sayCostMinusIva($upgrade_room_rate['upgrade_cost'], $upgrade_room_rate['idprice']);
				$upgrade_room_tax = $upgrade_cost_plus_tax - $upgrade_cost_minus_tax;
				// calculate tax difference
				$upgtax_difference = $upgrade_room_tax - $current_room_tax;
			}
			
		}

		// update booking total amounts as first
		if ($upgrade_difference > 0) {
			$upd_booking = new stdClass;
			$upd_booking->id = $booking['id'];
			$upd_booking->total = (float)($booking['total'] + $upgrade_difference);
			$upd_booking->tot_taxes = (float)($booking['tot_taxes'] + $upgtax_difference);

			$dbo->updateObject('#__vikbooking_orders', $upd_booking, 'id');
		}

		// perform the room upgrade (switch)
		$broom_record = new stdClass;
		$broom_record->id = $booking_rooms[$room_index]['id'];
		$broom_record->idorder = $booking_rooms[$room_index]['idorder'];
		$broom_record->idroom = $room_id;
		$broom_record->idtar = $upgrade_room_rate['id'];
		$broom_record->room_cost = $upgrade_room_cost;

		$dbo->updateObject('#__vikbooking_ordersrooms', $broom_record, ['id', 'idorder']);

		// Booking History
		$hist_descr = !empty($booking_rooms[$room_index]['room_name']) ? $booking_rooms[$room_index]['room_name'] . ' ' : '';
		$hist_descr .= '-&gt; ' . $upgrade_options['rooms'][$room_id]['name'];
		VikBooking::getBookingHistoryInstance()->setBid($booking['id'])->store('UR', $hist_descr);

		// output the JSON encoded successful response
		VBOHttpDocument::getInstance()->json(['success' => 1]);
	}

	/**
	 * End-point used to ping the execution of the scheduled crontab runners.
	 * 
	 * @return  void
	 * 
	 * @since   1.17 (J) - 1.7 (WP)
	 */
	public function crontab()
	{
		VBOFactory::getCrontabSimulator()->run();
		exit;
	}

	/**
	 * End-point used to route at runtime the link to the requested room details page.
	 * Originally introduced to improve landing page URL accuracy with Google VR.
	 * 
	 * @return  void
	 * 
	 * @since   1.18.3 (J) - 1.8.3 (WP)
	 */
	public function route_listing_details()
	{
		$app = JFactory::getApplication();

		$listing_id = $app->input->getUint('listing_id', 0);

		if (!$listing_id) {
			VBOHttpDocument::getInstance($app)->close(400, 'Missing listing ID.');
		}

		// make sure the requested listing exists
		$listing_data = VikBooking::getRoomInfo($listing_id);

		if (!$listing_data) {
			VBOHttpDocument::getInstance($app)->close(404, 'Listing not found.');
		}

		if (empty($listing_data['avail'])) {
			VBOHttpDocument::getInstance($app)->close(404, 'Listing currently unavailable.');
		}

		// get user country and locale (preferred language), if available, to find the best language
		$user_country = $app->input->getString('country', '');
		$user_lang 	  = $app->input->getString('ulang', '');
		$user_country = empty($user_country) && !empty($user_lang) ? $user_lang : $user_country;
		$best_lang 	  = (!empty($user_country) || !empty($user_lang)) && class_exists('VikChannelManager') ? VikChannelManager::guessBookingLangFromCountry($user_country, $user_lang) : '';

		/**
		 * Adjust best language in case the default website lang is different than English and
		 * the visitor speaks a foreign language. This way we make English the best language.
		 * I.E. Website default lang is IT, visitor's lang is DE, we make it land on EN because DE isn't available.
		 */
		$current_lang = JFactory::getLanguage()->getTag();
		if (empty($best_lang) && !empty($user_lang) && substr(strtolower($user_lang), 0, 2) != 'en' && substr(strtolower($current_lang), 0, 2) != 'en') {
			// check if English is available
			foreach (VikBooking::getVboApplication()->getKnownLanguages() as $ltag => $ldet) {
				if (substr(strtolower($ltag), 0, 2) == 'en') {
					// grab this English-esque language
					$best_lang = $ltag;
					break;
				}
			}
		}

		// build query arguments for routing
		$route_query_args = [
			'option' => 'com_vikbooking',
			'view'   => 'roomdetails',
			'roomid' => $listing_id,
		];

		// route the best URI (if possible)
		try {
			// find the best page id for the URL according to the CMS
			$itemid = null;

			if (VBOPlatformDetection::isWordPress()) {
				/**
				 * @wponly  route the best Shortcode for the booking process ("room details" or "search form").
				 * 			the best booking language is passed over the model to find the best shortcode.
				 */
				$model 	= JModel::getInstance('vikbooking', 'shortcodes', 'admin');

				// grab all Shortcodes and parse them to find the best one for this room, if any
				$shortcodes = $model->all();
				foreach ($shortcodes as $shortcode) {
					if ($shortcode->type != 'roomdetails' || empty($shortcode->post_id)) {
						continue;
					}
					$page_params = json_decode($shortcode->json);
					if (!is_object($page_params) || !isset($page_params->roomid) || (int) $page_params->roomid != $listing_id) {
						continue;
					}
					if (!empty($best_lang) && $shortcode->lang == $best_lang) {
						// always give higher priority to the exact lang
						$itemid = $shortcode->post_id;
					}
					if (empty($itemid)) {
						// in case no perfect lang found, use this post id for this room-type
						$itemid = $shortcode->post_id;
					}
				}

				if (empty($itemid)) {
					// default to the best shortcode for this lang of type "search form"
					$itemid = $model->best(['vikbooking'], $best_lang);
					if (!$itemid) {
						// if we really get to this point, try to fetch the first non-empty shortcode, if any
						$shortcodes = $model->all($columns = 'post_id', $full = true);
						if ($shortcodes) {
							// grab the very first active shortcode, no matter what's the type of it
							$itemid = $shortcodes[0]->post_id;
						}
					}
				}
			} else {
				/**
				 * @joomlaonly  inject the language to the query arguments list before routing
				 */
				if (!empty($best_lang)) {
					$route_query_args['lang'] = $best_lang;
				}

				if (class_exists('VCMFactory')) {
					$best_menuitem_id = VCMFactory::getPlatform()->getPagerouter()->findProperPageId(['roomdetails'], ['roomid' => $room_type_id, 'lang' => $best_lang]);

					if ($best_menuitem_id) {
						$itemid = $best_menuitem_id;
					} else {
						$itemid = VCMFactory::getPlatform()->getPagerouter()->findProperPageId(['vikbooking'], $best_lang);
					}
				}
			}

			// route final URL with all arguments
			$routed_url = VikBooking::externalroute($route_query_args, false, $itemid);
		} catch (Throwable $e) {
			// raise an error
			VBOHttpDocument::getInstance($app)->close(500, 'Could not route to the requested page.');
		}

		// redirect the request to the proper page
		$app->redirect($routed_url);
		$app->close();
	}
}
