PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.6
VikBooking Hotel Booking Engine & PMS v1.8.6
1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.7.3 All 36 releases
vikbooking / site / controller.php

controller.php in VikBooking Hotel Booking Engine & PMS 1.8.6, at site/controller.php

5,164 lines 197.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage com_vikbooking
5 * @author Alessio Gaggii - e4j - Extensionsforjoomla.com
6 * @copyright Copyright (C) 2018 e4j - Extensionsforjoomla.com. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 * @link https://vikwp.com
9 */
10
11 defined('ABSPATH') or die('No script kiddies please!');
12
13 jimport('joomla.application.component.controller');
14
15 class VikBookingController extends JControllerVikBooking
16 {
17 public function display($cachable = false, $urlparams = array())
18 {
19 $view = VikRequest::getVar('view', '');
20 switch ($view) {
21 case 'roomslist':
22 case 'roomdetails':
23 case 'searchdetails':
24 case 'loginregister':
25 case 'orderslist':
26 case 'promotions':
27 case 'availability':
28 case 'packageslist':
29 case 'packagedetails':
30 case 'searchsuggestions':
31 case 'booking':
32 case 'operators':
33 case 'tableaux':
34 case 'precheckin':
35 case 'revstay':
36 case 'tinyurl':
37 VikRequest::setVar('view', $view);
38 break;
39 default:
40 VikRequest::setVar('view', 'vikbooking');
41 }
42 parent::display();
43 }
44
45 public function search()
46 {
47 VikRequest::setVar('view', 'search');
48 parent::display();
49 }
50
51 public function showprc()
52 {
53 VikRequest::setVar('view', 'showprc');
54 parent::display();
55 }
56
57 public function oconfirm()
58 {
59 $requirelogin = VikBooking::requireLogin();
60 if($requirelogin) {
61 if(VikBooking::userIsLogged()) {
62 VikRequest::setVar('view', 'oconfirm');
63 } else {
64 VikRequest::setVar('view', 'loginregister');
65 }
66 } else {
67 VikRequest::setVar('view', 'oconfirm');
68 }
69 parent::display();
70 }
71
72 public function register()
73 {
74 $mainframe = JFactory::getApplication();
75 $dbo = JFactory::getDBO();
76
77 //user data
78 $pname = VikRequest::getString('fname', '', 'request');
79 $plname = VikRequest::getString('lname', '', 'request');
80 $pemail = VikRequest::getString('email', '', 'request');
81 $pusername = VikRequest::getString('username', '', 'request');
82 $ppassword = VikRequest::getString('password', '', 'request');
83 $pconfpassword = VikRequest::getString('confpassword', '', 'request');
84 //
85 //order data
86 $pitemid = VikRequest::getString('Itemid', '', 'request');
87 $proomid = VikRequest::getVar('roomid', array());
88 $pdays = VikRequest::getInt('days', '', 'request');
89 $pcheckin = VikRequest::getInt('checkin', '', 'request');
90 $pcheckout = VikRequest::getInt('checkout', '', 'request');
91 $proomsnum = VikRequest::getInt('roomsnum', '', 'request');
92 $padults = VikRequest::getVar('adults', array());
93 $pchildren = VikRequest::getVar('children', array());
94 $rooms = array();
95 $arrpeople = array();
96 for($ir = 1; $ir <= $proomsnum; $ir++) {
97 $ind = $ir - 1;
98 if (!empty($proomid[$ind])) {
99 $q = "SELECT * FROM `#__vikbooking_rooms` WHERE `id`='".intval($proomid[$ind])."' AND `avail`='1';";
100 $dbo->setQuery($q);
101 $dbo->execute();
102 if ($dbo->getNumRows() > 0) {
103 $takeroom = $dbo->loadAssocList();
104 $rooms[$ir] = $takeroom[0];
105 }
106 }
107 if (!empty($padults[$ind])) {
108 $arrpeople[$ir]['adults'] = intval($padults[$ind]);
109 } else {
110 $arrpeople[$ir]['adults'] = 0;
111 }
112 if (!empty($pchildren[$ind])) {
113 $arrpeople[$ir]['children'] = intval($pchildren[$ind]);
114 } else {
115 $arrpeople[$ir]['children'] = 0;
116 }
117 }
118 $prices = array();
119 foreach($rooms as $num => $r) {
120 $ppriceid = VikRequest::getString('priceid'.$num, '', 'request');
121 if (!empty($ppriceid)) {
122 $prices[$num] = intval($ppriceid);
123 }
124 }
125 $selopt = array();
126 $q = "SELECT * FROM `#__vikbooking_optionals` ORDER BY `#__vikbooking_optionals`.`ordering` ASC;";
127 $dbo->setQuery($q);
128 $dbo->execute();
129 if ($dbo->getNumRows() > 0) {
130 $optionals = $dbo->loadAssocList();
131 foreach ($rooms as $num => $r) {
132 foreach ($optionals as $opt) {
133 if (!empty($opt['ageintervals']) && $arrpeople[$num]['children'] > 0) {
134 $tmpvar = VikRequest::getInt('optid'.$num.$opt['id'], []);
135 if (is_array($tmpvar) && $tmpvar) {
136 $optagenames = VikBooking::getOptionIntervalsAges($opt['ageintervals']);
137 $optagepcent = VikBooking::getOptionIntervalsPercentage($opt['ageintervals']);
138 $optageovrct = VikBooking::getOptionIntervalChildOverrides($opt, $arrpeople[$num]['adults'], $arrpeople[$num]['children']);
139 $optorigname = $opt['name'];
140 foreach ($tmpvar as $child_num => $chvar) {
141 $opt['quan'] = $chvar;
142 $opt['chageintv'] = $chvar;
143 //ignore calculation as percetage value to reconstruct the URL
144 $ageintervals_child_string = isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $opt['ageintervals'];
145 $optagecosts = VikBooking::getOptionIntervalsCosts($ageintervals_child_string);
146 $opt['cost'] = $optagecosts[($chvar - 1)];
147 $opt['name'] = $optorigname.' ('.$optagenames[($chvar - 1)].')';
148 $selopt[$num][] = $opt;
149 }
150 }
151 } else {
152 $tmpvar = VikRequest::getString('optid'.$num.$opt['id'], '', 'request');
153 if (!empty($tmpvar)) {
154 $opt['quan'] = $tmpvar;
155 $selopt[$num][] = $opt;
156 }
157 }
158 }
159 }
160 }
161 $strpriceid = "";
162 foreach($prices as $num => $pid) {
163 $strpriceid .= ($num > 1 ? "&" : "")."priceid".$num."=".$pid;
164 }
165 $stroptid = "";
166 for($ir = 1; $ir <= $proomsnum; $ir++) {
167 if (isset($selopt[$ir]) && is_array($selopt[$ir])) {
168 foreach($selopt[$ir] as $opt) {
169 if (array_key_exists('chageintv', $opt)) {
170 $stroptid .= "&optid".$ir.$opt['id']."[]=".$opt['chageintv'];
171 } else {
172 $stroptid .= "&optid".$ir.$opt['id']."=".$opt['quan'];
173 }
174 }
175 }
176 }
177 $strroomid = "";
178 foreach ($rooms as $num => $r) {
179 $strroomid .= "&roomid[]=".$r['id'];
180 }
181 $straduchild = "";
182 foreach ($arrpeople as $indroom => $aduch) {
183 $straduchild .= "&adults[]=".$aduch['adults'];
184 $straduchild .= "&children[]=".$aduch['children'];
185 }
186
187 $qstring = $strpriceid.$stroptid.$strroomid.$straduchild."&roomsnum=".$proomsnum."&days=".$pdays."&checkin=".$pcheckin."&checkout=".$pcheckout.(!empty($pitemid) ? "&Itemid=".$pitemid : "");
188 //
189 if (!VikBooking::userIsLogged()) {
190 if (!empty($pname) && !empty($plname) && !empty($pusername) && !empty($pemail) && $ppassword == $pconfpassword) {
191 //save user
192 $newuserid=VikBooking::addJoomlaUser($pname." ".$plname, $pusername, $pemail, $ppassword);
193
194 if ($newuserid!=false && strlen($newuserid)) {
195
196 /**
197 * @wponly the return URL should be passed within the $option array of $app->login()
198 */
199 $redirect_to = JRoute::rewrite('index.php?option=com_vikbooking&task=oconfirm&'.$qstring, false);
200
201 //registration success
202 $credentials = array('username' => $pusername, 'password' => $ppassword );
203 //autologin
204 $mainframe->login($credentials, array('redirect' => $redirect_to));
205 $currentUser = JFactory::getUser();
206 $currentUser->setLastVisit(time());
207 $currentUser->set('guest', 0);
208 //
209 $mainframe->redirect($redirect_to);
210 } else {
211 //error while saving new user
212 VikError::raiseWarning('', JText::translate('VBREGERRSAVING'));
213 $mainframe->redirect(JRoute::rewrite('index.php?option=com_vikbooking&view=loginregister&'.$qstring, false));
214 }
215 } else {
216 //invalid data
217 VikError::raiseWarning('', JText::translate('VBREGERRINSDATA'));
218 $mainframe->redirect(JRoute::rewrite('index.php?option=com_vikbooking&view=loginregister&'.$qstring, false));
219 }
220 } else {
221 //user is already logged in, proceed
222 $mainframe->redirect(JRoute::rewrite('index.php?option=com_vikbooking&task=oconfirm&'.$qstring, false));
223 }
224 }
225
226 public function saveorder()
227 {
228 $dbo = JFactory::getDbo();
229 $session = JFactory::getSession();
230 $app = JFactory::getApplication();
231 $vbo_tn = VikBooking::getTranslator();
232
233 // availability helper
234 $av_helper = VikBooking::getAvailabilityInstance();
235
236 $prooms = VikRequest::getVar('rooms', array());
237 $proomindex = VikRequest::getVar('roomindex', array());
238 $proomsnum = VikRequest::getInt('roomsnum', 0, 'request');
239 $padults = VikRequest::getVar('adults', array());
240 $pchildren = VikRequest::getVar('children', array());
241 $pdays = VikRequest::getInt('days', 0, 'request');
242 $pcouponcode = VikRequest::getString('couponcode', '', 'request');
243 $pcheckin = VikRequest::getInt('checkin', 0, 'request');
244 $pcheckout = VikRequest::getInt('checkout', 0, 'request');
245 $pprtar = VikRequest::getVar('prtar', array());
246 $ppriceid = VikRequest::getVar('priceid', array());
247 $poptionals = VikRequest::getString('optionals', '', 'request');
248 $ptotdue = VikRequest::getString('totdue', '', 'request');
249 $pgpayid = VikRequest::getString('gpayid', '', 'request');
250 $ppkg_id = VikRequest::getInt('pkg_id', '', 'request');
251 $pnodep = VikRequest::getInt('nodep', '', 'request');
252 $split_stay = VikRequest::getVar('split_stay', array());
253 $pitemid = VikRequest::getInt('Itemid', '', 'request');
254
255 $validtoken = true;
256 if (VikBooking::tokenForm()) {
257 $validtoken = false;
258 $pviktoken = VikRequest::getString('viktoken', '', 'request');
259 $sessvbtkn = $session->get('vikbtoken', '');
260 if (!empty($pviktoken) && $sessvbtkn == $pviktoken) {
261 $session->set('vikbtoken', '');
262 $validtoken = true;
263 }
264 if (!$validtoken) {
265 $validtoken = JSession::checkToken();
266 }
267 }
268
269 if (!$validtoken) {
270 showSelectVb(JText::translate('VBINVALIDTOKEN'));
271 return;
272 }
273
274 $q = "SELECT * FROM `#__vikbooking_custfields` ORDER BY `#__vikbooking_custfields`.`ordering` ASC;";
275 $dbo->setQuery($q);
276 $cfields = $dbo->loadAssocList();
277
278 $suffdata = true;
279 $useremail = "";
280 $usercountry = '';
281 $nominatives = [];
282 $t_first_name = '';
283 $t_last_name = '';
284 $phone_number = '';
285 $fieldflags = [];
286 if ($cfields) {
287 $vbo_tn->translateContents($cfields, '#__vikbooking_custfields');
288 foreach ($cfields as $cf) {
289 if (intval($cf['required']) == 1 && $cf['type'] != 'separator' && $cf['type'] != 'state') {
290 $tmpcfval = VikRequest::getString('vbf' . $cf['id'], '', 'request');
291 if (!strlen(str_replace(' ', '', trim($tmpcfval)))) {
292 $suffdata = false;
293 break;
294 }
295 }
296 }
297 //save user email, nominatives, phone number and create custdata array
298 $arrcustdata = [];
299 $arrcfields = [];
300 $emailwasfound = false;
301 foreach ($cfields as $cf) {
302 $user_inp_val = VikRequest::getString('vbf' . $cf['id'], '', 'request');
303 if (intval($cf['isemail']) == 1 && $emailwasfound == false) {
304 $useremail = trim($user_inp_val);
305 $emailwasfound = true;
306 }
307 if ($cf['isnominative'] == 1) {
308 if (strlen(str_replace(' ', '', trim($user_inp_val)))) {
309 $nominatives[] = $user_inp_val;
310 }
311 }
312 if ($cf['isphone'] == 1) {
313 if (strlen(str_replace(' ', '', trim($user_inp_val)))) {
314 $phone_number = $user_inp_val;
315 }
316 }
317 if (!empty($cf['flag'])) {
318 if (strlen(str_replace(' ', '', trim($user_inp_val)))) {
319 $fieldflags[$cf['flag']] = $user_inp_val;
320 }
321 }
322 if ($cf['type'] != 'separator' && $cf['type'] != 'country' && ( $cf['type'] != 'checkbox' || ($cf['type'] == 'checkbox' && intval($cf['required']) != 1) ) ) {
323 // check the input value to store for the customer raw information string
324 $def_user_inp_val = $user_inp_val;
325 // check for state/province field
326 if ($cf['type'] == 'state' && strlen(str_replace(' ', '', trim($user_inp_val)))) {
327 /**
328 * In order to assign the proper state/province to the customer,
329 * we treat this type of field as if it was a "field flag" type.
330 *
331 * @since 1.16.0 (J) - 1.6.0 (WP)
332 */
333 $fieldflags['state'] = $user_inp_val;
334
335 // attempt to save the full state name, not the 2-char code
336 $def_user_inp_val = VBOStateHelper::getFullName($user_inp_val, $usercountry);
337 }
338 $arrcustdata[JText::translate($cf['name'])] = $def_user_inp_val;
339
340 // store the original input value for this custom field ID
341 $arrcfields[$cf['id']] = $user_inp_val;
342 } elseif ($cf['type'] == 'country') {
343 $countryval = $user_inp_val;
344 if (!empty($countryval) && strstr($countryval, '::') !== false) {
345 $countryparts = explode('::', $countryval);
346 $usercountry = $countryparts[0];
347 $arrcustdata[JText::translate($cf['name'])] = $countryparts[1];
348 } else {
349 $arrcustdata[JText::translate($cf['name'])] = '';
350 }
351 }
352 }
353 }
354 if (!empty($phone_number) && !empty($usercountry)) {
355 $phone_number = VikBooking::checkPhonePrefixCountry($phone_number, $usercountry);
356 }
357
358 if ($suffdata !== true) {
359 showSelectVb(JText::translate('VBINSUFDATA'));
360 return;
361 }
362
363 if (count($nominatives) >= 2) {
364 $t_last_name = array_pop($nominatives);
365 $t_first_name = array_pop($nominatives);
366 }
367
368 $secdiff = $pcheckout - $pcheckin;
369 $daysdiff = $secdiff / 86400;
370 if (is_int($daysdiff)) {
371 if ($daysdiff < 1) {
372 $daysdiff = 1;
373 }
374 } else {
375 if ($daysdiff < 1) {
376 $daysdiff = 1;
377 } else {
378 $sum = floor($daysdiff) * 86400;
379 $newdiff = $secdiff - $sum;
380 $maxhmore = VikBooking::getHoursMoreRb() * 3600;
381 if ($maxhmore >= $newdiff) {
382 $daysdiff = floor($daysdiff);
383 } else {
384 $daysdiff = ceil($daysdiff);
385 }
386 }
387 }
388
389 if (!VikBooking::dayValidTs($pdays, $pcheckin, $pcheckout) || $pdays != $daysdiff) {
390 showSelectVb(JText::translate('VBINCONGRDATA'));
391 return;
392 }
393
394 // get check-in and check-out dates information
395 $checkin_info = getdate($pcheckin);
396 $checkout_info = getdate($pcheckout);
397
398 /**
399 * Check split stay information.
400 *
401 * @since 1.16.0 (J) - 1.6.0 (WP)
402 */
403 if (!empty($split_stay) && count($split_stay) == count($prooms) && count($split_stay) == $proomsnum && $proomsnum > 1) {
404 // valid split stay request vars received
405 $split_stay_checkins = [];
406 $split_stay_checkouts = [];
407 $split_stay_nights = [];
408 foreach ($split_stay as $sps_k => $split_room) {
409 // calculate and set the exact check-in and check-out timestamps for this split-room
410 $room_checkin = VikBooking::getDateTimestamp($split_room['checkin'], $checkin_info['hours'], $checkin_info['minutes'], $checkin_info['seconds']);
411 $room_checkout = VikBooking::getDateTimestamp($split_room['checkout'], $checkout_info['hours'], $checkout_info['minutes'], $checkout_info['seconds']);
412 $split_stay_checkins[] = $room_checkin;
413 $split_stay_checkouts[] = $room_checkout;
414 // update split stay information
415 $split_room['checkin_ts'] = $room_checkin;
416 $split_room['checkout_ts'] = $room_checkout;
417 $split_room['nights'] = $av_helper->countNightsOfStay($room_checkin, $room_checkout);
418 $split_stay_nights[] = $split_room['nights'];
419 $split_stay[$sps_k] = $split_room;
420 }
421 // validate minimum and maximum stay dates for the split stay
422 if (empty($split_stay_checkins) || empty($split_stay_checkouts)) {
423 // error
424 showSelectVb('Empty stay dates for split stay rooms');
425 return;
426 }
427 if (array_sum($split_stay_nights) != $daysdiff) {
428 showSelectVb('Invalid sum of total nights for split stay rooms');
429 return;
430 }
431 if (min($split_stay_checkins) != $pcheckin) {
432 // error
433 showSelectVb('Invalid checkin stay date for split stay rooms');
434 return;
435 }
436 if (max($split_stay_checkouts) != $pcheckout) {
437 // error
438 showSelectVb('Invalid checkout stay date for split stay rooms');
439 return;
440 }
441 } else {
442 // unset any possible value as it's invalid
443 $split_stay = [];
444 }
445
446 $currencyname = VikBooking::getCurrencyName();
447 $rooms = [];
448 $prices = [];
449 $arrpeople = [];
450 for ($ir = 1; $ir <= $proomsnum; $ir++) {
451 $ind = $ir - 1;
452 if (!empty($prooms[$ind])) {
453 $q = "SELECT * FROM `#__vikbooking_rooms` WHERE `id`=" . (int)$prooms[$ind] . " AND `avail`='1';";
454 $dbo->setQuery($q);
455 $rdata = $dbo->loadAssoc();
456 if ($rdata) {
457 $rooms[$ir] = $rdata;
458 }
459 }
460 if (!empty($padults[$ind])) {
461 $arrpeople[$ir]['adults'] = intval($padults[$ind]);
462 } else {
463 $arrpeople[$ir]['adults'] = 0;
464 }
465 if (!empty($pchildren[$ind])) {
466 $arrpeople[$ir]['children'] = intval($pchildren[$ind]);
467 } else {
468 $arrpeople[$ir]['children'] = 0;
469 }
470 $arrpeople[$ir]['pets'] = 0;
471 $prices[$ir] = intval($ppriceid[$ind]);
472 }
473 if (count($rooms) != $proomsnum) {
474 VikError::raiseWarning('', JText::translate('VBROOMNOTFND'));
475 $app->redirect(JRoute::rewrite('index.php?option=com_vikbooking'));
476 exit;
477 }
478 $vbo_tn->translateContents($rooms, '#__vikbooking_rooms');
479
480 // package
481 $pkg = [];
482 if (!empty($ppkg_id)) {
483 $pkg = VikBooking::validateRoomPackage($ppkg_id, $rooms, $daysdiff, $pcheckin, $pcheckout);
484 if (!is_array($pkg) || (is_array($pkg) && !(count($pkg) > 0)) ) {
485 if (!is_array($pkg)) {
486 VikError::raiseWarning('', $pkg);
487 }
488 $app->redirect(JRoute::rewrite("index.php?option=com_vikbooking&view=packagedetails&pkgid=".$ppkg_id.(!empty($pitemid) ? "&Itemid=".$pitemid : ""), false));
489 exit;
490 }
491 }
492
493 $tars = [];
494 $validfares = true;
495 foreach ($rooms as $num => $r) {
496 if (count($pkg)) {
497 break;
498 }
499
500 // determine the number of nights of stay and dates to consider
501 $use_los = (int)$daysdiff;
502 $room_checkin = $pcheckin;
503 $room_checkout = $pcheckout;
504 if (!empty($split_stay) && !empty($split_stay[($num - 1)]) && $split_stay[($num - 1)]['idroom'] == $r['id']) {
505 $use_los = (int)$split_stay[($num - 1)]['nights'];
506 $room_checkin = $split_stay[($num - 1)]['checkin_ts'];
507 $room_checkout = $split_stay[($num - 1)]['checkout_ts'];
508 }
509
510 $q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `idroom`=" . (int)$r['id'] . " AND `days`=" . $use_los . " AND `idprice`=" . $prices[$num];
511 $dbo->setQuery($q, 0, 1);
512 $dbo->execute();
513 if (!$dbo->getNumRows()) {
514 $validfares = false;
515 break;
516 }
517 $tar = $dbo->loadAssocList();
518
519 // apply seasonal rates
520 $tar = VikBooking::applySeasonsRoom($tar, $room_checkin, $room_checkout);
521
522 // apply OBP rules
523 $tar = VBORoomHelper::getInstance()->applyOBPRules($tar, $r, $arrpeople[$num]['adults']);
524
525 // push room tariffs
526 $tars[$num] = $tar;
527 }
528
529 if ($validfares !== true) {
530 showSelectVb(JText::translate('VBINCONGRDATAREC'));
531 return;
532 }
533
534 $isdue = 0;
535 $tot_taxes = 0;
536 $tot_city_taxes = 0;
537 $tot_fees = 0;
538 $tot_damage_dep = 0;
539 $rooms_costs_map = [];
540 $is_package = (bool)(count($pkg) > 0);
541 if ($is_package === true) {
542 foreach ($rooms as $num => $r) {
543 $pkg_cost = $pkg['pernight_total'] == 1 ? ($pkg['cost'] * $daysdiff) : $pkg['cost'];
544 $pkg_cost = $pkg['perperson'] == 1 ? ($pkg_cost * ($arrpeople[$num]['adults'] > 0 ? $arrpeople[$num]['adults'] : 1)) : $pkg_cost;
545 $cost_plus_tax = VikBooking::sayPackagePlusIva($pkg_cost, $pkg['idiva']);
546 $isdue += $cost_plus_tax;
547 if ($cost_plus_tax == $pkg_cost) {
548 $cost_minus_tax = VikBooking::sayPackageMinusIva($pkg_cost, $pkg['idiva']);
549 $tot_taxes += ($pkg_cost - $cost_minus_tax);
550 } else {
551 $tot_taxes += ($cost_plus_tax - $pkg_cost);
552 }
553 }
554 } else {
555 foreach ($tars as $num => $tar) {
556 $cost_plus_tax = VikBooking::sayCostPlusIva($tar[0]['cost'], $tar[0]['idprice']);
557 $isdue += $cost_plus_tax;
558 if ($cost_plus_tax == $tar[0]['cost']) {
559 $cost_minus_tax = VikBooking::sayCostMinusIva($tar[0]['cost'], $tar[0]['idprice']);
560 $tot_taxes += ($tar[0]['cost'] - $cost_minus_tax);
561 } else {
562 $tot_taxes += ($cost_plus_tax - $tar[0]['cost']);
563 }
564 $rooms_costs_map[$num] = $tar[0]['cost'];
565 }
566 }
567
568 /**
569 * Custom check-in/out times due to late check-out/early check-in options or custom listing settings.
570 *
571 * @since 1.17.2 (J) - 1.7.2 (WP)
572 * @since 1.18.3 (J) - 1.8.3 (WP) added support to check-in/out times at listing-level.
573 */
574 $custom_checkinout = [];
575 if (count($rooms) === 1) {
576 $listing_custom_checkin = VikBooking::getRoomParam('checkin', ($rooms[(key($rooms))]['params'] ?? ''));
577 $listing_custom_checkout = VikBooking::getRoomParam('checkout', ($rooms[(key($rooms))]['params'] ?? ''));
578 if ($listing_custom_checkin) {
579 // set listing-level check-in time in seconds
580 $listing_custom_checkin_parts = explode(':', $listing_custom_checkin);
581 $listing_custom_checkin = (intval($listing_custom_checkin_parts[0]) * 3600) + (intval($listing_custom_checkin_parts[1]) * 60);
582 }
583 if ($listing_custom_checkout) {
584 // set listing-level check-out time in seconds
585 $listing_custom_checkout_parts = explode(':', $listing_custom_checkout);
586 $listing_custom_checkout = (intval($listing_custom_checkout_parts[0]) * 3600) + (intval($listing_custom_checkout_parts[1]) * 60);
587 }
588 if ($listing_custom_checkin || $listing_custom_checkout) {
589 // set listing-level check-in/out times in seconds
590 $custom_checkinout = [
591 (int) $listing_custom_checkin,
592 (int) $listing_custom_checkout,
593 ];
594 }
595 }
596
597 $selopt = [];
598 $optstr = [];
599 $children_age = [];
600 if (!empty($poptionals)) {
601 $stepo = explode(";", $poptionals);
602 foreach ($stepo as $roptkey => $oo) {
603 if (empty($oo)) {
604 continue;
605 }
606 $stept = explode(":", $oo);
607 $rnoid = explode("_", $stept[0]);
608 $room_ind = $rnoid[0] - 1;
609
610 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . (int)$rnoid[1];
611 $dbo->setQuery($q, 0, 1);
612 $actopt = $dbo->loadAssocList();
613 if (!$actopt) {
614 continue;
615 }
616 $vbo_tn->translateContents($actopt, '#__vikbooking_optionals');
617
618 // option params
619 $opt_params = !empty($actopt[0]['oparams']) ? json_decode($actopt[0]['oparams'], true) : [];
620 $opt_params = is_array($opt_params) ? $opt_params : [];
621
622 // determine the number of nights of stay and dates to consider
623 $use_los = (int)$daysdiff;
624 $room_checkin = $pcheckin;
625 $room_checkout = $pcheckout;
626 if (!empty($split_stay) && !empty($split_stay[$room_ind])) {
627 $use_los = (int)$split_stay[$room_ind]['nights'];
628 $room_checkin = $split_stay[$room_ind]['checkin_ts'];
629 $room_checkout = $split_stay[$room_ind]['checkout_ts'];
630 }
631
632 $chvar = '';
633 if (!empty($actopt[0]['ageintervals']) && $arrpeople[$rnoid[0]]['children'] > 0 && strstr($stept[1], '-') != false) {
634 $optagenames = VikBooking::getOptionIntervalsAges($actopt[0]['ageintervals']);
635 $optagepcent = VikBooking::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
636 $optageovrct = VikBooking::getOptionIntervalChildOverrides($actopt[0], $arrpeople[$rnoid[0]]['adults'], $arrpeople[$rnoid[0]]['children']);
637 $child_num = VikBooking::getRoomOptionChildNumber($poptionals, $actopt[0]['id'], $roptkey, $arrpeople[$rnoid[0]]['children']);
638 $optagecosts = VikBooking::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
639 $agestept = explode('-', $stept[1]);
640 $stept[1] = $agestept[0];
641 $chvar = $agestept[1];
642 if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
643 //percentage value of the adults tariff
644 if ($is_package === true) {
645 $optagecosts[($chvar - 1)] = ($pkg['pernight_total'] == 1 ? ($pkg['cost'] * $daysdiff) : $pkg['cost']) * $optagecosts[($chvar - 1)] / 100;
646 } else {
647 $optagecosts[($chvar - 1)] = $tars[$rnoid[0]][0]['cost'] * $optagecosts[($chvar - 1)] / 100;
648 }
649 } elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
650 //VBO 1.10 - percentage value of room base cost
651 if ($is_package === true) {
652 $optagecosts[($chvar - 1)] = ($pkg['pernight_total'] == 1 ? ($pkg['cost'] * $daysdiff) : $pkg['cost']) * $optagecosts[($chvar - 1)] / 100;
653 } else {
654 $display_rate = isset($tars[$rnoid[0]][0]['room_base_cost']) ? $tars[$rnoid[0]][0]['room_base_cost'] : $tars[$rnoid[0]][0]['cost'];
655 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
656 }
657 }
658 $actopt[0]['chageintv'] = $chvar;
659 $actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
660 $actopt[0]['quan'] = $stept[1];
661 $selopt[$rnoid[0]][] = $actopt[0];
662 $selopt['room'.$rnoid[0]] = $selopt['room'.$rnoid[0]].$actopt[0]['id'].":".$stept[1]."-".$chvar.";";
663 $realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $use_los * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
664 $children_age[$rnoid[0]][] = array('ageinterval' => $optagenames[($chvar - 1)], 'age' => '', 'cost' => $realcost);
665 } else {
666 $actopt[0]['quan'] = $stept[1];
667 // VBO 1.11 - options percentage cost of the room total fee
668 if ($is_package === true) {
669 $deftar_basecosts = $pkg['pernight_total'] == 1 ? ($pkg['cost'] * $daysdiff) : $pkg['cost'];
670 } else {
671 $deftar_basecosts = $tars[$rnoid[0]][0]['cost'];
672 }
673 $actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
674 //
675 $selopt[$rnoid[0]][] = $actopt[0];
676 if (!isset($selopt['room'.$rnoid[0]])) {
677 $selopt['room'.$rnoid[0]] = '';
678 }
679 $selopt['room'.$rnoid[0]] .= $actopt[0]['id'] . ":" . $stept[1] . ";";
680 $realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $use_los * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
681 }
682 if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
683 $realcost = $actopt[0]['maxprice'];
684 if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
685 $realcost = $actopt[0]['maxprice'] * $stept[1];
686 }
687 }
688
689 /**
690 * Count pets, if any.
691 *
692 * @since 1.16.2 (J) - 1.6.2 (WP)
693 */
694 if ($opt_params['pet_fee'] ?? 0) {
695 $tot_pets = 1;
696 if ($actopt[0]['hmany'] > 0 && $stept[1] > 1) {
697 $tot_pets = (int)$stept[1];
698 }
699 $arrpeople[$rnoid[0]]['pets'] = $tot_pets;
700 }
701
702 /**
703 * Custom check-in/out times due to late check-out/early check-in options.
704 *
705 * @since 1.17.2 (J) - 1.7.2 (WP)
706 */
707 if (($opt_params['custom_checkinout'] ?? 0) && (($opt_params['set_checkin'] ?? 0) || ($opt_params['set_checkout'] ?? 0))) {
708 $custom_checkinout = [
709 ($opt_params['set_checkin'] ?? 0),
710 ($opt_params['set_checkout'] ?? 0),
711 ];
712 }
713
714 $realcost = ($actopt[0]['perperson'] == 1 ? ($realcost * $arrpeople[$rnoid[0]]['adults']) : $realcost);
715
716 /**
717 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
718 *
719 * @since 1.17.7 (J) - 1.7.7 (WP)
720 */
721 $use_room_cost = $is_package === true ? $pkg['cost'] : ($tars[$rnoid[0]][0]['cost'] ?? 0);
722 $custom_calc_booking = ['days' => $use_los];
723 $custom_calc_booking_room = array_merge($arrpeople[$rnoid[0]], ['room_cost' => $use_room_cost]);
724 $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$realcost, &$actopt[0], $custom_calc_booking, $custom_calc_booking_room]);
725 if ($custom_calculation) {
726 $realcost = (float) $custom_calculation[0];
727 }
728
729 $opt_minus_iva = VikBooking::sayOptionalsMinusIva($realcost, $actopt[0]['idiva']);
730 $tmpopr = VikBooking::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
731 if ($actopt[0]['is_citytax'] == 1) {
732 $tot_city_taxes += $opt_minus_iva;
733 } elseif ($actopt[0]['is_fee'] == 1) {
734 $tot_fees += $opt_minus_iva;
735 } elseif ($opt_params['damagedep'] ?? 0) {
736 $tot_damage_dep += $opt_minus_iva;
737 }
738 // VBO 1.11 - always calculate the amount of tax no matter if this is already a tax or a fee
739 if ($tmpopr == $realcost) {
740 $tot_taxes += ($realcost - $opt_minus_iva);
741 } else {
742 $tot_taxes += ($tmpopr - $realcost);
743 }
744 //
745 $isdue += $tmpopr;
746 $optstr[$rnoid[0]][] = ($stept[1] > 1 ? $stept[1] . " " : "") . $actopt[0]['name'] . ": " . $tmpopr . " " . $currencyname . "\n";
747 }
748 }
749
750 $origtotdue = $isdue;
751 $usedcoupon = false;
752 $strcouponeff = '';
753
754 // access current customer
755 $cpin = VikBooking::getCPinIstance();
756 $customer_details = $cpin->loadCustomerDetails();
757
758 // coupon
759 if (strlen($pcouponcode) && $is_package !== true) {
760 $coupon = VikBooking::getCouponInfo($pcouponcode);
761 $valid_customer_coupon = true;
762 if (!empty($coupon) && !empty($coupon['customers'])) {
763 if (empty($customer_details['id']) || !in_array($customer_details['id'], $coupon['customers'])) {
764 $valid_customer_coupon = false;
765 }
766 }
767 if (!empty($coupon) && $valid_customer_coupon) {
768 $coupondateok = true;
769 if (strlen((string)$coupon['datevalid'])) {
770 $dateparts = explode("-", $coupon['datevalid']);
771 $pickinfo = $checkin_info;
772 $dropinfo = $checkout_info;
773 $checkpick = mktime(0, 0, 0, $pickinfo['mon'], $pickinfo['mday'], $pickinfo['year']);
774 $checkdrop = mktime(0, 0, 0, $dropinfo['mon'], $dropinfo['mday'], $dropinfo['year']);
775 if (!($checkpick >= $dateparts[0] && $checkpick <= $dateparts[1] && $checkdrop >= $dateparts[0] && $checkdrop <= $dateparts[1])) {
776 $coupondateok = false;
777 }
778 }
779 if (!empty($coupon['minlos']) && $coupon['minlos'] > $daysdiff) {
780 $coupondateok = false;
781 }
782 if ($coupondateok) {
783 $couponroomok = true;
784 if (!$coupon['allvehicles']) {
785 foreach ($rooms as $num => $r) {
786 if (!(preg_match("/;".$r['id'].";/i", $coupon['idrooms']))) {
787 $couponroomok = false;
788 break;
789 }
790 }
791 }
792 if ($couponroomok) {
793 $coupontotok = true;
794 if (strlen((string)$coupon['mintotord'])) {
795 if ($isdue < $coupon['mintotord']) {
796 $coupontotok = false;
797 }
798 }
799 if ($coupon['maxtotord'] > 0 && $isdue > $coupon['maxtotord']) {
800 $coupontotok = false;
801 }
802
803 /**
804 * Trigger event to allow third-party plugins to implement additional coupon validations or manipulation.
805 *
806 * @since 1.16.7 (J) - 1.6.7 (WP)
807 */
808 if ($coupontotok) {
809 $coupon_validation = VBOFactory::getPlatform()->getDispatcher()->filter('onValidateCouponCode', [&$coupon]);
810 if (is_array($coupon_validation) && in_array(false, $coupon_validation, true)) {
811 $coupontotok = false;
812 }
813 }
814
815 if ($coupontotok) {
816 $usedcoupon = true;
817 if ($coupon['percentot'] == 1) {
818 // percent value
819 $minuscoupon = 100 - $coupon['value'];
820 /**
821 * We allow coupon codes to be applied on the entire reservation or as always just on the total minus mandatory taxes.
822 *
823 * @since 1.13.5 (J) - 1.3.5 (WP)
824 * @since 1.14.3 (J) - 1.4.3 (WP) we also exclude the amount of taxes beside the mandatory fees.
825 * @since 1.16.0 (J) - 1.6.0 (WP) taxes are proportionally calculated when coupon before tax.
826 * @since 1.16.8 (J) - 1.6.8 (WP) with coupon before taxes, discounted amount calculation is an equal subtration.
827 */
828 $prev_isdue = $isdue;
829 $tot_net = ($isdue - $tot_taxes - $tot_city_taxes - $tot_fees - $tot_damage_dep);
830 $coupondiscount = ($coupon['excludetaxes'] ? $tot_net : $isdue) * $coupon['value'] / 100;
831 $isdue = ($coupon['excludetaxes'] ? $tot_net : $isdue) * $minuscoupon / 100;
832 $tot_taxes = $coupon['excludetaxes'] ? ($tot_taxes * ($tot_net - $coupondiscount) / $tot_net) : $tot_taxes;
833 $isdue += $coupon['excludetaxes'] ? ($tot_taxes + $tot_city_taxes + $tot_fees + $tot_damage_dep) : 0;
834 $coupondiscount = abs($prev_isdue - $isdue);
835 } else {
836 // total value
837 $coupondiscount = $coupon['value'];
838 // isdue : taxes = coupon_discount : x
839 $tax_prop = $tot_taxes * $coupon['value'] / $isdue;
840 $tot_taxes -= $tax_prop;
841 $tot_taxes = $tot_taxes < 0 ? 0 : $tot_taxes;
842 $isdue -= $coupon['value'];
843 $isdue = $isdue < 0 ? 0 : $isdue;
844 }
845 $strcouponeff = $coupon['id'].';'.$coupondiscount.';'.$coupon['code'];
846 }
847 }
848 }
849 }
850 }
851
852 $strisdue = number_format($isdue, 2) . 'vikbooking';
853 $ptotdue = number_format($ptotdue, 2) . 'vikbooking';
854 if ($strisdue != $ptotdue && abs(round($isdue, 2) - round((float) $ptotdue, 2)) <= 0.01) {
855 showSelectVb(JText::translate('VBINCONGRTOT'));
856 return;
857 }
858
859 // pay full amount cookie (2 weeks)
860 $nodep_set = !empty($pnodep) ? '1' : '0';
861 $nodep_time_set = !empty($pnodep) ? (time() + (86400 * 14)) : (time() - (86400 * 14));
862 $cookie = JFactory::getApplication()->input->cookie;
863 VikRequest::setCookie('vboFA', $nodep_set, $nodep_time_set, '/');
864
865 // modify booking
866 $mod_booking = [];
867 $skip_busy_ids = [];
868 $cur_mod = $session->get('vboModBooking', '');
869 if (is_array($cur_mod) && $cur_mod) {
870 $mod_booking = $cur_mod;
871 $skip_busy_ids = VikBooking::loadBookingBusyIds($mod_booking['id']);
872 }
873
874 $nowts = time();
875 $checkts = $nowts;
876 $today_bookings = VikBooking::todayBookings();
877 if ($today_bookings) {
878 $checkts = mktime(0, 0, 0, date('n'), date('j'), date('Y'));
879 }
880 if (!($checkts <= $pcheckin && $checkts < $pcheckout && $pcheckin < $pcheckout)) {
881 showSelectVb(JText::translate('VBINVALIDDATES'));
882 return;
883 }
884
885 $roomsavailable = true;
886 foreach ($rooms as $num => $r) {
887 // determine the number of nights of stay and dates to consider
888 $use_los = (int)$daysdiff;
889 $room_checkin = $pcheckin;
890 $room_checkout = $pcheckout;
891 if (!empty($split_stay) && !empty($split_stay[($num - 1)]) && $split_stay[($num - 1)]['idroom'] == $r['id']) {
892 $use_los = (int)$split_stay[($num - 1)]['nights'];
893 $room_checkin = $split_stay[($num - 1)]['checkin_ts'];
894 $room_checkout = $split_stay[($num - 1)]['checkout_ts'];
895 }
896
897 if (!VikBooking::roomNotLocked($r['id'], $r['units'], $room_checkin, $room_checkout, true, $skip_busy_ids)) {
898 $roomsavailable = false;
899 break;
900 }
901 }
902 if ($roomsavailable !== true) {
903 showSelectVb(JText::translate('VBROOMBOOKEDBYOTHER'));
904 return;
905 }
906
907 // save in session the checkin and checkout time of the reservation made
908 $session->set('vikbooking_order_checkin', $pcheckin);
909 $session->set('vikbooking_order_checkout', $pcheckout);
910
911 // handle booking sid and customer information summary string
912 $sid = $mod_booking ? $mod_booking['sid'] : VikBooking::getSecretLink();
913 $custdata = VikBooking::buildCustData($arrcustdata, "\r\n");
914
915 if (VBOPlatformDetection::isWordPress()) {
916 $viklink = JURI::root() . "index.php?option=com_vikbooking&view=booking&sid=" . $sid . "&ts=" . $nowts . (!empty($pnodep) ? "&nodep=".$pnodep : "") . (!empty($pitemid) ? "&Itemid=" . $pitemid : "");
917 } else {
918 $bestitemid = VikBooking::findProperItemIdType(array('booking'));
919 $viklink = VikBooking::externalroute("index.php?option=com_vikbooking&view=booking&sid=" . $sid . "&ts=" . $nowts . (!empty($pnodep) ? "&nodep=".$pnodep : ""), false, (!empty($bestitemid) ? $bestitemid : null));
920 }
921
922 $admail = VikBooking::getAdminMail();
923 $ftitle = VikBooking::getFrontTitle();
924 $pricestr = [];
925 if ($is_package === true) {
926 foreach ($rooms as $num => $r) {
927 $pkg_cost = $pkg['pernight_total'] == 1 ? ($pkg['cost'] * $daysdiff) : $pkg['cost'];
928 $pkg_cost = $pkg['perperson'] == 1 ? ($pkg_cost * ($arrpeople[$num]['adults'] > 0 ? $arrpeople[$num]['adults'] : 1)) : $pkg_cost;
929 $cost_plus_tax = VikBooking::sayPackagePlusIva($pkg_cost, $pkg['idiva']);
930 $pricestr[$num] = $pkg['name'].": ".$cost_plus_tax." ".$currencyname;
931 }
932 } else {
933 foreach ($tars as $num => $tar) {
934 $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'] : "");
935 }
936 }
937
938 $currentUser = JFactory::getUser();
939 $langtag = $vbo_tn->current_lang;
940 $vcmchanneldata = $session->get('vcmChannelData', '');
941 $vcmchanneldata = !empty($vcmchanneldata) && is_array($vcmchanneldata) && count($vcmchanneldata) > 0 ? $vcmchanneldata : '';
942
943 // attempt to save customer
944 $cpin->setCustomerExtraInfo($fieldflags);
945 $cpin->saveCustomerDetails($t_first_name, $t_last_name, $useremail, $phone_number, $usercountry, $arrcfields);
946
947 // collect all room IDs involved
948 $rooms_involved = [];
949 foreach ($rooms as $room_booked) {
950 if (!in_array($room_booked['id'], $rooms_involved)) {
951 $rooms_involved[] = $room_booked['id'];
952 }
953 }
954
955 $must_payment = $mod_booking ? false : VikBooking::areTherePayments($rooms_involved);
956 $payment = [];
957 if ($must_payment) {
958 $payment = VikBooking::getPayment($pgpayid);
959 }
960 if ($must_payment && empty($payment)) {
961 // error, payment was not selected
962 VikError::raiseWarning('', JText::translate('ERRSELECTPAYMENT'));
963
964 // build redirect URI values
965 $redirect_uri_vals = [
966 'option' => 'com_vikbooking',
967 'task' => 'oconfirm',
968 ];
969
970 foreach ($prices as $num => $pid) {
971 $redirect_uri_vals['priceid' . $num] = $pid;
972 }
973
974 for ($ir = 1; $ir <= $proomsnum; $ir++) {
975 if (isset($selopt[$ir]) && is_array($selopt[$ir])) {
976 foreach ($selopt[$ir] as $opt) {
977 if (array_key_exists('chageintv', $opt)) {
978 if (!isset($redirect_uri_vals['optid' . $ir . $opt['id']])) {
979 $redirect_uri_vals['optid' . $ir . $opt['id']] = [];
980 }
981 $redirect_uri_vals['optid' . $ir . $opt['id']][] = $opt['chageintv'];
982 } else {
983 $redirect_uri_vals['optid' . $ir . $opt['id']] = $opt['quan'];
984 }
985 }
986 }
987 }
988
989 $redirect_uri_vals['roomid'] = [];
990 foreach ($rooms as $num => $r) {
991 $redirect_uri_vals['roomid'][] = $r['id'];
992 }
993
994 $redirect_uri_vals['adults'] = [];
995 $redirect_uri_vals['children'] = [];
996 foreach ($arrpeople as $indroom => $aduch) {
997 $redirect_uri_vals['adults'][] = $aduch['adults'];
998 $redirect_uri_vals['children'][] = $aduch['children'];
999 }
1000
1001 $redirect_uri_vals['roomsnum'] = $proomsnum;
1002 $redirect_uri_vals['days'] = $pdays;
1003 $redirect_uri_vals['checkin'] = $pcheckin;
1004 $redirect_uri_vals['checkout'] = $pcheckout;
1005 if (!empty($split_stay)) {
1006 $redirect_uri_vals['split_stay'] = $split_stay;
1007 }
1008 $redirect_uri_vals['Itemid'] = !empty($pitemid) ? $pitemid : null;
1009
1010 $app->redirect(JRoute::rewrite('index.php?' . http_build_query($redirect_uri_vals), false));
1011 exit;
1012 }
1013
1014 // turnover seconds
1015 $turnover_secs = VikBooking::getHoursRoomAvail() * 3600;
1016 $realback = $turnover_secs + $pcheckout;
1017
1018 // push data to tracker for conversion
1019 $vbo_tracker = VikBooking::getTracker();
1020 $vbo_tracker->pushDates($pcheckin, $pcheckout, $pdays)->pushParty($arrpeople)->pushData('idcustomer', $cpin->getNewCustomerId());
1021
1022 /**
1023 * Custom check-in/out times due to late check-out/early check-in options.
1024 *
1025 * @since 1.17.2 (J) - 1.7.2 (WP)
1026 */
1027 if ($custom_checkinout) {
1028 // overwrite check-in and/or check-out timestamp(s)
1029 if ($custom_checkinout[0] >= 3600) {
1030 // overwrite check-in timestamp
1031 $time_hours = floor($custom_checkinout[0] / 3600);
1032 $time_minutes = floor(($custom_checkinout[0] - ($time_hours * 3600)) / 60);
1033 $pcheckin = mktime($time_hours, $time_minutes, 0, $checkin_info['mon'], $checkin_info['mday'], $checkin_info['year']);
1034 }
1035 if ($custom_checkinout[1] >= 3600) {
1036 // overwrite check-out timestamp
1037 $time_hours = floor($custom_checkinout[1] / 3600);
1038 $time_minutes = floor(($custom_checkinout[1] - ($time_hours * 3600)) / 60);
1039 $pcheckout = mktime($time_hours, $time_minutes, 0, $checkout_info['mon'], $checkout_info['mday'], $checkout_info['year']);
1040 // overwrite "realback" timestamp as well
1041 $realback = $turnover_secs + $pcheckout;
1042 }
1043 }
1044
1045 if (!$mod_booking && ((!empty($payment) && intval($payment['setconfirmed']) == 1) || !$must_payment || ($usedcoupon && $isdue <= 0))) {
1046 // 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
1047 $arrbusy = [];
1048 foreach ($rooms as $num => $r) {
1049 // determine the number of nights of stay and dates to consider
1050 $room_checkin = $pcheckin;
1051 $room_checkout = $pcheckout;
1052 $room_realback = $realback;
1053 if (!empty($split_stay) && !empty($split_stay[($num - 1)]) && $split_stay[($num - 1)]['idroom'] == $r['id']) {
1054 $room_checkin = $split_stay[($num - 1)]['checkin_ts'];
1055 $room_checkout = $split_stay[($num - 1)]['checkout_ts'];
1056 $room_realback = $turnover_secs + $room_checkout;
1057 }
1058
1059 $busy_record = new stdClass;
1060 $busy_record->idroom = $r['id'];
1061 $busy_record->checkin = $room_checkin;
1062 $busy_record->checkout = $room_checkout;
1063 $busy_record->realback = $room_realback;
1064
1065 $dbo->insertObject('#__vikbooking_busy', $busy_record, 'id');
1066
1067 if (!isset($busy_record->id)) {
1068 showSelectVb('Critical error while occupying the rooms. Please try again');
1069 return;
1070 }
1071
1072 $arrbusy[$num] = $busy_record->id;
1073 }
1074
1075 // store booking
1076 $booking_record = new stdClass;
1077 $booking_record->custdata = $custdata;
1078 $booking_record->ts = $nowts;
1079 $booking_record->status = 'confirmed';
1080 $booking_record->days = $pdays;
1081 $booking_record->checkin = $pcheckin;
1082 $booking_record->checkout = $pcheckout;
1083 $booking_record->custmail = $useremail;
1084 $booking_record->sid = $sid;
1085 $booking_record->idpayment = !empty($payment) ? ($payment['id'] . '=' . $payment['name']) : null;
1086 $booking_record->ujid = $currentUser->id;
1087 $booking_record->coupon = $usedcoupon === true ? $strcouponeff : null;
1088 $booking_record->roomsnum = count($rooms);
1089 $booking_record->total = (float)$isdue;
1090 $booking_record->channel = is_array($vcmchanneldata) && !empty($vcmchanneldata['name']) ? $vcmchanneldata['name'] : null;
1091 $booking_record->lang = $langtag;
1092 $booking_record->country = !empty($usercountry) ? $usercountry : null;
1093 $booking_record->tot_taxes = (float)$tot_taxes;
1094 $booking_record->tot_city_taxes = (float)$tot_city_taxes;
1095 $booking_record->tot_fees = (float)$tot_fees;
1096 if ($tot_damage_dep) {
1097 $booking_record->tot_damage_dep = (float) $tot_damage_dep;
1098 }
1099 $booking_record->phone = $phone_number;
1100 $booking_record->pkg = $is_package === true ? (int)$pkg['id'] : null;
1101 $booking_record->split_stay = !empty($split_stay) ? 1 : 0;
1102
1103 /**
1104 * Trigger event to allow third party plugins to overwrite any booking property before it gets created.
1105 *
1106 * @since 1.18.3 (J) - 1.8.3 (WP)
1107 */
1108 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeCreateBookingRecord', [$booking_record, $rooms, $tars, $selopt, $arrpeople]);
1109
1110 $dbo->insertObject('#__vikbooking_orders', $booking_record, 'id');
1111
1112 if (!isset($booking_record->id)) {
1113 showSelectVb('Critical error while saving the booking. Please try again');
1114 return;
1115 }
1116 $neworderid = $booking_record->id;
1117
1118 // ConfirmationNumber
1119 $confirmnumber = VikBooking::generateConfirmNumber($neworderid, true);
1120
1121 // assign room specific unit
1122 $set_room_indexes = (VikBooking::autoRoomUnit() || (count($proomindex) == count($rooms)));
1123 $room_indexes_usemap = [];
1124 $room_indexes_forcemap = [];
1125
1126 foreach ($rooms as $num => $r) {
1127 $q = "INSERT INTO `#__vikbooking_ordersbusy` (`idorder`,`idbusy`) VALUES(" . (int)$neworderid . ", " . (int)$arrbusy[$num] . ");";
1128 $dbo->setQuery($q);
1129 $dbo->execute();
1130 $json_ch_age = '';
1131 if (array_key_exists($num, $children_age)) {
1132 $json_ch_age = json_encode($children_age[$num]);
1133 }
1134 // assign room specific unit
1135 $room_indexes = $set_room_indexes === true ? VikBooking::getRoomUnitNumsAvailable(array('id' => $neworderid, 'checkin' => $pcheckin, 'checkout' => $pcheckout), $r['id']) : array();
1136 $use_ind_key = 0;
1137 $force_rindex = 0;
1138 if ($room_indexes && isset($room_indexes_forcemap[$r['id']])) {
1139 // an index for this same room was forced already, reset the values
1140 foreach ($room_indexes as $av_key => $av_index) {
1141 if (in_array((int)$av_index, $room_indexes_forcemap[$r['id']])) {
1142 unset($room_indexes[$av_key]);
1143 }
1144 }
1145 $room_indexes = array_values($room_indexes);
1146 }
1147 if ($room_indexes) {
1148 if (count($proomindex) == count($rooms) && !empty($proomindex[($num - 1)])) {
1149 // exact distinctive feature index selected
1150 foreach ($room_indexes as $av_index) {
1151 if ((int)$av_index == (int)$proomindex[($num - 1)]) {
1152 // requested index is available
1153 $force_rindex = (int)$proomindex[($num - 1)];
1154 if (isset($room_indexes_forcemap[$r['id']]) && in_array($force_rindex, $room_indexes_forcemap[$r['id']])) {
1155 // cannot book the same unit twice
1156 $force_rindex = 0;
1157 continue;
1158 }
1159 break;
1160 }
1161 }
1162 if ($force_rindex) {
1163 // store the forced index for any possible equal room booked later in the same loop
1164 if (!isset($room_indexes_forcemap[$r['id']])) {
1165 $room_indexes_forcemap[$r['id']] = [];
1166 }
1167 array_push($room_indexes_forcemap[$r['id']], $force_rindex);
1168 }
1169 }
1170 if (!array_key_exists($r['id'], $room_indexes_usemap)) {
1171 $room_indexes_usemap[$r['id']] = $use_ind_key;
1172 } else {
1173 $use_ind_key = $room_indexes_usemap[$r['id']];
1174 }
1175 if (isset($room_indexes[$use_ind_key])) {
1176 $rooms[$num]['roomindex'] = (int)$room_indexes[$use_ind_key];
1177 }
1178 }
1179 //
1180 $pkg_cost = 0;
1181 if ($is_package === true) {
1182 $pkg_cost = $pkg['pernight_total'] == 1 ? ($pkg['cost'] * $daysdiff) : $pkg['cost'];
1183 $pkg_cost = $pkg['perperson'] == 1 ? ($pkg_cost * ($arrpeople[$num]['adults'] > 0 ? $arrpeople[$num]['adults'] : 1)) : $pkg_cost;
1184 // $pkg_cost = VikBooking::sayPackagePlusIva($pkg_cost, $pkg['idiva']);
1185 }
1186
1187 $oroom_record = new stdClass;
1188 $oroom_record->idorder = (int)$neworderid;
1189 $oroom_record->idroom = (int)$r['id'];
1190 $oroom_record->adults = (int)$arrpeople[$num]['adults'];
1191 $oroom_record->children = (int)$arrpeople[$num]['children'];
1192 $oroom_record->pets = isset($arrpeople[$num]['pets']) ? (int)$arrpeople[$num]['pets'] : 0;
1193 $oroom_record->idtar = (int)$tars[$num][0]['id'];
1194 $oroom_record->optionals = isset($selopt['room'.$num]) ? $selopt['room'.$num] : null;
1195 $oroom_record->childrenage = (!empty($json_ch_age) ? $json_ch_age : null);
1196 $oroom_record->t_first_name = $t_first_name;
1197 $oroom_record->t_last_name = $t_last_name;
1198 $oroom_record->roomindex = null;
1199 if ($force_rindex) {
1200 $oroom_record->roomindex = $force_rindex;
1201 } elseif ($room_indexes && isset($room_indexes[$use_ind_key])) {
1202 $oroom_record->roomindex = (int)$room_indexes[$use_ind_key];
1203 }
1204 $oroom_record->pkg_id = ($is_package === true ? (int)$pkg['id'] : null);
1205 $oroom_record->pkg_name = ($is_package === true ? $pkg['name'] : null);
1206 $oroom_record->cust_cost = ($is_package === true ? $pkg_cost : null);
1207 $oroom_record->cust_idiva = ($is_package === true ? (int)$pkg['idiva'] : null);
1208 $oroom_record->room_cost = (array_key_exists($num, $rooms_costs_map) ? $rooms_costs_map[$num] : null);
1209
1210 $dbo->insertObject('#__vikbooking_ordersrooms', $oroom_record, 'id');
1211
1212 if ($room_indexes) {
1213 $room_indexes_usemap[$r['id']]++;
1214 }
1215 }
1216
1217 if (!empty($split_stay)) {
1218 // save transient on db for split stay information
1219 VBOFactory::getConfig()->set('split_stay_' . $neworderid, json_encode($split_stay));
1220 }
1221
1222 // customer booking
1223 $cpin->saveCustomerBooking($neworderid);
1224
1225 if ($usedcoupon === true && $coupon['type'] == 2) {
1226 $q = "DELETE FROM `#__vikbooking_coupons` WHERE `id`='".$coupon['id']."';";
1227 $dbo->setQuery($q);
1228 $dbo->execute();
1229 }
1230
1231 // check if some of the rooms booked have shared calendars
1232 VikBooking::updateSharedCalendars($neworderid, array(), $pcheckin, $pcheckout);
1233
1234 // send email notification to guest and admin
1235 VikBooking::sendBookingEmail($neworderid, ['guest', 'admin']);
1236
1237 //SMS
1238 VikBooking::sendBookingSMS($neworderid);
1239
1240 //Booking History
1241 VikBooking::getBookingHistoryInstance()->setBid($neworderid)->store('NC', 'IP: '.VikRequest::getVar('REMOTE_ADDR', '', 'server'));
1242
1243 //invoke VikChannelManager
1244 if (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "synch.vikbooking.php")) {
1245 require_once(VCM_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "synch.vikbooking.php");
1246 $vcm = new SynchVikBooking($neworderid);
1247 $vcm->setPushType('new')->sendRequest();
1248 }
1249
1250 // VBO 1.11 - push data to tracker for conversion
1251 $vbo_tracker->pushData('idorder', $neworderid)->closeTrack();
1252 $vbo_tracker->resetTrack();
1253
1254 $app->redirect(JRoute::rewrite("index.php?option=com_vikbooking&view=booking&sid=" . $sid . "&ts=" . $nowts . (!empty($pnodep) ? "&nodep=".$pnodep : "") . (!empty($pitemid) ? "&Itemid=" . $pitemid : ""), false));
1255 } elseif ($mod_booking) {
1256 // booking modification statement
1257 // get current orders-busy relations
1258 $old_busy_ids = [];
1259 $q = "SELECT `idbusy` FROM `#__vikbooking_ordersbusy` WHERE `idorder`=".(int)$mod_booking['id'].";";
1260 $dbo->setQuery($q);
1261 $getbusy = $dbo->loadAssocList();
1262 if ($getbusy) {
1263 foreach ($getbusy as $gbu) {
1264 array_push($old_busy_ids, $gbu['idbusy']);
1265 }
1266 }
1267 //remove current busy records
1268 if (count($old_busy_ids)) {
1269 $q = "DELETE FROM `#__vikbooking_busy` WHERE `id` IN (".implode(', ', $old_busy_ids).");";
1270 $dbo->setQuery($q);
1271 $dbo->execute();
1272 }
1273 $q = "DELETE FROM `#__vikbooking_ordersbusy` WHERE `idorder`=".(int)$mod_booking['id'].";";
1274 $dbo->setQuery($q);
1275 $dbo->execute();
1276 //get current rooms (for VCM and for composing the log)
1277 $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;";
1278 $dbo->setQuery($q);
1279 $dbo->execute();
1280 $old_ordersrooms = $dbo->loadAssocList();
1281 $mod_booking['rooms_info'] = $old_ordersrooms;
1282 //remove current rooms
1283 $q = "DELETE FROM `#__vikbooking_ordersrooms` WHERE `idorder`=".(int)$mod_booking['id'].";";
1284 $dbo->setQuery($q);
1285 $dbo->execute();
1286 //update the booking by creating first the new busy records
1287 $arrbusy = [];
1288 foreach ($rooms as $num => $r) {
1289 $q = "INSERT INTO `#__vikbooking_busy` (`idroom`,`checkin`,`checkout`,`realback`) VALUES(".(int)$r['id'].", ".$dbo->quote($pcheckin).", ".$dbo->quote($pcheckout).", ".$dbo->quote($realback).");";
1290 $dbo->setQuery($q);
1291 $dbo->execute();
1292 $lid = $dbo->insertid();
1293 $arrbusy[$num] = $lid;
1294 }
1295 // assign room specific unit
1296 $set_room_indexes = (VikBooking::autoRoomUnit() || (count($proomindex) == count($rooms)));
1297 $room_indexes_usemap = [];
1298 $room_indexes_forcemap = [];
1299 //create the new rooms and orders-busy relations
1300 foreach ($rooms as $num => $r) {
1301 $q = "INSERT INTO `#__vikbooking_ordersbusy` (`idorder`,`idbusy`) VALUES(".(int)$mod_booking['id'].", ".(int)$arrbusy[$num].");";
1302 $dbo->setQuery($q);
1303 $dbo->execute();
1304 $json_ch_age = '';
1305 if (array_key_exists($num, $children_age)) {
1306 $json_ch_age = json_encode($children_age[$num]);
1307 }
1308 // assign room specific unit
1309 $room_indexes = $set_room_indexes === true ? VikBooking::getRoomUnitNumsAvailable(array('id' => $mod_booking['id'], 'checkin' => $pcheckin, 'checkout' => $pcheckout), $r['id']) : array();
1310 $use_ind_key = 0;
1311 $force_rindex = 0;
1312 if ($room_indexes && isset($room_indexes_forcemap[$r['id']])) {
1313 // an index for this same room was forced already, reset the values
1314 foreach ($room_indexes as $av_key => $av_index) {
1315 if (in_array((int)$av_index, $room_indexes_forcemap[$r['id']])) {
1316 unset($room_indexes[$av_key]);
1317 }
1318 }
1319 $room_indexes = array_values($room_indexes);
1320 }
1321 if ($room_indexes) {
1322 if (count($proomindex) == count($rooms) && !empty($proomindex[($num - 1)])) {
1323 // exact distinctive feature index selected
1324 foreach ($room_indexes as $av_index) {
1325 if ((int)$av_index == (int)$proomindex[($num - 1)]) {
1326 // requested index is available
1327 $force_rindex = (int)$proomindex[($num - 1)];
1328 if (isset($room_indexes_forcemap[$r['id']]) && in_array($force_rindex, $room_indexes_forcemap[$r['id']])) {
1329 // cannot book the same unit twice
1330 $force_rindex = 0;
1331 continue;
1332 }
1333 break;
1334 }
1335 }
1336 if ($force_rindex) {
1337 // store the forced index for any possible equal room booked later in the same loop
1338 if (!isset($room_indexes_forcemap[$r['id']])) {
1339 $room_indexes_forcemap[$r['id']] = [];
1340 }
1341 array_push($room_indexes_forcemap[$r['id']], $force_rindex);
1342 }
1343 }
1344 if (!array_key_exists($r['id'], $room_indexes_usemap)) {
1345 $room_indexes_usemap[$r['id']] = $use_ind_key;
1346 } else {
1347 $use_ind_key = $room_indexes_usemap[$r['id']];
1348 }
1349 if (isset($room_indexes[$use_ind_key])) {
1350 $rooms[$num]['roomindex'] = (int)$room_indexes[$use_ind_key];
1351 }
1352 }
1353 //
1354 $pkg_cost = 0;
1355 if ($is_package === true) {
1356 $pkg_cost = $pkg['pernight_total'] == 1 ? ($pkg['cost'] * $daysdiff) : $pkg['cost'];
1357 $pkg_cost = $pkg['perperson'] == 1 ? ($pkg_cost * ($arrpeople[$num]['adults'] > 0 ? $arrpeople[$num]['adults'] : 1)) : $pkg_cost;
1358 // $pkg_cost = VikBooking::sayPackagePlusIva($pkg_cost, $pkg['idiva']);
1359 }
1360
1361 $oroom_record = new stdClass;
1362 $oroom_record->idorder = (int)$mod_booking['id'];
1363 $oroom_record->idroom = (int)$r['id'];
1364 $oroom_record->adults = (int)$arrpeople[$num]['adults'];
1365 $oroom_record->children = (int)$arrpeople[$num]['children'];
1366 $oroom_record->pets = isset($arrpeople[$num]['pets']) ? (int)$arrpeople[$num]['pets'] : 0;
1367 $oroom_record->idtar = (int)$tars[$num][0]['id'];
1368 $oroom_record->optionals = isset($selopt['room'.$num]) ? $selopt['room'.$num] : null;
1369 $oroom_record->childrenage = (!empty($json_ch_age) ? $json_ch_age : null);
1370 $oroom_record->t_first_name = $t_first_name;
1371 $oroom_record->t_last_name = $t_last_name;
1372 $oroom_record->roomindex = null;
1373 if ($force_rindex) {
1374 $oroom_record->roomindex = $force_rindex;
1375 } elseif ($room_indexes && isset($room_indexes[$use_ind_key])) {
1376 $oroom_record->roomindex = (int)$room_indexes[$use_ind_key];
1377 }
1378 $oroom_record->pkg_id = ($is_package === true ? (int)$pkg['id'] : null);
1379 $oroom_record->pkg_name = ($is_package === true ? $pkg['name'] : null);
1380 $oroom_record->cust_cost = ($is_package === true ? $pkg_cost : null);
1381 $oroom_record->cust_idiva = ($is_package === true ? (int)$pkg['idiva'] : null);
1382 $oroom_record->room_cost = (array_key_exists($num, $rooms_costs_map) ? $rooms_costs_map[$num] : null);
1383
1384 $dbo->insertObject('#__vikbooking_ordersrooms', $oroom_record, 'id');
1385
1386 if ($room_indexes) {
1387 $room_indexes_usemap[$r['id']]++;
1388 }
1389 }
1390
1391 // update the booking record (do not touch information like sid, confirmnumber, payment method etc..)
1392 $logmod = VikBooking::getLogBookingModification($mod_booking);
1393 $mod_notes = $logmod.(!empty($mod_booking['adminnotes']) ? "\n\n".$mod_booking['adminnotes'] : '');
1394 // if old total lower than new total, increment paymcount to allow a new payment (if configuration setting enabled)
1395 $mod_paymcount = (int)$mod_booking['paymcount'];
1396 if ($mod_booking['total'] < $isdue) {
1397 $mod_paymcount++;
1398 }
1399
1400 $q = $dbo->getQuery(true)
1401 ->update($dbo->qn('#__vikbooking_orders'))
1402 ->set($dbo->qn('custdata') . ' = ' . $dbo->q($custdata))
1403 ->set($dbo->qn('ts') . ' = ' . $nowts)
1404 ->set($dbo->qn('days') . ' = ' . $pdays)
1405 ->set($dbo->qn('checkin') . ' = ' . $dbo->q($pcheckin))
1406 ->set($dbo->qn('checkout') . ' = ' . $dbo->q($pcheckout))
1407 ->set($dbo->qn('custmail') . ' = ' . $dbo->q($useremail))
1408 ->set($dbo->qn('ujid') . ' = ' . (int) $currentUser->id)
1409 ->set($dbo->qn('coupon') . ' = ' . ($usedcoupon === true ? $dbo->q($strcouponeff) : 'NULL'))
1410 ->set($dbo->qn('roomsnum') . ' = ' . count($rooms))
1411 ->set($dbo->qn('total') . ' = ' . $isdue)
1412 ->set($dbo->qn('channel') . ' = ' . (is_array($vcmchanneldata) ? $dbo->q($vcmchanneldata['name']) : (!empty($mod_booking['channel']) ? $dbo->q($mod_booking['channel']) : 'NULL')))
1413 ->set($dbo->qn('paymcount') . ' = ' . $mod_paymcount)
1414 ->set($dbo->qn('adminnotes') . ' = ' . $dbo->q($mod_notes))
1415 ->set($dbo->qn('lang') . ' = ' . $dbo->q($langtag))
1416 ->set($dbo->qn('country') . ' = ' . (!empty($usercountry) ? $dbo->q($usercountry) : 'NULL'))
1417 ->set($dbo->qn('tot_taxes') . ' = ' . $tot_taxes)
1418 ->set($dbo->qn('tot_city_taxes') . ' = ' . $tot_city_taxes)
1419 ->set($dbo->qn('tot_fees') . ' = ' . $tot_fees)
1420 ->set($dbo->qn('tot_damage_dep') . ' = ' . $tot_damage_dep)
1421 ->set($dbo->qn('phone') . ' = ' . $dbo->q($phone_number))
1422 ->set($dbo->qn('pkg') . ' = ' . ($is_package === true ? (int) $pkg['id'] : 'NULL'))
1423 ->where($dbo->qn('id') . ' = ' . (int) $mod_booking['id']);
1424
1425 $dbo->setQuery($q);
1426 $dbo->execute();
1427
1428 /**
1429 * Trigger event to allow third party plugins to run after the booking is modified.
1430 *
1431 * @since 1.18.3 (J) - 1.8.3 (WP)
1432 */
1433 VBOFactory::getPlatform()->getDispatcher()->trigger('onAfterModifyBookingRecord', [$mod_booking, $rooms, $tars, $selopt, $arrpeople]);
1434
1435 // remove the coupon used (should never been allowed for modifications)
1436 if ($usedcoupon == true && $coupon['type'] == 2) {
1437 $q = "DELETE FROM `#__vikbooking_coupons` WHERE `id`=".(int)$coupon['id'].";";
1438 $dbo->setQuery($q);
1439 $dbo->execute();
1440 }
1441
1442 // unset any previously booked room due to calendar sharing (should not be necessary because busy records have already been purged)
1443 VikBooking::cleanSharedCalendarsBusy($mod_booking['id']);
1444 // check if some of the rooms booked have shared calendars
1445 VikBooking::updateSharedCalendars($mod_booking['id'], array(), $pcheckin, $pcheckout);
1446
1447 //send email messages (admin and customer) and invoke SMS send
1448 VikBooking::sendBookingEmail($mod_booking['id'], array('guest', 'admin'), true, false, $type = 'modified');
1449
1450 //SMS
1451 VikBooking::sendBookingSMS($mod_booking['id']);
1452
1453 //Booking History
1454 VikBooking::getBookingHistoryInstance()->setBid($mod_booking['id'])->store('MW', $logmod);
1455
1456 //invoke VikChannelManager
1457 if (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "synch.vikbooking.php")) {
1458 $vcm_obj = VikBooking::getVcmInvoker();
1459 $vcm_obj->setOids(array($mod_booking['id']))->setSyncType('modify')->setOriginalBooking($mod_booking);
1460 $vcm_obj->doSync();
1461 }
1462
1463 //unset the session value
1464 $session->set('vboModBooking', '');
1465
1466 // VBO 1.11 - push data to tracker for conversion
1467 $vbo_tracker->pushData('idorder', $mod_booking['id'])->pushMessage(JText::translate('VBOBOOKINGMODOK'))->closeTrack();
1468 $vbo_tracker->resetTrack();
1469
1470 $app->enqueueMessage(JText::translate('VBOBOOKINGMODOK'));
1471 $app->redirect(JRoute::rewrite("index.php?option=com_vikbooking&view=booking&sid=" . $sid . "&ts=" . $nowts . (!empty($pnodep) ? "&nodep=".$pnodep : "") . (!empty($pitemid) ? "&Itemid=" . $pitemid : ""), false));
1472 } else {
1473 // booking must have status stand-by and proceed to the payment
1474 $booking_record = new stdClass;
1475 $booking_record->custdata = $custdata;
1476 $booking_record->ts = $nowts;
1477 $booking_record->status = 'standby';
1478 $booking_record->days = $pdays;
1479 $booking_record->checkin = $pcheckin;
1480 $booking_record->checkout = $pcheckout;
1481 $booking_record->custmail = $useremail;
1482 $booking_record->sid = $sid;
1483 $booking_record->idpayment = !empty($payment) ? ($payment['id'] . '=' . $payment['name']) : null;
1484 $booking_record->ujid = $currentUser->id;
1485 $booking_record->coupon = $usedcoupon === true ? $strcouponeff : null;
1486 $booking_record->roomsnum = count($rooms);
1487 $booking_record->total = (float)$isdue;
1488 $booking_record->channel = is_array($vcmchanneldata) && !empty($vcmchanneldata['name']) ? $vcmchanneldata['name'] : null;
1489 $booking_record->lang = $langtag;
1490 $booking_record->country = !empty($usercountry) ? $usercountry : null;
1491 $booking_record->tot_taxes = (float)$tot_taxes;
1492 $booking_record->tot_city_taxes = (float)$tot_city_taxes;
1493 $booking_record->tot_fees = (float)$tot_fees;
1494 if ($tot_damage_dep) {
1495 $booking_record->tot_damage_dep = (float) $tot_damage_dep;
1496 }
1497 $booking_record->phone = $phone_number;
1498 $booking_record->pkg = $is_package === true ? (int)$pkg['id'] : null;
1499 $booking_record->split_stay = !empty($split_stay) ? 1 : 0;
1500
1501 /**
1502 * Trigger event to allow third party plugins to overwrite any booking property before it gets created.
1503 *
1504 * @since 1.18.3 (J) - 1.8.3 (WP)
1505 */
1506 VBOFactory::getPlatform()->getDispatcher()->trigger('onBeforeCreateBookingRecord', [$booking_record, $rooms, $tars, $selopt, $arrpeople]);
1507
1508 $dbo->insertObject('#__vikbooking_orders', $booking_record, 'id');
1509
1510 if (!isset($booking_record->id)) {
1511 showSelectVb('Critical error while saving the booking. Please try again');
1512 return;
1513 }
1514 $neworderid = $booking_record->id;
1515
1516 $room_indexes_forcemap = [];
1517 foreach ($rooms as $num => $r) {
1518 $json_ch_age = '';
1519 if (array_key_exists($num, $children_age)) {
1520 $json_ch_age = json_encode($children_age[$num]);
1521 }
1522
1523 $pkg_cost = 0;
1524 if ($is_package === true) {
1525 $pkg_cost = $pkg['pernight_total'] == 1 ? ($pkg['cost'] * $daysdiff) : $pkg['cost'];
1526 $pkg_cost = $pkg['perperson'] == 1 ? ($pkg_cost * ($arrpeople[$num]['adults'] > 0 ? $arrpeople[$num]['adults'] : 1)) : $pkg_cost;
1527 // $pkg_cost = VikBooking::sayPackagePlusIva($pkg_cost, $pkg['idiva']);
1528 }
1529
1530 $oroom_record = new stdClass;
1531 $oroom_record->idorder = (int)$neworderid;
1532 $oroom_record->idroom = (int)$r['id'];
1533 $oroom_record->adults = (int)$arrpeople[$num]['adults'];
1534 $oroom_record->children = (int)$arrpeople[$num]['children'];
1535 $oroom_record->pets = isset($arrpeople[$num]['pets']) ? (int)$arrpeople[$num]['pets'] : 0;
1536 $oroom_record->idtar = (int)$tars[$num][0]['id'];
1537 $oroom_record->optionals = isset($selopt['room'.$num]) ? $selopt['room'.$num] : null;
1538 $oroom_record->childrenage = (!empty($json_ch_age) ? $json_ch_age : null);
1539 $oroom_record->t_first_name = $t_first_name;
1540 $oroom_record->t_last_name = $t_last_name;
1541 $oroom_record->roomindex = null;
1542 if (count($proomindex) == count($rooms) && !empty($proomindex[($num - 1)])) {
1543 // check if the sub-unit requested is available
1544 if (!isset($room_indexes_forcemap[$r['id']])) {
1545 $room_indexes_forcemap[$r['id']] = [];
1546 }
1547 $room_indexes = VikBooking::getRoomUnitNumsAvailable(array('id' => $neworderid, 'checkin' => $pcheckin, 'checkout' => $pcheckout), $r['id']);
1548 $force_rindex = 0;
1549 foreach ($room_indexes as $av_index) {
1550 if ((int)$av_index == (int)$proomindex[($num - 1)] && !in_array((int)$proomindex[($num - 1)], $room_indexes_forcemap[$r['id']])) {
1551 // requested index is available
1552 $force_rindex = (int)$proomindex[($num - 1)];
1553 array_push($room_indexes_forcemap[$r['id']], $force_rindex);
1554 break;
1555 }
1556 }
1557 if (!empty($force_rindex)) {
1558 $oroom_record->roomindex = $force_rindex;
1559 }
1560 }
1561 $oroom_record->pkg_id = ($is_package === true ? (int)$pkg['id'] : null);
1562 $oroom_record->pkg_name = ($is_package === true ? $pkg['name'] : null);
1563 $oroom_record->cust_cost = ($is_package === true ? $pkg_cost : null);
1564 $oroom_record->cust_idiva = ($is_package === true ? (int)$pkg['idiva'] : null);
1565 $oroom_record->room_cost = (array_key_exists($num, $rooms_costs_map) ? $rooms_costs_map[$num] : null);
1566
1567 $dbo->insertObject('#__vikbooking_ordersrooms', $oroom_record, 'id');
1568 }
1569
1570 if ($usedcoupon === true && $coupon['type'] == 2) {
1571 $q = "DELETE FROM `#__vikbooking_coupons` WHERE `id`=" . (int)$coupon['id'] . ";";
1572 $dbo->setQuery($q);
1573 $dbo->execute();
1574 }
1575
1576 // lock rooms waiting to be confirmed
1577 $lock_until_ts = VikBooking::getMinutesLock(true);
1578 foreach ($rooms as $num => $r) {
1579 // determine the number of nights of stay and dates to consider
1580 $room_checkin = $pcheckin;
1581 $room_checkout = $pcheckout;
1582 $room_realback = $realback;
1583 if (!empty($split_stay) && !empty($split_stay[($num - 1)]) && $split_stay[($num - 1)]['idroom'] == $r['id']) {
1584 $room_checkin = $split_stay[($num - 1)]['checkin_ts'];
1585 $room_checkout = $split_stay[($num - 1)]['checkout_ts'];
1586 $room_realback = $turnover_secs + $room_checkout;
1587 }
1588
1589 $tmp_lock_record = new stdClass;
1590 $tmp_lock_record->idroom = $r['id'];
1591 $tmp_lock_record->checkin = $room_checkin;
1592 $tmp_lock_record->checkout = $room_checkout;
1593 $tmp_lock_record->until = $lock_until_ts;
1594 $tmp_lock_record->realback = $room_realback;
1595 $tmp_lock_record->idorder = (int)$neworderid;
1596
1597 $dbo->insertObject('#__vikbooking_tmplock', $tmp_lock_record, 'id');
1598 }
1599
1600 if (!empty($split_stay)) {
1601 // save transient on db for split stay information
1602 VBOFactory::getConfig()->set('split_stay_' . $neworderid, json_encode($split_stay));
1603 }
1604
1605 // Customer Booking
1606 $cpin->saveCustomerBooking($neworderid);
1607
1608 // send email notification to guest and admin
1609 VikBooking::sendBookingEmail($neworderid, ['guest', 'admin']);
1610
1611 //SMS
1612 VikBooking::sendBookingSMS($neworderid);
1613
1614 //Booking History
1615 VikBooking::getBookingHistoryInstance()->setBid($neworderid)->store('NP', 'IP: ' . VikRequest::getVar('REMOTE_ADDR', '', 'server'));
1616
1617 /**
1618 * Invoke VikChannelManager also in case of pending reservations.
1619 *
1620 * @since 1.16.5 (J) - 1.6.5 (WP)
1621 *
1622 * @requires VCM >= 1.8.20
1623 */
1624 if (class_exists('VCMRequestAvailability')) {
1625 VikBooking::getVcmInvoker()
1626 ->setOids([$neworderid])
1627 ->setSyncType('new')
1628 ->doSync();
1629 }
1630
1631 // VBO 1.11 - push data to tracker for conversion
1632 $vbo_tracker->pushData('idorder', $neworderid)->closeTrack();
1633 $vbo_tracker->resetTrack();
1634
1635 // redirect URI to pending booking details
1636 $booking_details_uri = "index.php?option=com_vikbooking&view=booking&sid=" . $sid . "&ts=" . $nowts . (!empty($pnodep) ? "&nodep=".$pnodep : "") . (!empty($pitemid) ? "&Itemid=" . $pitemid : "");
1637
1638 /**
1639 * Trigger event to allow third-party plugins to manipulate the redirect URI.
1640 *
1641 * @since 1.17.2 (J) - 1.7.2 (WP)
1642 */
1643 VBOFactory::getPlatform()->getDispatcher()->trigger('onRedirectOrder', [&$booking_details_uri, $neworderid]);
1644
1645 // redirect to booking details page
1646 $app->redirect(JRoute::rewrite($booking_details_uri, false));
1647 }
1648 }
1649
1650 public function vieworder()
1651 {
1652 VikRequest::setVar('view', 'booking');
1653 parent::display();
1654 }
1655
1656 public function cancelrequest()
1657 {
1658 if (!JSession::checkToken()) {
1659 // missing CSRF-proof token
1660 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
1661 }
1662
1663 $dbo = JFactory::getDbo();
1664 $mainframe = JFactory::getApplication();
1665
1666 $psid = VikRequest::getString('sid', '', 'request');
1667 $pidorder = VikRequest::getString('idorder', '', 'request');
1668
1669 if (!empty($psid) && !empty($pidorder)) {
1670 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=".intval($pidorder)." AND `sid`=".$dbo->quote($psid).";";
1671 $dbo->setQuery($q);
1672 $order = $dbo->loadAssocList();
1673 if ($order) {
1674 $pemail = VikRequest::getString('email', '', 'request');
1675 $preason = VikRequest::getString('reason', '', 'request');
1676 if (!empty($pemail) && !empty($preason)) {
1677 $to = VikBooking::getAdminMail();
1678 if(strpos($to, ',') !== false) {
1679 $all_recipients = explode(',', $to);
1680 foreach ($all_recipients as $k => $v) {
1681 if(empty($v)) {
1682 unset($all_recipients[$k]);
1683 }
1684 }
1685 if(count($all_recipients) > 0) {
1686 $to = $all_recipients;
1687 }
1688 }
1689 //Booking History
1690 VikBooking::getBookingHistoryInstance()->setBid($order[0]['id'])->store('CR', $pemail."\n".$preason);
1691 //
1692 $subject = JText::translate('VBCANCREQUESTEMAILSUBJ') . ' #' . $order[0]['id'];
1693 // @wponly we do not need to pass the "best item ID" to externalroute()
1694 $uri = VikBooking::externalroute("index.php?option=com_vikbooking&view=booking&sid=" . $order[0]['sid'] . "&ts=" . $order[0]['ts'], false);
1695 $msg = JText::sprintf('VBCANCREQUESTEMAILHEAD', $order[0]['id'], $uri)."\n\n".$preason;
1696 $vbo_app = VikBooking::getVboApplication();
1697 $adsendermail = VikBooking::getSenderMail();
1698 $vbo_app->sendMail($adsendermail, $adsendermail, $to, $pemail, $subject, $msg, false);
1699 $mainframe->enqueueMessage(JText::translate('VBCANCREQUESTMAILSENT'));
1700 $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));
1701 } else {
1702 $mainframe->redirect(JRoute::rewrite("index.php?option=com_vikbooking&view=booking&sid=".$order[0]['sid']."&ts=".$order[0]['ts'], false));
1703 }
1704 } else {
1705 $mainframe->redirect("index.php");
1706 }
1707 } else {
1708 $mainframe->redirect("index.php");
1709 }
1710 }
1711
1712 public function reqinfo()
1713 {
1714 $proomid = VikRequest::getInt('roomid', '', 'request');
1715 $preqinfotoken = VikRequest::getInt('reqinfotoken', '', 'request');
1716 $pitemid = VikRequest::getInt('Itemid', '', 'request');
1717 $dbo = JFactory::getDBO();
1718 $session = JFactory::getSession();
1719 $mainframe = JFactory::getApplication();
1720 $vbo_app = VikBooking::getVboApplication();
1721 if (!empty($proomid)) {
1722 $q = "SELECT `id`,`name` FROM `#__vikbooking_rooms` WHERE `id`=".(int)$proomid.";";
1723 $dbo->setQuery($q);
1724 $dbo->execute();
1725 if ($dbo->getNumRows() == 1) {
1726 $room = $dbo->loadAssocList();
1727 $goto = JRoute::rewrite('index.php?option=com_vikbooking&view=roomdetails&roomid='.$room[0]['id'].'&Itemid='.$pitemid, false);
1728 $preqname = VikRequest::getString('reqname', '', 'request');
1729 $preqemail = VikRequest::getString('reqemail', '', 'request');
1730 $preqmess = VikRequest::getString('reqmess', '', 'request');
1731 if (!empty($preqemail) && !empty($preqmess)) {
1732 /**
1733 * captcha verification
1734 *
1735 * @since 1.2.3
1736 */
1737 if ($vbo_app->isCaptcha() && !$vbo_app->reCaptcha('check')) {
1738 VikError::raiseWarning('', 'Invalid Captcha');
1739 $mainframe->redirect($goto);
1740 exit;
1741 }
1742 //
1743 $sesstoken = $session->get('vboreqinfo'.$room[0]['id'], '');
1744 if((int)$sesstoken == (int)$preqinfotoken) {
1745 $session->set('vboreqinfo'.$room[0]['id'], '');
1746 $to = VikBooking::getAdminMail();
1747 if(strpos($to, ',') !== false) {
1748 $all_recipients = explode(',', $to);
1749 foreach ($all_recipients as $k => $v) {
1750 if(empty($v)) {
1751 unset($all_recipients[$k]);
1752 }
1753 }
1754 if(count($all_recipients) > 0) {
1755 $to = $all_recipients;
1756 }
1757 }
1758 $subject = JText::sprintf('VBOROOMREQINFOSUBJ', $room[0]['name']);
1759 $msg = JText::translate('VBOROOMREQINFONAME').": ".$preqname."\n\n".JText::translate('VBOROOMREQINFOEMAIL').": ".$preqemail."\n\n".JText::translate('VBOROOMREQINFOMESS').":\n\n".$preqmess;
1760 $adsendermail = VikBooking::getSenderMail();
1761 $vbo_app->sendMail($adsendermail, $adsendermail, $to, $preqemail, $subject, $msg, false);
1762 $mainframe->enqueueMessage(JText::translate('VBOROOMREQINFOSENTOK'));
1763 } else {
1764 VikError::raiseWarning('', JText::translate('VBOROOMREQINFOTKNERR'));
1765 }
1766 $mainframe->redirect($goto);
1767 } else {
1768 VikError::raiseWarning('', JText::translate('VBOROOMREQINFOMISSFIELD'));
1769 $mainframe->redirect($goto);
1770 }
1771 } else {
1772 $mainframe->redirect("index.php");
1773 }
1774 } else {
1775 $mainframe->redirect("index.php");
1776 }
1777 }
1778
1779 public function cron_exec()
1780 {
1781 if (VBOPlatformDetection::isWordPress())
1782 {
1783 // in WordPress it is no more needed to schedule a server cron job
1784 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.');
1785 }
1786
1787 $app = JFactory::getApplication();
1788
1789 $id_cron = $app->input->getUint('cron_id', 0);
1790 $key = $app->input->getString('cronkey', '');
1791
1792 $model = VBOMvcModel::getInstance('cronjob');
1793
1794 // dispatch the cron job by injecting the cron key within the
1795 // configuration array, in order to make sure that the execution
1796 // of the job has been requested by a reliable caller
1797 $response = $model->dispatch($id_cron, ['key' => $key]);
1798
1799 if ($response === false)
1800 {
1801 // an error has occurred
1802 $error = $model->getError();
1803
1804 if (!$error instanceof Exception)
1805 {
1806 // wrap error message in an exception for a better ease of use
1807 $error = new Exception($error ?: 'Error', 500);
1808 }
1809
1810 // terminate session with an error
1811 VBOHttpDocument::getInstance($app)->close($error->getCode(), $error->getMessage());
1812 }
1813
1814 // display response code and teminate the session
1815 echo $response;
1816 $app->close();
1817 }
1818
1819 public function notifypayment()
1820 {
1821 $app = JFactory::getApplication();
1822 $dbo = JFactory::getDbo();
1823
1824 $session = JFactory::getSession();
1825
1826 $config = VBOFactory::getConfig();
1827 $av_helper = VikBooking::getAvailabilityInstance();
1828
1829 $psid = VikRequest::getString('sid', '', 'request');
1830 $pts = VikRequest::getString('ts', '', 'request');
1831
1832 $nowdf = VikBooking::getDateFormat();
1833 if ($nowdf == "%d/%m/%Y") {
1834 $df = 'd/m/Y';
1835 } elseif ($nowdf == "%m/%d/%Y") {
1836 $df = 'm/d/Y';
1837 } else {
1838 $df = 'Y/m/d';
1839 }
1840
1841 if (!strlen($psid) || !strlen($pts)) {
1842 VBOHttpDocument::getInstance()->close(500, 'Missing information for fetching the booking');
1843 }
1844
1845 $admail = VikBooking::getAdminMail();
1846 $recipient_mail = $admail;
1847 if (!is_array($recipient_mail) && strpos($recipient_mail, ',') !== false) {
1848 $all_recipients = explode(',', $recipient_mail);
1849 foreach ($all_recipients as $k => $v) {
1850 if (empty($v)) {
1851 unset($all_recipients[$k]);
1852 }
1853 }
1854 if (count($all_recipients) > 0) {
1855 $recipient_mail = $all_recipients;
1856 }
1857 }
1858
1859 // load booking details
1860 $q = "SELECT * FROM `#__vikbooking_orders` WHERE (`sid`=" . $dbo->quote($psid) . " OR `idorderota`=" . $dbo->quote($psid) . ") AND `ts`=" . $dbo->quote($pts);
1861 $dbo->setQuery($q, 0, 1);
1862 $row = $dbo->loadAssoc();
1863 if (!$row) {
1864 VBOHttpDocument::getInstance()->close(404, 'Booking not found');
1865 }
1866
1867 // check if the language in use is the same as the one used during the checkout
1868 if (!empty($row['lang'])) {
1869 $lang = JFactory::getLanguage();
1870 if ($lang->getTag() != $row['lang']) {
1871 $lang->load('com_vikbooking', (VBOPlatformDetection::isWordPress() ? VIKBOOKING_SITE_LANG : JPATH_SITE), $row['lang'], true);
1872 if (VBOPlatformDetection::isJoomla()) {
1873 $lang->load('joomla', JPATH_SITE, $row['lang'], true);
1874 }
1875 }
1876 }
1877
1878 // translator
1879 $vbo_tn = VikBooking::getTranslator();
1880
1881 if ($row['status'] == 'confirmed' && !(VikBooking::multiplePayments() && $row['paymcount'] > 0)) {
1882 // booking can be paid only if not confirmed or if multiple payments are enabled and payment counter for booking greater than zero
1883 VBOHttpDocument::getInstance()->close(409, 'Conflicting and unexpected payment validation for this reservation');
1884 }
1885
1886 /**
1887 * Check split stay reservation data.
1888 *
1889 * @since 1.16.0 (J) - 1.6.0 (WP)
1890 */
1891 $split_stay = [];
1892 if ($row['split_stay'] && $row['status'] != 'confirmed') {
1893 // check for transient on DB
1894 $split_stay = $config->getArray('split_stay_' . $row['id'], []);
1895 }
1896
1897 // inject admin email
1898 $row['admin_email'] = $admail;
1899
1900 // turnover seconds
1901 $turnover_secs = VikBooking::getHoursRoomAvail() * 3600;
1902 $realback = $turnover_secs + $row['checkout'];
1903
1904 $currencyname = VikBooking::getCurrencyName();
1905 $ftitle = VikBooking::getFrontTitle();
1906 $nowts = time();
1907
1908 $rooms = [];
1909 $tars = [];
1910 $arrpeople = [];
1911 $is_package = (bool)(!empty($row['pkg']));
1912
1913 // load booked rooms
1914 $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;";
1915 $dbo->setQuery($q);
1916 $orderrooms = $dbo->loadAssocList();
1917 if ($orderrooms) {
1918 $vbo_tn->translateContents($orderrooms, '#__vikbooking_rooms', array('id' => 'r_reference_id'));
1919 foreach ($orderrooms as $kor => $or) {
1920 $num = $kor + 1;
1921 $rooms[$num] = $or;
1922 $arrpeople[$num]['adults'] = $or['adults'];
1923 $arrpeople[$num]['children'] = $or['children'];
1924 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
1925 // package or custom cost set from the back-end
1926 continue;
1927 }
1928
1929 // determine the number of nights of stay and dates to consider
1930 $use_los = $row['days'];
1931 $room_checkin = $row['checkin'];
1932 $room_checkout = $row['checkout'];
1933 if (!empty($split_stay) && !empty($split_stay[$kor]) && $split_stay[$kor]['idroom'] == $or['idroom']) {
1934 $use_los = (int)$split_stay[$kor]['nights'];
1935 $room_checkin = $split_stay[$kor]['checkin_ts'];
1936 $room_checkout = $split_stay[$kor]['checkout_ts'];
1937 }
1938
1939 $q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `id`=" . (int)$or['idtar'];
1940 $dbo->setQuery($q, 0, 1);
1941 $tar = $dbo->loadAssocList();
1942 if (!$tar) {
1943 continue;
1944 }
1945
1946 $tar = VikBooking::applySeasonsRoom($tar, $room_checkin, $room_checkout);
1947
1948 // apply OBP rules
1949 $tar = VBORoomHelper::getInstance()->applyOBPRules($tar, $or, $or['adults']);
1950
1951 // push tariff
1952 $tars[$num] = $tar[0];
1953 }
1954 }
1955
1956 // inject values
1957 $row['order_rooms'] = $orderrooms;
1958 $row['fares'] = $tars;
1959
1960 // invoke the payment method class
1961 $exppay = explode('=', ($row['idpayment'] ?? ''));
1962 $payment = VikBooking::getPayment($exppay[0], $vbo_tn);
1963
1964 /**
1965 * Scan the booking and related rooms for damage deposit payment data.
1966 *
1967 * @since 1.17.6 (J) - 1.7.6 (WP)
1968 */
1969 $damage_deposit_payment = VBORoomHelper::getInstance()->getDamageDepositSplitPayment($row, $orderrooms);
1970
1971 if ($app->input->getBool('dd') && !empty($damage_deposit_payment['payment_window']['pay_id'])) {
1972 // load the proper payment driver
1973 $payment = VikBooking::getPayment($damage_deposit_payment['payment_window']['pay_id']) ?: $payment;
1974 }
1975
1976 if (!$payment) {
1977 VBOHttpDocument::getInstance()->close(500, 'Could not load payment processor for validation.');
1978 }
1979
1980 // calculate booking totals
1981 $isdue = 0;
1982 $tot_taxes = 0;
1983 $tot_city_taxes = 0;
1984 $tot_fees = 0;
1985 $tot_damage_dep = 0;
1986 $pricestr = [];
1987 $optstr = [];
1988 foreach ($orderrooms as $kor => $or) {
1989 $num = $kor + 1;
1990
1991 // determine the number of nights of stay and dates to consider
1992 $use_los = $row['days'];
1993 $room_checkin = $row['checkin'];
1994 $room_checkout = $row['checkout'];
1995 if (!empty($split_stay) && !empty($split_stay[$kor]) && $split_stay[$kor]['idroom'] == $or['idroom']) {
1996 $use_los = (int)$split_stay[$kor]['nights'];
1997 $room_checkin = $split_stay[$kor]['checkin_ts'];
1998 $room_checkout = $split_stay[$kor]['checkout_ts'];
1999 }
2000
2001 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
2002 // package cost or cust_cost may not be inclusive of taxes if prices tax included is off
2003 $calctar = VikBooking::sayPackagePlusIva($or['cust_cost'], $or['cust_idiva']);
2004 $isdue += $calctar;
2005 if ($calctar == $or['cust_cost']) {
2006 $cost_minus_tax = VikBooking::sayPackageMinusIva($or['cust_cost'], $or['cust_idiva']);
2007 $tot_taxes += ($or['cust_cost'] - $cost_minus_tax);
2008 } else {
2009 $tot_taxes += ($calctar - $or['cust_cost']);
2010 }
2011 $pricestr[$num] = (!empty($or['pkg_name']) ? $or['pkg_name'] : (!empty($or['otarplan']) ? ucwords($or['otarplan']) : JText::translate('VBOROOMCUSTRATEPLAN'))).": ".$calctar." ".$currencyname;
2012 } elseif (array_key_exists($num, $tars) && is_array($tars[$num])) {
2013 $calctar = VikBooking::sayCostPlusIva($tars[$num]['cost'], $tars[$num]['idprice']);
2014 $tars[$num]['calctar'] = $calctar;
2015 $isdue += $calctar;
2016 if ($calctar == $tars[$num]['cost']) {
2017 $cost_minus_tax = VikBooking::sayCostMinusIva($tars[$num]['cost'], $tars[$num]['idprice']);
2018 $tot_taxes += ($tars[$num]['cost'] - $cost_minus_tax);
2019 } else {
2020 $tot_taxes += ($calctar - $tars[$num]['cost']);
2021 }
2022 $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'] : "");
2023 }
2024 if (!empty($or['optionals'])) {
2025 $stepo = explode(";", $or['optionals']);
2026 foreach ($stepo as $roptkey => $oo) {
2027 if (empty($oo)) {
2028 continue;
2029 }
2030 $stept = explode(":", $oo);
2031 $q = "SELECT * FROM `#__vikbooking_optionals` WHERE `id`=" . $dbo->quote($stept[0]) . ";";
2032 $dbo->setQuery($q);
2033 $actopt = $dbo->loadAssocList();
2034 if ($actopt) {
2035 $vbo_tn->translateContents($actopt, '#__vikbooking_optionals');
2036
2037 // option params
2038 $opt_params = !empty($actopt[0]['oparams']) ? json_decode($actopt[0]['oparams'], true) : [];
2039 $opt_params = is_array($opt_params) ? $opt_params : [];
2040
2041 $chvar = '';
2042 if (!empty($actopt[0]['ageintervals']) && $or['children'] > 0 && strstr($stept[1], '-') != false) {
2043 $optagenames = VikBooking::getOptionIntervalsAges($actopt[0]['ageintervals']);
2044 $optagepcent = VikBooking::getOptionIntervalsPercentage($actopt[0]['ageintervals']);
2045 $optageovrct = VikBooking::getOptionIntervalChildOverrides($actopt[0], $or['adults'], $or['children']);
2046 $child_num = VikBooking::getRoomOptionChildNumber($or['optionals'], $actopt[0]['id'], $roptkey, $or['children']);
2047 $optagecosts = VikBooking::getOptionIntervalsCosts(isset($optageovrct['ageintervals_child' . ($child_num + 1)]) ? $optageovrct['ageintervals_child' . ($child_num + 1)] : $actopt[0]['ageintervals']);
2048 $agestept = explode('-', $stept[1]);
2049 $stept[1] = $agestept[0];
2050 $chvar = $agestept[1];
2051 if (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 1) {
2052 //percentage value of the adults tariff
2053 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
2054 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
2055 } else {
2056 $optagecosts[($chvar - 1)] = $tars[$num]['cost'] * $optagecosts[($chvar - 1)] / 100;
2057 }
2058 } elseif (array_key_exists(($chvar - 1), $optagepcent) && $optagepcent[($chvar - 1)] == 2) {
2059 //VBO 1.10 - percentage value of room base cost
2060 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
2061 $optagecosts[($chvar - 1)] = $or['cust_cost'] * $optagecosts[($chvar - 1)] / 100;
2062 } else {
2063 $display_rate = isset($tars[$num]['room_base_cost']) ? $tars[$num]['room_base_cost'] : $tars[$num]['cost'];
2064 $optagecosts[($chvar - 1)] = $display_rate * $optagecosts[($chvar - 1)] / 100;
2065 }
2066 }
2067 $actopt[0]['chageintv'] = $chvar;
2068 $actopt[0]['name'] .= ' ('.$optagenames[($chvar - 1)].')';
2069 $actopt[0]['quan'] = $stept[1];
2070 $realcost = (intval($actopt[0]['perday']) == 1 ? (floatval($optagecosts[($chvar - 1)]) * $use_los * $stept[1]) : (floatval($optagecosts[($chvar - 1)]) * $stept[1]));
2071 } else {
2072 $actopt[0]['quan'] = $stept[1];
2073 // VBO 1.11 - options percentage cost of the room total fee
2074 if ($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
2075 $deftar_basecosts = $or['cust_cost'];
2076 } else {
2077 $deftar_basecosts = $tars[$num]['cost'];
2078 }
2079 $actopt[0]['cost'] = (int)$actopt[0]['pcentroom'] ? ($deftar_basecosts * $actopt[0]['cost'] / 100) : $actopt[0]['cost'];
2080 //
2081 $realcost = (intval($actopt[0]['perday']) == 1 ? ($actopt[0]['cost'] * $use_los * $stept[1]) : ($actopt[0]['cost'] * $stept[1]));
2082 }
2083 if (!empty($actopt[0]['maxprice']) && $actopt[0]['maxprice'] > 0 && $realcost > $actopt[0]['maxprice']) {
2084 $realcost = $actopt[0]['maxprice'];
2085 if (intval($actopt[0]['hmany']) == 1 && intval($stept[1]) > 1) {
2086 $realcost = $actopt[0]['maxprice'] * $stept[1];
2087 }
2088 }
2089 if ($actopt[0]['perperson'] == 1) {
2090 $realcost = $realcost * $or['adults'];
2091 }
2092
2093 /**
2094 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
2095 *
2096 * @since 1.17.7 (J) - 1.7.7 (WP)
2097 */
2098 $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$realcost, &$actopt[0], $row, $or]);
2099 if ($custom_calculation) {
2100 $realcost = (float) $custom_calculation[0];
2101 }
2102
2103 $opt_minus_tax = VikBooking::sayOptionalsMinusIva($realcost, $actopt[0]['idiva']);
2104 $tmpopr = VikBooking::sayOptionalsPlusIva($realcost, $actopt[0]['idiva']);
2105 if ($actopt[0]['is_citytax'] == 1) {
2106 $tot_city_taxes += $opt_minus_tax;
2107 } elseif ($actopt[0]['is_fee'] == 1) {
2108 $tot_fees += $opt_minus_tax;
2109 } elseif ($opt_params['damagedep'] ?? 0) {
2110 $tot_damage_dep += $opt_minus_tax;
2111 }
2112 // always calculate the amount of tax no matter if this is already a tax or a fee
2113 if ($tmpopr == $realcost) {
2114 $tot_taxes += ($realcost - $opt_minus_tax);
2115 } else {
2116 $tot_taxes += ($tmpopr - $realcost);
2117 }
2118 //
2119 $isdue += $tmpopr;
2120 $optstr[$num][] = ($stept[1] > 1 ? $stept[1] . " " : "") . $actopt[0]['name'] . ": " . $tmpopr . " " . $currencyname . "\n";
2121 }
2122 }
2123 }
2124
2125 // custom extra costs
2126 if (!empty($or['extracosts'])) {
2127 $cur_extra_costs = json_decode($or['extracosts'], true);
2128 foreach ($cur_extra_costs as $eck => $ecv) {
2129 $ecplustax = !empty($ecv['idtax']) ? VikBooking::sayOptionalsPlusIva($ecv['cost'], $ecv['idtax']) : $ecv['cost'];
2130 $isdue += $ecplustax;
2131 $optstr[$num][] = $ecv['name'] . ": " . $ecplustax . " " . $currencyname."\n";
2132 }
2133 }
2134 }
2135
2136 // coupon
2137 $usedcoupon = false;
2138 $origisdue = $isdue;
2139 if (strlen($row['coupon']) > 0) {
2140 $usedcoupon = true;
2141 $expcoupon = explode(";", $row['coupon']);
2142 $isdue = $isdue - $expcoupon[1];
2143 }
2144
2145 if (empty($row['sid']) && !empty($row['idorderota']) && !empty($row['channel'])) {
2146 $row['sid'] = $row['idorderota'];
2147 }
2148
2149 /**
2150 * Trigger event to allow third-party plugins to manipulate the transaction data.
2151 *
2152 * @since 1.18.5 (J) - 1.8.5 (WP)
2153 */
2154 VBOFactory::getPlatform()->getDispatcher()->trigger('onInitPaymentTransaction', [&$row, &$payment['params'], []]);
2155
2156 if (VBOPlatformDetection::isWordPress()) {
2157 /**
2158 * @wponly The payment gateway is now loaded
2159 * using the apposite dispatcher.
2160 *
2161 * @since 1.0.5
2162 */
2163 JLoader::import('adapter.payment.dispatcher');
2164 $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'];
2165 $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'];
2166 $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";
2167 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
2168 $itemid = $model->best(array('booking'), (!empty($row['lang']) ? $row['lang'] : null));
2169 $extra_data = [];
2170 if ($itemid) {
2171 $return_url = str_replace(JUri::root(), '', $return_url);
2172 $error_url = str_replace(JUri::root(), '', $error_url);
2173 $notify_url = str_replace(JUri::root(), '', $notify_url);
2174 $return_url = JRoute::rewrite($return_url . "&Itemid={$itemid}", false);
2175 $error_url = JRoute::rewrite($error_url . "&Itemid={$itemid}", false);
2176 $notify_url = JRoute::rewrite($notify_url . "&Itemid={$itemid}", false);
2177 $extra_data = array(
2178 'return_url' => $return_url,
2179 'error_url' => $error_url,
2180 'notify_url' => $notify_url,
2181 );
2182 }
2183 $extra_data['transaction_currency'] = VikBooking::getCurrencyCodePp();
2184
2185 $obj = JPaymentDispatcher::getInstance('vikbooking', $payment['file'], array_merge($row, $extra_data), $payment['params']);
2186 } else {
2187 /**
2188 * @joomlaonly The Payment Factory library will invoke the gateway.
2189 * Make sure to pass the payment gateway some common variables together with the order record.
2190 *
2191 * @since 1.14.3
2192 */
2193 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'payments' . DIRECTORY_SEPARATOR . 'libraries' . DIRECTORY_SEPARATOR . 'factory.php';
2194
2195 $bestitemid = VikBooking::findProperItemIdType(array('booking'));
2196 $extra_data = array(
2197 '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)),
2198 '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)),
2199 '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),
2200 );
2201 $extra_data['transaction_currency'] = VikBooking::getCurrencyCodePp();
2202
2203 $obj = VBOPaymentFactory::getPaymentInstance($payment['file'], array_merge($row, $extra_data), $payment['params']);
2204 }
2205
2206 try {
2207 // let the gateway validate the payment transaction
2208 $array_result = $obj->validatePayment();
2209 } catch (Throwable $e) {
2210 // silently catch the error and set the log
2211 $array_result = [
2212 'verified' => 0,
2213 'log' => $e->getMessage() ?: 'Transaction error exception.',
2214 ];
2215 }
2216
2217 // build payment log
2218 $newpaymentlog = date('c')."\n".$array_result['log']."\n----------\n".$row['paymentlog'];
2219
2220 /**
2221 * OTA reservations containing PCI-DSS card details may receive additional payments through
2222 * the website for upselling or for payments requested. Therefore, the previous card logs
2223 * should be appended, not prepended to the current payment logs for the card details.
2224 *
2225 * @since 1.15.0 (J) - 1.5.0 (WP)
2226 */
2227 if (!empty($row['idorderota']) && !empty($row['channel']) && !empty($row['paymentlog'])) {
2228 if (stripos($row['paymentlog'], 'card number') !== false && strpos($row['paymentlog'], '*') !== false) {
2229 $newpaymentlog = $row['paymentlog'] . "\n----------\n" . date('c') . "\n" . $array_result['log'];
2230 }
2231 }
2232
2233 /**
2234 * Ensure the size of the log does not make the query fail.
2235 *
2236 * @since 1.16.9 (J) - 1.6.9 (WP)
2237 */
2238 if (strlen($newpaymentlog) > 55000) {
2239 $newpaymentlog = substr($newpaymentlog, 0, 55000) . '...';
2240 }
2241
2242 if ($array_result['verified'] == 1) {
2243 // valid payment
2244 $shouldpay = $isdue;
2245
2246 if ($payment['charge'] > 0.00) {
2247 if ($payment['ch_disc'] == 1) {
2248 // charge
2249 if ($payment['val_pcent'] == 1) {
2250 // fixed value
2251 $shouldpay += $payment['charge'];
2252 } else {
2253 // percent value
2254 $percent_to_pay = $shouldpay * $payment['charge'] / 100;
2255 $shouldpay += $percent_to_pay;
2256 }
2257 } else {
2258 // discount
2259 if ($payment['val_pcent'] == 1) {
2260 // fixed value
2261 $shouldpay -= $payment['charge'];
2262 } else {
2263 // percent value
2264 $percent_to_pay = $shouldpay * $payment['charge'] / 100;
2265 $shouldpay -= $percent_to_pay;
2266 }
2267 }
2268 }
2269
2270 // deposit may be skipped by customer choice
2271 $shouldpay_befdep = $shouldpay;
2272
2273 if (!VikBooking::payTotal()) {
2274 $percentdeposit = VikBooking::getAccPerCent();
2275 if ($percentdeposit > 0) {
2276 if (VikBooking::getTypeDeposit() == "fixed") {
2277 $shouldpay = $percentdeposit;
2278 } else {
2279 $shouldpay = $shouldpay * $percentdeposit / 100;
2280 }
2281 }
2282 }
2283
2284 // check if a damage deposit was allowed to be paid
2285 $shouldpay_dd = $damage_deposit_payment['damagedep_gross'] ?? 0;
2286 $shouldpay_befdd = $shouldpay - $shouldpay_dd;
2287
2288 // check if the total amount paid is the same as the order total
2289 if (isset($array_result['tot_paid'])) {
2290 $shouldpay = round($shouldpay, 2);
2291 $shouldpay_befdep = round($shouldpay_befdep, 2);
2292 $shouldpay_less_damagedep = round(($row['total'] - $row['tot_damage_dep']), 2);
2293 $totreceived = round($array_result['tot_paid'], 2);
2294 if ($shouldpay != $totreceived && $shouldpay_befdep != $totreceived && $shouldpay_befdd != $totreceived && $shouldpay_less_damagedep != $totreceived && $shouldpay_dd != $totreceived && $row['paymcount'] == 0) {
2295 // the amount paid is different than the order total
2296 // fares might have changed or the deposit might be different
2297 // Sending just an email to the admin that will check
2298 $vbo_app = VikBooking::getVboApplication();
2299 $adsendermail = VikBooking::getSenderMail();
2300 $vbo_app->sendMail($adsendermail, $adsendermail, $recipient_mail, $adsendermail, JText::translate('VBTOTPAYMENTINVALID'), JText::sprintf('VBTOTPAYMENTINVALIDTXT', $row['id'], $totreceived." (".$array_result['tot_paid'].")", $shouldpay), false);
2301 }
2302
2303 // amount paid should be stored as exclusive of transaction fees/discounts
2304 if ($payment['charge'] > 0.00) {
2305 if ($payment['ch_disc'] == 1) {
2306 // charge
2307 if ($payment['val_pcent'] == 1) {
2308 // fixed value
2309 $array_result['tot_paid'] -= $payment['charge'];
2310 } else {
2311 // percent value
2312 $array_result['tot_paid'] = ($array_result['tot_paid'] / ((100 + $payment['charge']) / 100));
2313 }
2314 } else {
2315 // discount
2316 if ($payment['val_pcent'] == 1) {
2317 // fixed value
2318 $array_result['tot_paid'] += $payment['charge'];
2319 } else {
2320 // percent value
2321 $array_result['tot_paid'] = $array_result['tot_paid'] * (100 + $payment['charge']) / 100;
2322 }
2323 }
2324 $array_result['tot_paid'] = round($array_result['tot_paid'], 2);
2325 }
2326 }
2327
2328 if ($row['paymcount'] == 0 || $row['status'] == 'standby') {
2329 foreach ($orderrooms as $indnum => $r) {
2330 $num = $indnum + 1;
2331
2332 // determine the number of nights of stay and dates to consider
2333 $room_checkin = $row['checkin'];
2334 $room_checkout = $row['checkout'];
2335 $room_realback = $turnover_secs + $row['checkout'];
2336 if (!empty($split_stay) && !empty($split_stay[$indnum]) && $split_stay[$indnum]['idroom'] == $r['idroom']) {
2337 $room_checkin = $split_stay[$indnum]['checkin_ts'];
2338 $room_checkout = $split_stay[$indnum]['checkout_ts'];
2339 $room_realback = $turnover_secs + $split_stay[$indnum]['checkout_ts'];
2340 }
2341
2342 $busy_record = new stdClass;
2343 $busy_record->idroom = $r['idroom'];
2344 $busy_record->checkin = $room_checkin;
2345 $busy_record->checkout = $room_checkout;
2346 $busy_record->realback = $room_realback;
2347
2348 $dbo->insertObject('#__vikbooking_busy', $busy_record, 'id');
2349
2350 if (!isset($busy_record->id)) {
2351 continue;
2352 }
2353
2354 $q = "INSERT INTO `#__vikbooking_ordersbusy` (`idorder`,`idbusy`) VALUES(" . (int)$row['id'] . ", " . (int)$busy_record->id . ");";
2355 $dbo->setQuery($q);
2356 $dbo->execute();
2357 }
2358 }
2359
2360 // ConfirmationNumber
2361 if ($row['paymcount'] == 0 || $row['status'] == 'standby') {
2362 $confirmnumber = VikBooking::generateConfirmNumber($row['id'], true);
2363 }
2364
2365 // update payable amount in case of up-sells or simply in case of another payment received
2366 $new_payable = isset($array_result['tot_paid']) && $array_result['tot_paid'] ? ($row['payable'] - $array_result['tot_paid']) : 0;
2367 $new_payable = $new_payable < 0 ? 0 : $new_payable;
2368
2369 // update booking record
2370 $booking_record = new stdClass;
2371 $booking_record->id = $row['id'];
2372 $booking_record->status = 'confirmed';
2373 if (isset($array_result['tot_paid']) && $array_result['tot_paid']) {
2374 $booking_record->totpaid = ($array_result['tot_paid'] + $row['totpaid']);
2375 }
2376 $booking_record->paymcount = ($row['paymcount'] + 1);
2377 if (!empty($array_result['log'])) {
2378 $booking_record->paymentlog = $newpaymentlog;
2379 }
2380 $booking_record->payable = $new_payable;
2381
2382 $dbo->updateObject('#__vikbooking_orders', $booking_record, 'id');
2383
2384 // assign room specific unit
2385 $set_room_indexes = VikBooking::autoRoomUnit();
2386 $room_indexes_usemap = [];
2387 if ($set_room_indexes === true) {
2388 $q = "SELECT `id`,`idroom`,`roomindex` FROM `#__vikbooking_ordersrooms` WHERE `idorder`=".(int)$row['id'].";";
2389 $dbo->setQuery($q);
2390 $orooms = $dbo->loadAssocList();
2391 foreach ($orooms as $oroom) {
2392 if (!empty($oroom['roomindex'])) {
2393 // room specific unit has already been assigned
2394 continue;
2395 }
2396 $room_indexes = VikBooking::getRoomUnitNumsAvailable($row, $oroom['idroom']);
2397 $use_ind_key = 0;
2398 if ($room_indexes) {
2399 if (!array_key_exists($oroom['idroom'], $room_indexes_usemap)) {
2400 $room_indexes_usemap[$oroom['idroom']] = $use_ind_key;
2401 } else {
2402 $use_ind_key = $room_indexes_usemap[$oroom['idroom']];
2403 }
2404 $q = "UPDATE `#__vikbooking_ordersrooms` SET `roomindex`=".(int)$room_indexes[$use_ind_key]." WHERE `id`=".(int)$oroom['id'].";";
2405 $dbo->setQuery($q);
2406 $dbo->execute();
2407 // update rooms references for the customer email sending function
2408 foreach ($rooms as $rnum => $rr) {
2409 if ($rr['or_id'] == $oroom['id']) {
2410 $rooms[$rnum]['roomindex'] = (int)$room_indexes[$use_ind_key];
2411 break;
2412 }
2413 }
2414 $room_indexes_usemap[$oroom['idroom']]++;
2415 }
2416 }
2417 }
2418
2419 // unlock room(s) for other imminent bookings
2420 $q = "DELETE FROM `#__vikbooking_tmplock` WHERE `idorder`=" . intval($row['id']) . ";";
2421 $dbo->setQuery($q);
2422 $dbo->execute();
2423
2424 // customer booking
2425 $q = "SELECT `idcustomer` FROM `#__vikbooking_customers_orders` WHERE `idorder`=".(int)$row['id'].";";
2426 $dbo->setQuery($q);
2427 $customer_id = $dbo->loadResult();
2428 if ($customer_id) {
2429 $cpin = VikBooking::getCPinIstance();
2430 $cpin->updateBookingCommissions($row['id'], $customer_id);
2431 }
2432
2433 // check if some of the rooms booked have shared calendars
2434 VikBooking::updateSharedCalendars($row['id'], array(), $row['checkin'], $row['checkout']);
2435
2436 /**
2437 * Trigger event to allow third-party plugins to choose whether payment notifications should be sent
2438 *
2439 * @since 1.16.10 (J) - 1.6.10 (WP)
2440 */
2441 $send_notifications = true;
2442 $should_send = VBOFactory::getPlatform()->getDispatcher()->filter('onPaymentReceivedShouldSendNotifications', [$row]);
2443 if (is_array($should_send) && in_array(false, $should_send, true)) {
2444 $send_notifications = false;
2445 }
2446
2447 if ($send_notifications) {
2448 // send email notification to guest and admin
2449 VikBooking::sendBookingEmail($row['id'], array('guest', 'admin'));
2450
2451 // SMS
2452 VikBooking::sendBookingSMS($row['id']);
2453 }
2454
2455 /**
2456 * Payment gateways may set and return the transaction information
2457 * to eventually support a later transaction of type refund.
2458 *
2459 * @since 1.14 (J) - 1.4.0 (WP)
2460 * @since 1.16.2 (J) - 1.6.2 (WP) we attempt to always store the amount paid with this transaction.
2461 * @since 1.16.9 (J) - 1.6.9 (WP) we attempt to store the amount of payment processing fees for the transaction.
2462 * @since 1.17.6 (J) - 1.7.6 (WP) added support to separate payment for damage deposit.
2463 */
2464 $tn_data = $array_result['transaction'] ?? null;
2465 if (isset($array_result['tot_paid']) && $array_result['tot_paid']) {
2466 // check event data payload to store
2467 if (is_array($tn_data)) {
2468 // set key
2469 $tn_data['amount_paid'] = (float) $array_result['tot_paid'];
2470 } elseif (is_object($tn_data)) {
2471 // set property
2472 $tn_data->amount_paid = (float) $array_result['tot_paid'];
2473 } elseif (!$tn_data) {
2474 // build an array (we add the payment name because we know there is no other transaction data)
2475 $tn_data = [
2476 'amount_paid' => (float) $array_result['tot_paid'],
2477 'payment_method' => $payment['name'],
2478 ];
2479 }
2480 }
2481 if ($tn_data && isset($array_result['tot_fees']) && $array_result['tot_fees']) {
2482 // check event data payload to store
2483 if (is_array($tn_data)) {
2484 // set key
2485 $tn_data['processing_fees'] = (float) $array_result['tot_fees'];
2486 } elseif (is_object($tn_data)) {
2487 // set property
2488 $tn_data->processing_fees = (float) $array_result['tot_fees'];
2489 }
2490 }
2491 if ($app->input->getBool('dd') && ($damage_deposit_payment['damagedep_gross'] ?? null)) {
2492 if ($tn_data) {
2493 $tn_data = (array) $tn_data;
2494 } else {
2495 $tn_data = [];
2496 }
2497 // damage deposit may be authorized without an amount paid returned
2498 $tn_data['damage_deposit'] = (float) $damage_deposit_payment['damagedep_gross'];
2499 }
2500
2501 // Booking History
2502 VikBooking::getBookingHistoryInstance()->setBid($row['id'])->setExtraData($tn_data)->store('P' . ($row['paymcount'] > 0 ? 'N' : '0'), $payment['name']);
2503
2504 // invoke VikChannelManager
2505 if (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "synch.vikbooking.php")) {
2506 require_once(VCM_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "synch.vikbooking.php");
2507 $vcm = new SynchVikBooking($row['id']);
2508 $vcm->setPushType('new')->sendRequest();
2509 }
2510 $vcmchanneldata = $session->get('vcmChannelData', '');
2511 if (!empty($vcmchanneldata)) {
2512 $session->set('vcmChannelData', '');
2513 }
2514 //end invoke VikChannelManager
2515 if (method_exists($obj, 'afterValidation')) {
2516 $obj->afterValidation(1);
2517 }
2518 } else {
2519 if (empty($array_result['skip_email'])) {
2520 $vbo_app = VikBooking::getVboApplication();
2521 $adsendermail = VikBooking::getSenderMail();
2522 $vbo_app->sendMail($adsendermail, $adsendermail, $recipient_mail, $adsendermail, JText::translate('VBPAYMENTNOTVER'), JText::translate('VBSERVRESP') . ":\n\n" . $array_result['log'], false);
2523 }
2524 if (!empty($array_result['log'])) {
2525 $q = "UPDATE `#__vikbooking_orders` SET `paymentlog`=".$dbo->quote($newpaymentlog)." WHERE `id`='" . $row['id'] . "';";
2526 $dbo->setQuery($q);
2527 $dbo->execute();
2528 }
2529 if (method_exists($obj, 'afterValidation')) {
2530 $obj->afterValidation(0);
2531 }
2532 }
2533 }
2534
2535 public function currencyconverter()
2536 {
2537 if (!JSession::checkToken()) {
2538 // missing CSRF-proof token
2539 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
2540 }
2541
2542 $session = JFactory::getSession();
2543 $pprices = VikRequest::getVar('prices', array(0));
2544 $pfromsymbol = VikRequest::getString('fromsymbol', '', 'request');
2545 $ptocurrency = VikRequest::getString('tocurrency', '', 'request');
2546 $pfromcurrency = VikRequest::getString('fromcurrency', '', 'request');
2547 $default_cur = !empty($pfromcurrency) ? $pfromcurrency : VikBooking::getCurrencyName();
2548 $response = array();
2549 if (!empty($default_cur) && !empty($pprices) && count($pprices) > 0 && !empty($ptocurrency)) {
2550 require_once(VBO_SITE_PATH . DS . "helpers" . DS ."currencyconverter.php");
2551 if ($default_cur != $ptocurrency) {
2552 $format = VikBooking::getNumberFormatData();
2553 $converter = new VboCurrencyConverter($default_cur, $ptocurrency, $pprices, explode(':', $format));
2554 $exchanged = $converter->convert();
2555 if (count($exchanged) > 0) {
2556 $response = $exchanged;
2557 $session->set('vboLastCurrency', $ptocurrency);
2558 } else {
2559 $conv_error = $converter->getError();
2560 $response['error'] = !empty($conv_error) ? $conv_error : JText::translate('VBERRCURCONVINVALIDDATA');
2561 }
2562 } else {
2563 $session->set('vboLastCurrency', $ptocurrency);
2564 foreach ($pprices as $i => $price) {
2565 $response[$i]['symbol'] = $pfromsymbol;
2566 $response[$i]['price'] = $price;
2567 }
2568 }
2569 } else {
2570 $response['error'] = JText::translate('VBERRCURCONVNODATA');
2571 }
2572 if(array_key_exists('error', $response)) {
2573 $session->set('vboLastCurrency', $ptocurrency);
2574 }
2575 echo json_encode($response);
2576 exit;
2577 }
2578
2579 public function signature()
2580 {
2581 VikRequest::setVar('view', 'signature');
2582 parent::display();
2583 }
2584
2585 public function storesignature()
2586 {
2587 if (!JSession::checkToken()) {
2588 // missing CSRF-proof token
2589 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
2590 }
2591
2592 $sid = VikRequest::getString('sid', '', 'request');
2593 $ts = VikRequest::getString('ts', '', 'request');
2594 $psignature = VikRequest::getString('signature', '', 'request', VIKREQUEST_ALLOWRAW);
2595 $ppad_width = VikRequest::getInt('pad_width', '', 'request');
2596 $ppad_ratio = VikRequest::getInt('pad_ratio', '', 'request');
2597 $pitemid = VikRequest::getInt('Itemid', '', 'request');
2598 $ptmpl = VikRequest::getString('tmpl', '', 'request');
2599 $dbo = JFactory::getDBO();
2600 $mainframe = JFactory::getApplication();
2601 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `ts`=" . $dbo->quote($ts) . " AND `sid`=" . $dbo->quote($sid) . " AND `status`='confirmed';";
2602 $dbo->setQuery($q);
2603 $dbo->execute();
2604 if ($dbo->getNumRows() < 1) {
2605 VikError::raiseWarning('', 'Booking not found');
2606 $mainframe->redirect('index.php');
2607 exit;
2608 }
2609 $row = $dbo->loadAssoc();
2610 $tonight = mktime(23, 59, 59, date('n'), date('j'), date('Y'));
2611 if ($tonight > $row['checkout']) {
2612 VikError::raiseWarning('', 'Check-out date is in the past');
2613 $mainframe->redirect('index.php');
2614 exit;
2615 }
2616 $customer = array();
2617 $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'].";";
2618 $dbo->setQuery($q);
2619 $dbo->execute();
2620 if ($dbo->getNumRows() > 0) {
2621 $customer = $dbo->loadAssoc();
2622 }
2623 if (!(count($customer) > 0)) {
2624 VikError::raiseWarning('', 'Customer not found');
2625 $mainframe->redirect('index.php');
2626 exit;
2627 }
2628 //check if the signature has been submitted
2629 $signature_data = '';
2630 $cont_type = '';
2631 if (!empty($psignature)) {
2632 /**
2633 * Implemented safe filtering of base64-encoded signature image
2634 * to obtain content and file extension.
2635 *
2636 * @since 1.15.1 (J) - 1.5.4 (WP)
2637 */
2638 if (preg_match("/^data:image\/(png|jpe?g|svg);base64,([A-Za-z0-9\/=+]+)$/", $psignature, $safe_match)) {
2639 $signature_data = base64_decode($safe_match[2]);
2640 $cont_type = $safe_match[1];
2641 }
2642 }
2643 $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);
2644 if (empty($signature_data)) {
2645 VikError::raiseWarning('', JText::translate('VBOSIGNATUREISEMPTY'));
2646 $mainframe->redirect($ret_link);
2647 exit;
2648 }
2649 //write file
2650 $sign_fname = $row['id'].'_'.$row['sid'].'_'.$customer['id'].'.'.$cont_type;
2651 $filepath = VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'idscans' . DIRECTORY_SEPARATOR . $sign_fname;
2652 $fp = fopen($filepath, 'w+');
2653 $bytes = fwrite($fp, $signature_data);
2654 fclose($fp);
2655 if ($bytes !== false && $bytes > 0) {
2656 //update the signature in the DB
2657 $q = "UPDATE `#__vikbooking_customers_orders` SET `signature`=".$dbo->quote($sign_fname)." WHERE `idorder`=".(int)$row['id'].";";
2658 $dbo->setQuery($q);
2659 $dbo->execute();
2660 $mainframe->enqueueMessage(JText::translate('VBOSIGNATURETHANKS'));
2661 //resize image for screens with high resolution
2662 if ($ppad_ratio > 1) {
2663 $new_width = floor(($ppad_width / 2));
2664 $creativik = new vikResizer();
2665 $creativik->proportionalImage($filepath, $filepath, $new_width, $new_width);
2666 }
2667 //
2668 } else {
2669 VikError::raiseWarning('', JText::translate('VBOERRSTORESIGNFILE'));
2670 }
2671 $mainframe->redirect($ret_link);
2672 exit;
2673 }
2674
2675 public function validatepin()
2676 {
2677 if (!JSession::checkToken()) {
2678 // missing CSRF-proof token
2679 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
2680 }
2681
2682 $cpin = VikBooking::getCPinIstance();
2683
2684 $ppin = VikRequest::getString('pin', '', 'request');
2685
2686 $response = [];
2687
2688 $customer = $cpin->getCustomerByPin($ppin);
2689 if ($customer) {
2690 $response = $customer;
2691 $response['success'] = 1;
2692
2693 if ($cpin->getCustomerCoupon($customer)) {
2694 // set flag indicating that the customer has got dedicated discounts
2695 $response['has_discounts'] = 1;
2696 }
2697 }
2698
2699 echo json_encode($response);
2700 exit;
2701 }
2702
2703 public function docancelbooking()
2704 {
2705 if (!JSession::checkToken()) {
2706 // missing CSRF-proof token
2707 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
2708 }
2709
2710 $dbo = JFactory::getDbo();
2711 $mainframe = JFactory::getApplication();
2712
2713 $psid = VikRequest::getString('sid', '', 'request');
2714 $pidorder = VikRequest::getString('idorder', '', 'request');
2715
2716 if (!empty($psid) && !empty($pidorder)) {
2717 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=".intval($pidorder)." AND `sid`=".$dbo->quote($psid)." AND `status`='confirmed';";
2718 $dbo->setQuery($q);
2719 $order = $dbo->loadAssocList();
2720 if ($order) {
2721 $pemail = VikRequest::getString('email', '', 'request');
2722 $preason = VikRequest::getString('reason', '', 'request');
2723 if (!empty($pemail) && !empty($preason)) {
2724 $to = VikBooking::getAdminMail();
2725 if (strpos($to, ',') !== false) {
2726 $all_recipients = explode(',', $to);
2727 foreach ($all_recipients as $k => $v) {
2728 if (empty($v)) {
2729 unset($all_recipients[$k]);
2730 }
2731 }
2732 if (count($all_recipients) > 0) {
2733 $to = $all_recipients;
2734 }
2735 }
2736 //check if the booking can be cancelled
2737 $days_to_arrival = 0;
2738 $is_refundable = 0;
2739 $daysadv_refund_arr = array();
2740 $daysadv_refund = 0;
2741 $now_info = getdate();
2742 $checkin_info = getdate($order[0]['checkin']);
2743 if ($now_info[0] < $checkin_info[0]) {
2744 while ($now_info[0] < $checkin_info[0]) {
2745 if (!($now_info['mday'] != $checkin_info['mday'] || $now_info['mon'] != $checkin_info['mon'] || $now_info['year'] != $checkin_info['year'])) {
2746 break;
2747 }
2748 $days_to_arrival++;
2749 $now_info = getdate(mktime(0, 0, 0, $now_info['mon'], ($now_info['mday'] + 1), $now_info['year']));
2750 }
2751 }
2752 $tars = array();
2753 $is_package = !empty($order[0]['pkg']) ? true : false;
2754 $orderrooms = array();
2755 $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;";
2756 $dbo->setQuery($q);
2757 $orderrooms = $dbo->loadAssocList();
2758 if ($orderrooms) {
2759 foreach($orderrooms as $kor => $or) {
2760 $num = $kor + 1;
2761 if($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
2762 //package or custom cost set from the back-end
2763 continue;
2764 }
2765 $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'] . "';";
2766 $dbo->setQuery($q);
2767 $tar = $dbo->loadAssocList();
2768 if ($tar) {
2769 $tars[$num] = $tar[0];
2770 }
2771 }
2772 }
2773 foreach ($tars as $num => $tar) {
2774 if ($tar['free_cancellation'] < 1) {
2775 //if at least one rate plan is non-refundable, the whole reservation cannot be cancelled
2776 $is_refundable = 0;
2777 $daysadv_refund_arr = array();
2778 break;
2779 }
2780 $is_refundable = 1;
2781 $daysadv_refund_arr[] = $tar['canc_deadline'];
2782 }
2783 //get the rate plan with the lowest cancellation deadline
2784 $daysadv_refund = count($daysadv_refund_arr) > 0 ? min($daysadv_refund_arr) : $daysadv_refund;
2785 $resmodcanc = VikBooking::getReservationModCanc();
2786 $resmodcanc = $days_to_arrival < 1 ? 0 : $resmodcanc;
2787 $resmodcancmin = VikBooking::getReservationModCancMin();
2788 $canc_allowed = ($resmodcanc > 1 && $resmodcanc != 2 && $is_refundable > 0 && $daysadv_refund <= $days_to_arrival && $days_to_arrival >= $resmodcancmin);
2789 if (!$canc_allowed) {
2790 VikError::raiseWarning('', JText::translate('VBOERRCANNOTCANCBOOK'));
2791 $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));
2792 exit;
2793 }
2794 //make the cancellation in the db and update the administrator notes with the reason specified by the customer
2795 $new_adminotes = JText::translate('VBOBOOKCANCELLEDEMAILSUBJ').' ('.$pemail.")\n".$preason."\n\n".$order[0]['adminnotes'];
2796 $q = "UPDATE `#__vikbooking_orders` SET `status`='cancelled',`adminnotes`=".$dbo->quote($new_adminotes)." WHERE `id`=".(int)$order[0]['id'].";";
2797 $dbo->setQuery($q);
2798 $dbo->execute();
2799 $q = "SELECT * FROM `#__vikbooking_ordersbusy` WHERE `idorder`=".(int)$order[0]['id'].";";
2800 $dbo->setQuery($q);
2801 $ordbusy = $dbo->loadAssocList();
2802 if ($ordbusy) {
2803 foreach ($ordbusy as $ob) {
2804 $q = "DELETE FROM `#__vikbooking_busy` WHERE `id`=".(int)$ob['idbusy'].";";
2805 $dbo->setQuery($q);
2806 $dbo->execute();
2807 }
2808 }
2809 $q = "DELETE FROM `#__vikbooking_ordersbusy` WHERE `idorder`=".(int)$order[0]['id'].";";
2810 $dbo->setQuery($q);
2811 $dbo->execute();
2812
2813 if ($order[0]['split_stay']) {
2814 // attempt to remove the transient record
2815 VBOFactory::getConfig()->remove('split_stay_' . $order[0]['id']);
2816 }
2817
2818 // Booking History
2819 $history_obj = VikBooking::getBookingHistoryInstance()->setBid($order[0]['id']);
2820 $history_obj->store('CW', $preason);
2821
2822 /**
2823 * Check if the amount paid can be refunded.
2824 *
2825 * @since 1.14 (J) - 1.4.0 (WP)
2826 */
2827 $admin_refund_error = '';
2828 $currencysymb = VikBooking::getCurrencySymb();
2829 $payment = VikBooking::getPayment($order[0]['idpayment']);
2830 $tn_driver = is_array($payment) ? $payment['file'] : null;
2831
2832 // transaction data validation callback
2833 $tn_data_callback = function($data) use ($tn_driver) {
2834 return (is_object($data) && isset($data->driver) && basename($data->driver, '.php') == basename($tn_driver, '.php'));
2835 };
2836 // get previous transactions
2837 $prev_tn_data = $history_obj->getEventsWithData(array('P0', 'PN'), $tn_data_callback);
2838
2839 if (is_array($prev_tn_data) && count($prev_tn_data) && $order[0]['totpaid'] > 0) {
2840 // previous transactions found and total paid > 0
2841 $refund_amount = $order[0]['totpaid'];
2842
2843 // push refund information for the payment gateway
2844 $order[0]['total_to_refund'] = $refund_amount;
2845 $order[0]['transaction'] = $prev_tn_data;
2846 $order[0]['refund_reason'] = $preason;
2847
2848 // push the transaction currency information
2849 $order[0]['transaction_currency'] = VikBooking::getCurrencyCodePp();
2850
2851 /**
2852 * Trigger event to allow third-party plugins to manipulate the transaction data.
2853 *
2854 * @since 1.18.5 (J) - 1.8.5 (WP)
2855 */
2856 VBOFactory::getPlatform()->getDispatcher()->trigger('onInitRefundTransaction', [&$order[0], &$payment['params']]);
2857
2858 /**
2859 * @wponly The payment gateway is loaded
2860 * through the apposite dispatcher.
2861 */
2862 JLoader::import('adapter.payment.dispatcher');
2863 $obj = JPaymentDispatcher::getInstance('vikbooking', $payment['file'], $order[0], $payment['params']);
2864
2865 // check if refund is supported by this gateway
2866 if (method_exists($obj, 'isRefundSupported') && $obj->isRefundSupported()) {
2867 // perform the refund transaction
2868 $array_result = $obj->refund();
2869
2870 if ($array_result['verified'] != 1) {
2871 // refund failed
2872 $admin_refund_error .= "\nRefund transaction failed\n";
2873 // get the refund error message
2874 $admin_refund_error .= !empty($array_result['log']) && is_string($array_result['log']) ? $array_result['log'] : '';
2875 } else {
2876 // refund was successful
2877
2878 /**
2879 * The history event extra data will contain the "amount_paid" (refunded).
2880 *
2881 * @since 1.16.9 (J) - 1.6.9 (WP)
2882 */
2883 if (!empty($array_result['tot_paid'])) {
2884 // overwrite the requested amount with the returned one
2885 $refund_amount = (float)$array_result['tot_paid'];
2886 }
2887 $history_obj->setExtraData([
2888 'amount_paid' => $refund_amount,
2889 ]);
2890
2891 // update total paid, total and refund columns for the booking
2892 $booking = new stdClass;
2893 $booking->id = $order[0]['id'];
2894 if ($order[0]['totpaid'] > 0) {
2895 $booking->totpaid = (float)($order[0]['totpaid'] - $refund_amount);
2896 }
2897 if ($order[0]['total'] > 0) {
2898 $booking->total = (float)($order[0]['total'] - $refund_amount);
2899 }
2900 $booking->refund = (float)$order[0]['refund'] + $refund_amount;
2901 // update record in db
2902 $dbo->updateObject('#__vikbooking_orders', $booking, 'id');
2903
2904 // store the refund event
2905 $event_descr = [
2906 '(' . $payment['name'] . ')',
2907 $currencysymb . ' ' . VikBooking::numberFormat($refund_amount),
2908 ];
2909 $history_obj->store('RF', implode("\n", $event_descr));
2910 }
2911 }
2912 }
2913
2914 // invoke VikChannelManager
2915 if (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "synch.vikbooking.php")) {
2916 $vcm_obj = VikBooking::getVcmInvoker();
2917 $vcm_obj->setOids(array($order[0]['id']))->setSyncType('cancel');
2918 $vcm_obj->doSync();
2919 }
2920 // end invoke VikChannelManager
2921
2922 //send email to the administrator
2923 $subject = JText::translate('VBOBOOKCANCELLEDEMAILSUBJ');
2924 // @wponly we do not need to pass the "best item id"
2925 $uri = VikBooking::externalroute("index.php?option=com_vikbooking&view=booking&sid=" . $order[0]['sid'] . "&ts=" . $order[0]['ts'], false);
2926 $msg = JText::sprintf('VBOBOOKCANCELLEDEMAILHEAD', $order[0]['id'], $uri) . "\n\n" . $preason . $admin_refund_error;
2927 $vbo_app = VikBooking::getVboApplication();
2928 $adsendermail = VikBooking::getSenderMail();
2929 $vbo_app->sendMail($adsendermail, $adsendermail, $to, $pemail, $subject, $msg, false);
2930
2931 // SMS
2932 VikBooking::sendBookingSMS($order[0]['id']);
2933
2934 // send cancellation email notification to guest
2935 VikBooking::sendBookingEmail($order[0]['id'], ['guest']);
2936
2937 // go back to the booking details page to show the new status
2938 $mainframe->enqueueMessage(JText::translate('VBOBOOKCANCELLEDRESP'));
2939 $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));
2940 } else {
2941 VikError::raiseWarning('', JText::translate('VBOERRMISSDATA'));
2942 $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));
2943 }
2944 } else {
2945 $mainframe->redirect("index.php");
2946 }
2947 } else {
2948 $mainframe->redirect("index.php");
2949 }
2950 }
2951
2952 public function cancelmodification()
2953 {
2954 $psid = VikRequest::getString('sid', '', 'request');
2955 $pidorder = VikRequest::getString('id', '', 'request');
2956 $dbo = JFactory::getDBO();
2957 $session = JFactory::getSession();
2958 $mainframe = JFactory::getApplication();
2959 if (!empty($psid) && !empty($pidorder)) {
2960 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=".intval($pidorder)." AND `sid`=".$dbo->quote($psid)." AND `status`='confirmed';";
2961 $dbo->setQuery($q);
2962 $dbo->execute();
2963 if ($dbo->getNumRows() == 1) {
2964 $order = $dbo->loadAssocList();
2965 //unset the session value and redirect
2966 $session->set('vboModBooking', '');
2967 $mainframe->redirect(JRoute::rewrite("index.php?option=com_vikbooking&view=booking&sid=".$order[0]['sid']."&ts=".$order[0]['ts'], false));
2968 } else {
2969 $mainframe->redirect("index.php");
2970 }
2971 } else {
2972 $mainframe->redirect("index.php");
2973 }
2974 }
2975
2976 public function tac_av_l()
2977 {
2978 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'tac.vikbooking.php');
2979
2980 //Channel Rates Module
2981 $pvbomodule = VikRequest::getInt('vbomodule', 0, 'request');
2982 $pshow_tax = VikRequest::getInt('show_tax', 0, 'request');
2983 $pdef_rplan = VikRequest::getInt('def_rplan', 0, 'request');
2984 $pchannels_sel = VikRequest::getVar('channels_sel', array());
2985 $pcheckin = VikRequest::getString('checkin', '', 'request');
2986 $pcheckout = VikRequest::getString('checkout', '', 'request');
2987 if ($pvbomodule > 0 && !empty($pcheckin) && !empty($pcheckout)) {
2988 //this is an ajax request, probably made by the module Vik Booking Channel Rates
2989 //we need to prepare some variables before calling the method.
2990 $start_date = date('Y-m-d', VikBooking::getDateTimestamp($pcheckin, 12, 0));
2991 $end_date = date('Y-m-d', VikBooking::getDateTimestamp($pcheckout, 10, 0));
2992 //set (only some) request variables (the rest is sent via Ajax)
2993 VikRequest::setVar('e4jauth', md5('vbo.e4j.vbo'));
2994 VikRequest::setVar('req_type', 'hotel_availability');
2995 VikRequest::setVar('start_date', $start_date);
2996 VikRequest::setVar('end_date', $end_date);
2997 //make call to get the result
2998 TACVBO::$getArray = true;
2999 $website_rates = TACVBO::tac_av_l([
3000 // always force adults and children to be injected as arguments to avoid request conflicts
3001 'adults' => VikRequest::getVar('adults', array()),
3002 'children' => VikRequest::getVar('children', array()) ?: [0],
3003 ]);
3004 //validate response
3005 if (!is_array($website_rates)) {
3006 //error returned
3007 echo json_encode(array('e4j.error' => $website_rates));
3008 exit;
3009 }
3010 if (is_array($website_rates) && isset($website_rates['e4j.error'])) {
3011 //another type of error returned
3012 echo json_encode($website_rates);
3013 exit;
3014 }
3015 if (is_array($website_rates) && !(count($website_rates) > 0)) {
3016 //empty response
3017 echo json_encode(array('e4j.error' => 'empty response'));
3018 exit;
3019 }
3020 //get the list of channels connected, filtered by ID
3021 $channels_map = VikBooking::getChannelsMap($pchannels_sel);
3022 //get the array with the lowest and preferred room rate
3023 $best_room_rate = VikBooking::getBestRoomRate($website_rates, $pdef_rplan);
3024 //get the charge/discount value for the OTAs rates from the Bulk Rates Cache of VCM
3025 $otas_rates_val = VikBooking::getOtasRatesVal($best_room_rate, true);
3026
3027 $otas_rmod = '';
3028 $otas_rmodpcent = 0;
3029 $otas_rmodval = 0;
3030 $otas_rmod_channels = array();
3031 if (!empty($otas_rates_val)) {
3032 if (is_array($otas_rates_val)) {
3033 $otas_rmod_channels = $otas_rates_val;
3034 $use_rates_val = $otas_rates_val[0];
3035 } else {
3036 // string
3037 $use_rates_val = $otas_rates_val;
3038 }
3039 $otas_rmod = substr($use_rates_val, 0, 1); // + or - (charge or discount)
3040 $otas_rmodpcent = substr($use_rates_val, -1) == '%' ? 1 : 0;
3041 $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)));
3042 }
3043 if (!count($best_room_rate)) {
3044 // nothing to parse
3045 echo json_encode(array('e4j.error' => 'no rates'));
3046 exit;
3047 }
3048 // build the response
3049 $final_cost = $pshow_tax > 0 ? ($best_room_rate['cost'] + $best_room_rate['taxes']) : $best_room_rate['cost'];
3050 $rates_resp = array(
3051 'website' => VikBooking::numberFormat($final_cost)
3052 );
3053 if (count($channels_map)) {
3054 $rates_resp['channels'] = array();
3055 }
3056 foreach ($channels_map as $ch) {
3057 $ch_final_cost = $final_cost;
3058
3059 /**
3060 * Check if an alteration for this channel has been specified.
3061 *
3062 * @since 1.15.0 (J) - 1.5.0 (WP)
3063 */
3064 $use_otas_rmod = $otas_rmod;
3065 $use_otas_rmodpcent = $otas_rmodpcent;
3066 $use_otas_rmodval = $otas_rmodval;
3067 if (is_array($otas_rmod_channels) && isset($otas_rmod_channels[$ch['id']])) {
3068 $use_rates_val = $otas_rmod_channels[$ch['id']];
3069 $use_otas_rmod = substr($use_rates_val, 0, 1); //+ or - (charge or discount)
3070 $use_otas_rmodpcent = substr($use_rates_val, -1) == '%' ? 1 : 0;
3071 $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)));
3072 }
3073
3074 if (!empty($use_otas_rmod)) {
3075 if ($use_otas_rmod == '+') {
3076 // charge
3077 if ($use_otas_rmodpcent > 0) {
3078 // percentage
3079 $ch_final_cost = $ch_final_cost * (100 + $use_otas_rmodval) / 100;
3080 } else {
3081 // absolute
3082 $ch_final_cost += $use_otas_rmodval * (!empty($best_room_rate['days']) ? $best_room_rate['days'] : 1);
3083 }
3084 } else {
3085 // discount (must be a fool)
3086 if ($use_otas_rmodpcent > 0) {
3087 // percentage
3088 $ch_final_cost = $ch_final_cost / (($use_otas_rmodval / 100) + 1);
3089 } else {
3090 // absolute
3091 $ch_final_cost -= $use_otas_rmodval * (!empty($best_room_rate['days']) ? $best_room_rate['days'] : 1);
3092 }
3093 }
3094 }
3095
3096 $rates_resp['channels'][$ch['id']] = VikBooking::numberFormat($ch_final_cost);
3097 }
3098 // output the response
3099 echo json_encode($rates_resp);
3100 exit;
3101 }
3102
3103 // proceed with the standard request (that will exit the process)
3104 TACVBO::tac_av_l();
3105 }
3106
3107 /**
3108 * Front-end authentication for the operators
3109 * through their authentication code.
3110 *
3111 * @since 1.11
3112 */
3113 public function operatorlogin()
3114 {
3115 /**
3116 * Secure the login form token.
3117 */
3118 if (!JSession::checkToken()) {
3119 // missing CSRF-proof token
3120 throw new Exception('The security token did not match.', 403);
3121 }
3122
3123 $app = JFactory::getApplication();
3124 $pauthcode = VikRequest::getString('authcode', '', 'request');
3125 $pitemid = VikRequest::getInt('Itemid', '', 'request');
3126 /**
3127 * We add "&auth=1" to the query string just to avoid caching for a redirect to the same login page URI.
3128 *
3129 * @since September 9th 2020
3130 */
3131 $goto = JRoute::rewrite('index.php?option=com_vikbooking&view=operators&auth=1'.(!empty($pitemid) ? '&Itemid='.$pitemid : ''), false);
3132
3133 if (empty($pauthcode) || !VikBooking::getOperatorInstance()->authOperator($pauthcode)) {
3134 // print warning message
3135 VikError::raiseWarning('', JText::translate('VBOOPERINVAUTHCODE'));
3136 }
3137
3138 $app->redirect($goto);
3139 }
3140
3141 /**
3142 * Front-end logout for the operators.
3143 *
3144 * @since 1.11
3145 */
3146 public function operatorlogout()
3147 {
3148 $app = JFactory::getApplication();
3149 $pitemid = VikRequest::getInt('Itemid', '', 'request');
3150 $goto = JRoute::rewrite('index.php?option=com_vikbooking&view=operators'.(!empty($pitemid) ? '&Itemid='.$pitemid : ''), false);
3151
3152 VikBooking::getOperatorInstance()->logoutOperator();
3153
3154 $app->redirect($goto);
3155 }
3156
3157 /**
3158 * Front-end Pre Check-in submit of the guests details.
3159 *
3160 * @since 1.12
3161 */
3162 public function storeprecheckin()
3163 {
3164 if (!JSession::checkToken()) {
3165 // missing CSRF-proof token
3166 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
3167 }
3168
3169 $dbo = JFactory::getDbo();
3170 $app = JFactory::getApplication();
3171
3172 $sid = $app->input->getString('sid', '');
3173 $ts = $app->input->getString('ts', '');
3174 $pguests = $app->input->get('guests', [], 'array');
3175 $pitemid = $app->input->getInt('Itemid', 0);
3176
3177 $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';";
3178 $dbo->setQuery($q);
3179 $order = $dbo->loadAssoc();
3180 if (!$order) {
3181 throw new Exception('Booking not found', 404);
3182 }
3183
3184 $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;";
3185 $dbo->setQuery($q);
3186 $orderrooms = $dbo->loadAssocList();
3187 if (!$orderrooms) {
3188 throw new Exception('No rooms found', 404);
3189 }
3190
3191 // access the customer record
3192 $customer = VikBooking::getCPinInstance()->getCustomerFromBooking($order['id']);
3193
3194 $q = "SELECT * FROM `#__vikbooking_customers_orders` WHERE `idorder`=".(int)$order['id'].";";
3195 $dbo->setQuery($q);
3196 $custorder = $dbo->loadAssoc();
3197 if (!$custorder) {
3198 throw new Exception('No customer found', 404);
3199 }
3200
3201 // booking details page
3202 $goto = JRoute::rewrite('index.php?option=com_vikbooking&view=booking&sid=' . $order['sid'] . '&ts=' . $order['ts'] . (!empty($pitemid) ? '&Itemid=' . $pitemid : ''), false);
3203 if (empty($order['sid']) && !empty($order['idorderota'])) {
3204 // booking details page for OTA bookings
3205 $goto = JRoute::rewrite('index.php?option=com_vikbooking&view=booking&sid=' . $order['idorderota'] . '&ts=' . $order['ts'] . (!empty($pitemid) ? '&Itemid=' . $pitemid : ''), false);
3206 }
3207
3208 // make sure pre-checkin is allowed
3209 $precheckin = VikBooking::precheckinEnabled();
3210 if ($precheckin) {
3211 // make sure the limit of days in advance is reflected
3212 $precheckin_mind = VikBooking::precheckinMinOffset();
3213 if ($precheckin_mind < 0) {
3214 // validation made prior to check-out date and time
3215 $precheckin = time() <= strtotime("{$precheckin_mind} days 23:59:59", $order['checkout']);
3216 } else {
3217 // classic validation prior to check-in date and time
3218 $precheckin_lim_ts = strtotime("+{$precheckin_mind} days 00:00:00");
3219 $precheckin = ($precheckin_lim_ts <= $order['checkin'] || ($precheckin_mind === 1 && time() <= $order['checkin']));
3220 }
3221 }
3222 if (!$precheckin) {
3223 // raise error and redirect in case of website or OTA booking
3224 VikError::raiseWarning('', 'Pre-checkin not allowed at this time');
3225 $app->redirect($goto);
3226 exit;
3227 }
3228
3229 // build guest details
3230 $guests_details = array();
3231
3232 // list of keys for the guests details collected via front-end
3233 $front_keys = array();
3234
3235 foreach ($pguests as $ind => $adults) {
3236 foreach ($adults as $aduind => $details) {
3237 foreach ($details as $detkey => $detval) {
3238 if (!in_array($detkey, $front_keys)) {
3239 // push the key of the guest details for later comparison
3240 array_push($front_keys, $detkey);
3241 }
3242 if (strlen($detval)) {
3243 // push value only if not empty
3244 if (!isset($guests_details[$ind])) {
3245 $guests_details[$ind] = array();
3246 }
3247 if (!isset($guests_details[$ind][$aduind])) {
3248 $guests_details[$ind][$aduind] = array();
3249 }
3250 $guests_details[$ind][$aduind][$detkey] = $detval;
3251 }
3252 }
3253 }
3254 }
3255
3256 /**
3257 * Compare the current data collected to the back-end pax_data in case there are some
3258 * fields dedicated to just the back-end for the admins (like extra_notes), and merge.
3259 */
3260 $curpaxdata = json_decode($custorder['pax_data'], true);
3261 if (is_array($curpaxdata) && count($curpaxdata)) {
3262 foreach ($curpaxdata as $ind => $adults) {
3263 if (!isset($guests_details[$ind])) {
3264 // current pax data include a room not present, set it to not lose it
3265 $guests_details[$ind] = $adults;
3266 }
3267 foreach ($adults as $aduind => $details) {
3268 if (!isset($guests_details[$ind][$aduind])) {
3269 // current pax data include a guest not present, set it to not lose it
3270 $guests_details[$ind][$aduind] = $details;
3271 }
3272 foreach ($details as $detkey => $detval) {
3273 if (!in_array($detkey, $front_keys)) {
3274 // merge this key probably reserved to the back-end
3275 $guests_details[$ind][$aduind][$detkey] = $detval;
3276 }
3277 }
3278 }
3279 }
3280 }
3281
3282 // update checkin information
3283 $q = "UPDATE `#__vikbooking_customers_orders` SET `pax_data`=".$dbo->quote(json_encode($guests_details))." WHERE `id`=".(int)$custorder['id'].";";
3284 $dbo->setQuery($q);
3285 $dbo->execute();
3286
3287 // Booking History
3288 VikBooking::getBookingHistoryInstance()->setBid($order['id'])->store('PC');
3289
3290 /**
3291 * Invoke the callback on the pax registration driver.
3292 *
3293 * @since 1.17.5 (J) - 1.7.5 (WP)
3294 */
3295 VBOCheckinPax::callbackPrecheckinDataStored(
3296 VBOFactory::getConfig()->getString('checkindata', 'basic'),
3297 (array) $guests_details,
3298 (array) $order,
3299 (array) $customer
3300 );
3301
3302 // print success message and redirect
3303 $app->enqueueMessage(JText::translate('VBOSUBMITPRECHECKINTNKS'));
3304 $app->redirect($goto);
3305 }
3306
3307 /**
3308 * Upsell extra services/options.
3309 *
3310 * @since 1.13 (J) - 1.3.0 (WP)
3311 */
3312 public function upsellextras()
3313 {
3314 $dbo = JFactory::getDbo();
3315 $app = JFactory::getApplication();
3316 $sid = VikRequest::getString('sid', '', 'request');
3317 $ts = VikRequest::getString('ts', '', 'request');
3318 $pitemid = VikRequest::getInt('Itemid', 0, 'request');
3319 $paddopt = VikRequest::getVar('addopt', array());
3320
3321 if (!$paddopt) {
3322 throw new Exception('No extra services selected', 404);
3323 }
3324
3325 // find the involved reservation, direct or OTA
3326 $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) . ";";
3327 $dbo->setQuery($q);
3328 $order = $dbo->loadAssoc();
3329 if (!$order) {
3330 throw new Exception('Booking not found', 404);
3331 }
3332
3333 // obtain the involved rooms (include room "params")
3334 $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;";
3335 $dbo->setQuery($q);
3336 $orderrooms = $dbo->loadAssocList();
3337 if (!$orderrooms) {
3338 throw new Exception('No rooms found', 404);
3339 }
3340
3341 // availability helper
3342 $av_helper = VikBooking::getAvailabilityInstance();
3343
3344 // room stay dates in case of split stay
3345 $room_stay_dates = [];
3346 if ($order['split_stay']) {
3347 if ($order['status'] == 'confirmed') {
3348 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($order['id']);
3349 } else {
3350 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $order['id'], []);
3351 }
3352 }
3353
3354 // load all valid and existing options as a security measure
3355 $alloptions = [];
3356 $q = "SELECT * FROM `#__vikbooking_optionals`;";
3357 $dbo->setQuery($q);
3358 $records = $dbo->loadAssocList();
3359 if (!$records) {
3360 throw new Exception('No options found', 404);
3361 }
3362 foreach ($records as $v) {
3363 $alloptions[$v['id']] = $v;
3364 }
3365
3366 /**
3367 * Custom check-in/out times due to late check-out/early check-in options or custom listing settings.
3368 *
3369 * @since 1.17.2 (J) - 1.7.2 (WP)
3370 * @since 1.18.3 (J) - 1.8.3 (WP) added support to check-in/out times at listing-level.
3371 */
3372 $custom_checkinout = [];
3373 if (count($orderrooms) === 1) {
3374 $listing_custom_checkin = VikBooking::getRoomParam('checkin', ($orderrooms[(key($orderrooms))]['room_params'] ?? ''));
3375 $listing_custom_checkout = VikBooking::getRoomParam('checkout', ($orderrooms[(key($orderrooms))]['room_params'] ?? ''));
3376 if ($listing_custom_checkin) {
3377 // set listing-level check-in time in seconds
3378 $listing_custom_checkin_parts = explode(':', $listing_custom_checkin);
3379 $listing_custom_checkin = (intval($listing_custom_checkin_parts[0]) * 3600) + (intval($listing_custom_checkin_parts[1]) * 60);
3380 }
3381 if ($listing_custom_checkout) {
3382 // set listing-level check-out time in seconds
3383 $listing_custom_checkout_parts = explode(':', $listing_custom_checkout);
3384 $listing_custom_checkout = (intval($listing_custom_checkout_parts[0]) * 3600) + (intval($listing_custom_checkout_parts[1]) * 60);
3385 }
3386 if ($listing_custom_checkin || $listing_custom_checkout) {
3387 // set listing-level check-in/out times in seconds
3388 $custom_checkinout = [
3389 (int) $listing_custom_checkin,
3390 (int) $listing_custom_checkout,
3391 ];
3392 }
3393 }
3394
3395 /**
3396 * Gather the damage deposit options to be paid separately.
3397 *
3398 * @since 1.18.6 (J) - 1.8.6 (WP)
3399 */
3400 $separate_dd_options = [];
3401
3402 // build the extras booked
3403 $extras_booked = [];
3404 foreach ($orderrooms as $kor => $or) {
3405 if (!isset($paddopt[$kor]) || !$paddopt[$kor]) {
3406 continue;
3407 }
3408
3409 // determine proper nights of stay
3410 $room_stay_nights = $order['days'];
3411 if ($order['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
3412 $room_stay_checkin = !empty($room_stay_dates[$kor]['checkin_ts']) ? $room_stay_dates[$kor]['checkin_ts'] : $room_stay_dates[$kor]['checkin'];
3413 $room_stay_checkout = !empty($room_stay_dates[$kor]['checkout_ts']) ? $room_stay_dates[$kor]['checkout_ts'] : $room_stay_dates[$kor]['checkout'];
3414 $room_stay_nights = $av_helper->countNightsOfStay($room_stay_checkin, $room_stay_checkout);
3415 }
3416
3417 $extraoptstr = '';
3418 foreach ($paddopt[$kor] as $optid => $quant) {
3419 if (strpos($or['optionals'], $optid . ':') === 0 || strpos($or['optionals'], ';' . $optid . ':') > 0) {
3420 // this option has already been booked, skip it
3421 continue;
3422 }
3423 if (!isset($alloptions[$optid])) {
3424 // this option ID does not exist, skip it
3425 continue;
3426 }
3427 $extraoptstr .= $optid . ':' . (int)$quant . ';';
3428
3429 // option params
3430 $opt_params = !empty($alloptions[$optid]['oparams']) ? (array) json_decode($alloptions[$optid]['oparams'], true) : [];
3431
3432 /**
3433 * Custom check-in/out times due to late check-out/early check-in options.
3434 *
3435 * @since 1.17.2 (J) - 1.7.2 (WP)
3436 */
3437 if (($opt_params['custom_checkinout'] ?? 0) && (($opt_params['set_checkin'] ?? 0) || ($opt_params['set_checkout'] ?? 0))) {
3438 $custom_checkinout = [
3439 ($opt_params['set_checkin'] ?? 0),
3440 ($opt_params['set_checkout'] ?? 0),
3441 ];
3442 }
3443
3444 /**
3445 * Check if this is a damage deposit with a separate payment window.
3446 *
3447 * @since 1.18.6 (J) - 1.8.6 (WP)
3448 */
3449 if (($opt_params['damagedep'] ?? 0) && !empty($opt_params['damagedep_settings']['paywhen'])) {
3450 // damage deposit option with separate payment defined
3451 $future_payable_dd = true;
3452 if (!empty($opt_params['damagedep_settings']['bmaxlos']) && ($order['days'] ?? 1) > $opt_params['damagedep_settings']['bmaxlos']) {
3453 // maximum nights of stay validation failed
3454 $future_payable_dd = false;
3455 } elseif (empty($opt_params['damagedep_settings']['payid'])) {
3456 // separate payment method ID validation failed
3457 $future_payable_dd = false;
3458 }
3459 if ($future_payable_dd === true) {
3460 // push damage deposit option ID to be paid separately
3461 $separate_dd_options[] = (int) $optid;
3462 }
3463 }
3464
3465 // push option booked
3466 $extras_booked[] = [
3467 'id' => $optid,
3468 'idroom' => $or['idroom'],
3469 'name' => $alloptions[$optid]['name'],
3470 'quant' => $quant,
3471 'room_cost' => (!empty($or['cust_cost']) ? $or['cust_cost'] : $or['room_cost']),
3472 'room_name' => $or['room_name'],
3473 'optcost' => $alloptions[$optid]['cost'],
3474 'adults' => $or['adults'],
3475 'children' => $or['children'],
3476 'nights' => $room_stay_nights,
3477 ];
3478 }
3479
3480 // update options for this room record
3481 $newoptstr = $or['optionals'] . $extraoptstr;
3482 $q = "UPDATE `#__vikbooking_ordersrooms` SET `optionals`=" . $dbo->quote($newoptstr) . " WHERE `id`={$or['id']};";
3483 $dbo->setQuery($q);
3484 $dbo->execute();
3485 }
3486
3487 // increase booking total amount and build event log for the history
3488 $currency = VikBooking::getCurrencySymb();
3489 $totrooms = count($orderrooms);
3490 $increase = 0;
3491 $future_dd = 0;
3492 $add_tax = 0;
3493 $extraslog = [];
3494 foreach ($extras_booked as $extra) {
3495 $o = $alloptions[(int)$extra['id']];
3496 if ((int)$o['pcentroom']) {
3497 // make sure we have a cost for the room, or we should skip this type of option for "incomplete" bookings
3498 if (empty($extra['room_cost'])) {
3499 continue;
3500 }
3501 $o['cost'] = ($extra['room_cost'] * $o['cost'] / 100);
3502 }
3503 $optcost = intval($o['perday']) == 1 ? ($o['cost'] * $extra['nights']) : $o['cost'];
3504 if (!empty($o['maxprice']) && $o['maxprice'] > 0 && $optcost > $o['maxprice']) {
3505 $optcost = $o['maxprice'];
3506 }
3507 if ($o['perperson'] == 1) {
3508 $optcost = $optcost * $extra['adults'];
3509 }
3510 $optcost *= $extra['quant'];
3511
3512 /**
3513 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
3514 *
3515 * @since 1.17.7 (J) - 1.7.7 (WP)
3516 */
3517 $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$optcost, &$o, $order, $extra]);
3518 if ($custom_calculation) {
3519 $optcost = (float) $custom_calculation[0];
3520 }
3521
3522 $floatoptprice = VikBooking::sayOptionalsPlusIva($optcost, $o['idiva']);
3523 $netoptprice = VikBooking::sayOptionalsMinusIva($optcost, $o['idiva']);
3524 $increase += $floatoptprice;
3525 $add_tax += $floatoptprice - $netoptprice;
3526 array_push($extraslog, ($totrooms > 1 ? $extra['room_name'] . ': ' : '') . $extra['name'] . ($extra['quant'] > 1 ? ' (x' . $extra['quant'] . ')' : '') . ' ' . $currency . ' ' . VikBooking::numberFormat($floatoptprice));
3527
3528 if (in_array((int) $extra['id'], $separate_dd_options)) {
3529 // this is a damage deposit option that will be paid separately
3530 $future_dd += $floatoptprice;
3531 }
3532 }
3533
3534 $newtotbooking = $order['total'] + $increase;
3535 $new_tot_taxes = $order['tot_taxes'] + $add_tax;
3536
3537 /**
3538 * Important: the 'paymcount' should be increased only if the status is
3539 * "confirmed" or no rooms may be occupied when receiving a payment.
3540 */
3541 $q = $dbo->getQuery(true)
3542 ->update($dbo->qn('#__vikbooking_orders'))
3543 ->set($dbo->qn('total') . ' = ' . $dbo->q($newtotbooking))
3544 ->set($dbo->qn('paymcount') . ' = ' . ($order['status'] == 'confirmed' && (int)$order['paymcount'] < 1 ? '1' : $order['paymcount']))
3545 ->set($dbo->qn('tot_taxes') . ' = ' . $dbo->q($new_tot_taxes))
3546 ->set($dbo->qn('payable') . ' = ' . $dbo->q(((float)$order['payable'] + $increase - $future_dd)))
3547 ->where($dbo->qn('id') . ' = ' . (int) $order['id']);
3548
3549 /**
3550 * Custom check-in/out times due to late check-out/early check-in options.
3551 *
3552 * @since 1.17.2 (J) - 1.7.2 (WP)
3553 */
3554 if ($custom_checkinout) {
3555 $checkin_info = getdate($order['checkin']);
3556 $checkout_info = getdate($order['checkout']);
3557 // overwrite check-in and/or check-out timestamp(s)
3558 if ($custom_checkinout[0] >= 3600) {
3559 // overwrite check-in timestamp
3560 $time_hours = floor($custom_checkinout[0] / 3600);
3561 $time_minutes = floor(($custom_checkinout[0] - ($time_hours * 3600)) / 60);
3562 $new_booking_checkin = mktime($time_hours, $time_minutes, 0, $checkin_info['mon'], $checkin_info['mday'], $checkin_info['year']);
3563 // update db record field
3564 $q->set($dbo->qn('checkin') . ' = ' . $dbo->q($new_booking_checkin));
3565 }
3566 if ($custom_checkinout[1] >= 3600) {
3567 // overwrite check-out timestamp
3568 $time_hours = floor($custom_checkinout[1] / 3600);
3569 $time_minutes = floor(($custom_checkinout[1] - ($time_hours * 3600)) / 60);
3570 $new_booking_checkout = mktime($time_hours, $time_minutes, 0, $checkout_info['mon'], $checkout_info['mday'], $checkout_info['year']);
3571 // update db record field
3572 $q->set($dbo->qn('checkout') . ' = ' . $dbo->q($new_booking_checkout));
3573 }
3574 }
3575
3576 // update booking record on db
3577 $dbo->setQuery($q);
3578 $dbo->execute();
3579
3580 // Booking History
3581 VikBooking::getBookingHistoryInstance($order['id'])->store('UE', implode("\n", $extraslog));
3582
3583 /**
3584 * Trigger event to allow third-party plugins to choose whether upselling notifications should be sent.
3585 *
3586 * @since 1.17.2 (J) - 1.7.2 (WP)
3587 */
3588 $send_notifications = true;
3589 $should_send = VBOFactory::getPlatform()->getDispatcher()->filter('onUpsellingReceivedShouldSendNotifications', [$order]);
3590 if (is_array($should_send) && in_array(false, $should_send, true)) {
3591 $send_notifications = false;
3592 }
3593
3594 if ($send_notifications) {
3595 // send email notification to guest and admin
3596 VikBooking::sendBookingEmail($order['id'], array('guest', 'admin'));
3597 }
3598
3599 $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);
3600 $app->enqueueMessage(JText::translate('VBOUPSELLRESULTOK'));
3601 $app->redirect($goto);
3602 $app->close();
3603 }
3604
3605 /**
3606 * Submits a new review.
3607 *
3608 * @since 1.3.0
3609 */
3610 public function sendreview()
3611 {
3612 $dbo = JFactory::getDbo();
3613 $app = JFactory::getApplication();
3614 $vbo_tn = VikBooking::getTranslator();
3615 $sid = VikRequest::getString('sid', '', 'request');
3616 $ts = VikRequest::getString('ts', '', 'request');
3617 $ratingmess = VikRequest::getString('ratingmess', '', 'request');
3618 $rating = VikRequest::getVar('rating', array(), 'request', 'array');
3619 $pitemid = VikRequest::getInt('Itemid', 0, 'request');
3620
3621 $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';";
3622 $dbo->setQuery($q);
3623 $dbo->execute();
3624 if (!$dbo->getNumRows()) {
3625 throw new Exception('Booking not found', 404);
3626 }
3627 $order = $dbo->loadAssoc();
3628
3629 // make sure a review can be left for this booking
3630 if (!VikBooking::canBookingBeReviewed($order)) {
3631 throw new Exception('Cannot leave a review at this time', 403);
3632 }
3633
3634 $orderrooms = array();
3635 $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;";
3636 $dbo->setQuery($q);
3637 $dbo->execute();
3638 if (!$dbo->getNumRows()) {
3639 throw new Exception('No rooms found', 404);
3640 }
3641 $orderrooms = $dbo->loadAssocList();
3642
3643 // get customer information
3644 $customer = VikBooking::getCPinIstance()->getCustomerFromBooking($order['id']);
3645
3646 // booking details page
3647 $goto = JRoute::rewrite('index.php?option=com_vikbooking&view=booking&sid=' . $order['sid'] . '&ts=' . $order['ts'] . (!empty($pitemid) ? '&Itemid=' . $pitemid : ''), false);
3648 if (empty($order['sid']) && !empty($order['idorderota'])) {
3649 // booking details page for OTA bookings
3650 $goto = JRoute::rewrite('index.php?option=com_vikbooking&view=booking&sid=' . $order['idorderota'] . '&ts=' . $order['ts'] . (!empty($pitemid) ? '&Itemid=' . $pitemid : ''), false);
3651 }
3652
3653 // reviews settings
3654 $gr_approval = VikBooking::guestReviewsApproval();
3655 $gr_type = VikBooking::guestReviewsType();
3656 $gr_services = VikBooking::guestReviewsServices();
3657 $rawservices = $gr_services;
3658 $vbo_tn->translateContents($gr_services, '#__vikbooking_greview_service');
3659
3660 // make sure all ratings are not empty
3661 if ($gr_type == 'global') {
3662 if (empty($rating[0]) || intval($rating[0]) < 1 || intval($rating[0]) > 5) {
3663 // no or invalid single-rating received
3664 VikError::raiseWarning('', 'Please rate your experience to leave a review.');
3665 $app->redirect($goto);
3666 exit;
3667 }
3668 } else {
3669 if (count($gr_services) != count($rating) || !count($rating)) {
3670 // something is missing
3671 VikError::raiseWarning('', 'Please rate your experience for all services');
3672 $app->redirect($goto);
3673 exit;
3674 }
3675 // make sure all ratings are valid
3676 foreach ($rating as $k => $score) {
3677 if (empty($score) || intval($score) < 1 || intval($score) > 5) {
3678 // no or invalid rating received for this service
3679 VikError::raiseWarning('', 'Please rate your experience to leave a review (missing ' . (isset($gr_services[$k]) ? $gr_services[$k]['service_name'] : '-----') . ').');
3680 $app->redirect($goto);
3681 exit;
3682 }
3683 }
3684 }
3685
3686 // average review score (in base 10) and services map
3687 $avg_score = 0;
3688 $serv_scores = array();
3689
3690 // gather the information
3691 foreach ($rating as $k => $score) {
3692 // rating in base 10
3693 $score = ((int)$score * 2);
3694 //
3695 $avg_score += $score;
3696 if ($gr_type == 'service' && isset($gr_services[$k]) && !empty($gr_services[$k]['service_name'])) {
3697 $skey = $gr_services[$k]['service_name'];
3698 $serv_scores[$skey] = $score;
3699 }
3700 }
3701 // this will be the review_score
3702 $avg_score = round(($avg_score / count($rating)), 2);
3703
3704 // build review content object
3705 $review_content = new stdClass;
3706
3707 // creation date
3708 $review_content->created_timestamp = date('Y-m-d H:i:s');
3709
3710 // scoring per service (if any)
3711 $review_content->scoring = new stdClass;
3712 if (count($serv_scores)) {
3713 $counter = 0;
3714 foreach ($serv_scores as $snametranx => $servscore) {
3715 // we build the object with the original names of the services, as they could have been translated
3716 $origskey = $rawservices[$counter]['service_name'];
3717 $review_content->scoring->{$origskey} = $servscore;
3718 $counter++;
3719 }
3720 }
3721 // scoring total value ("review_score" is a protected key) is added no matter of the review type (service/global)
3722 $review_content->scoring->review_score = $avg_score;
3723
3724 // reviewer information
3725 $review_content->reviewer = new stdClass;
3726 $customer_name = '';
3727 if ($customer) {
3728 $review_content->reviewer->name = $customer['first_name'];
3729 $review_content->reviewer->country_code = $customer['country'];
3730 $customer_name = $customer['first_name'] . ' ' . $customer['last_name'];
3731 } else {
3732 $revuname = '';
3733 if (!empty($order['custdata'])) {
3734 $uinfos = explode("\n", $order['custdata']);
3735 $first_info = explode(':', $uinfos[0]);
3736 if (count($first_info) > 1) {
3737 unset($first_info[0]);
3738 $revuname = implode(':', $first_info);
3739 } else {
3740 $revuname = trim($first_info[0]);
3741 }
3742 }
3743 $review_content->reviewer->name = $revuname;
3744 $review_content->reviewer->country_code = $order['country'];
3745 $customer_name = $revuname;
3746 }
3747
3748 // maximum 2000 chars for the message review to avoid spammers
3749 if (!empty($ratingmess) && strlen($ratingmess) > 2000) {
3750 $ratingmess = substr($ratingmess, 0, 2000);
3751 }
3752
3753 // review message
3754 $review_content->content = new stdClass;
3755 $review_content->content->message = !empty($ratingmess) ? $ratingmess : null;
3756
3757 // null reply
3758 $review_content->reply = null;
3759
3760 // check if multiple accounts to find the property name
3761 $property_name = null;
3762 $has_multiaccounts = false;
3763 $multi_map = array();
3764 $q = "SELECT * FROM `#__vikchannelmanager_roomsxref`;";
3765 $dbo->setQuery($q);
3766 $dbo->execute();
3767 if ($dbo->getNumRows()) {
3768 $xref_data = $dbo->loadAssocList();
3769 foreach ($xref_data as $xref) {
3770 if (empty($xref['prop_params'])) {
3771 continue;
3772 }
3773 if (!isset($multi_map[$xref['idchannel']])) {
3774 $multi_map[$xref['idchannel']] = array();
3775 }
3776 if (!isset($multi_map[$xref['idchannel']][$xref['prop_params']])) {
3777 $multi_map[$xref['idchannel']][$xref['prop_params']] = 0;
3778 }
3779 $multi_map[$xref['idchannel']][$xref['prop_params']]++;
3780 }
3781 foreach ($multi_map as $ch_id => $ch_params) {
3782 if (count($ch_params) > 1) {
3783 $has_multiaccounts = true;
3784 break;
3785 }
3786 }
3787 }
3788 if ($has_multiaccounts && (int)$order['roomsnum'] === 1) {
3789 // find the category name of the room booked (if any, and if one room booked)
3790 $q = "SELECT `idcat` FROM `#__vikbooking_rooms` WHERE `id`={$orderrooms[0]['idroom']};";
3791 $dbo->setQuery($q);
3792 $dbo->execute();
3793 if ($dbo->getNumRows()) {
3794 $allcats = $dbo->loadResult();
3795 if (!empty($allcats)) {
3796 $parts = explode(';', $allcats);
3797 if (count($parts) === 2) {
3798 // just one category, get the name of it
3799 $property_name = VikBooking::getCategoryName($parts[0]);
3800 }
3801 }
3802 }
3803 if (empty($property_name)) {
3804 // category not found, get the room name
3805 $property_name = $orderrooms[0]['name'];
3806 }
3807 }
3808
3809 // create record
3810 $review_record = new stdClass;
3811 $review_record->review_id = -1;
3812 $review_record->prop_first_param = null;
3813 $review_record->prop_name = $property_name;
3814 $review_record->channel = null;
3815 $review_record->uniquekey = 0;
3816 $review_record->idorder = $order['id'];
3817 $review_record->dt = JFactory::getDate()->toSql(true);
3818 $review_record->customer_name = $customer_name;
3819 $review_record->lang = JFactory::getLanguage()->getTag();
3820 $review_record->score = $avg_score;
3821 $review_record->country = $order['country'];
3822 $review_record->content = json_encode($review_content);
3823 $review_record->published = ($gr_approval == 'auto' ? 1 : 0);
3824
3825 // insert review
3826 if ($dbo->insertObject('#__vikchannelmanager_otareviews', $review_record, 'id')) {
3827 $app->enqueueMessage(JText::translate('VBOTHANKSREVIEWLEFT'));
3828 // Booking History
3829 VikBooking::getBookingHistoryInstance()->setBid($order['id'])->store('GR');
3830 } else {
3831 VikError::raiseWarning('', JText::translate('VBOREVIEWGENERROR'));
3832 }
3833
3834 // update global score for website and this property (if multiple accounts)
3835 $globscore_id = null;
3836 $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));
3837 $dbo->setQuery($q, 0, 1);
3838 $dbo->execute();
3839 if ($dbo->getNumRows()) {
3840 $globscore_id = $dbo->loadResult();
3841 }
3842 // select score for all reviews for the website and this account
3843 $services_scores = array();
3844 $services_revscount = array();
3845 $revs_count = 0;
3846 $super_tot = 0;
3847 $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)) . ";";
3848 $dbo->setQuery($q);
3849 $dbo->execute();
3850 if ($dbo->getNumRows()) {
3851 $all_scores = $dbo->loadAssocList();
3852 $revs_count += count($all_scores);
3853 foreach ($all_scores as $s) {
3854 $super_tot += $s['score'];
3855 // check if scores were given per service
3856 $s['content'] = json_decode($s['content'], true);
3857 if (isset($s['content']['scoring']) && count($s['content']['scoring']) > 1) {
3858 // review was left for services
3859 foreach ($s['content']['scoring'] as $sname => $sval) {
3860 if (!isset($services_scores[$sname])) {
3861 $services_scores[$sname] = 0;
3862 $services_revscount[$sname] = 0;
3863 }
3864 $services_scores[$sname] += $sval;
3865 $services_revscount[$sname]++;
3866 }
3867 }
3868 }
3869 }
3870 // global average score
3871 $revs_count = $revs_count > 0 ? $revs_count : 1;
3872 $glob_avg_score = ($super_tot / $revs_count);
3873 // services average score
3874 $glob_servs_avg_score = array();
3875 foreach ($services_scores as $sname => $val) {
3876 $services_revscount[$sname] = isset($services_revscount[$sname]) && $services_revscount[$sname] > 0 ? $services_revscount[$sname] : 1;
3877 $glob_servs_avg_score[$sname] = ($val / $services_revscount[$sname]);
3878 }
3879
3880 // build global score content
3881 $glob_score_content = new stdClass;
3882 $glob_score_content->review_score = new stdClass;
3883 $glob_score_content->review_score->score = $glob_avg_score;
3884 $glob_score_content->review_score->review_count = $revs_count;
3885 foreach ($glob_servs_avg_score as $sname => $val) {
3886 $glob_score_content->{$sname} = new stdClass;
3887 $glob_score_content->{$sname}->score = $val;
3888 $glob_score_content->{$sname}->review_count = $services_revscount[$sname];
3889 }
3890
3891 // build global score object
3892 $glob_score_obj = new stdClass;
3893 if (!empty($globscore_id)) {
3894 $glob_score_obj->id = $globscore_id;
3895 }
3896 $glob_score_obj->prop_first_param = null;
3897 $glob_score_obj->prop_name = $property_name;
3898 $glob_score_obj->channel = null;
3899 $glob_score_obj->uniquekey = 0;
3900 $glob_score_obj->last_updated = JFactory::getDate()->toSql(true);
3901 $glob_score_obj->score = round($glob_avg_score, 2);
3902 $glob_score_obj->content = json_encode($glob_score_content);
3903
3904 // update or create global score
3905 if (!empty($globscore_id)) {
3906 $dbo->updateObject('#__vikchannelmanager_otascores', $glob_score_obj, 'id');
3907 } else {
3908 $dbo->insertObject('#__vikchannelmanager_otascores', $glob_score_obj, 'id');
3909 }
3910
3911 // redirect to main view
3912 $app->redirect($goto);
3913 }
3914
3915 /**
3916 * AJAX task to get one monthly availability calendar of the room details page.
3917 *
3918 * @since 1.13.5
3919 */
3920 public function get_avcalendars_data()
3921 {
3922 $dbo = JFactory::getDbo();
3923 $rid = VikRequest::getInt('rid', 0, 'request');
3924 $direction = VikRequest::getString('direction', 'next', 'request');
3925 $fromdt = VikRequest::getString('fromdt', '', 'request');
3926 $nextdt = VikRequest::getString('nextdt', '', 'request');
3927 $prevdt = VikRequest::getString('prevdt', '', 'request');
3928
3929 // make sure vars are not empty
3930 if (empty($rid) || empty($direction) || empty($fromdt) || empty($nextdt) || empty($prevdt)) {
3931 /**
3932 * Search engines may follow this endpoint, so we have to exit with HTTP status code 200
3933 *
3934 * @since 1.16.0 (J) - 1.6.0 (WP)
3935 */
3936 VBOHttpDocument::getInstance()->json(['Invalid request variables']);
3937 }
3938
3939 // date format
3940 $vbo_df = VikBooking::getDateFormat();
3941 if ($vbo_df == "%d/%m/%Y") {
3942 $vbo_df = 'd/m/Y';
3943 } elseif ($vbo_df == "%m/%d/%Y") {
3944 $vbo_df = 'm/d/Y';
3945 } else {
3946 $vbo_df = 'Y/m/d';
3947 }
3948
3949 // configuration settings
3950 $numcalendars = VikBooking::numCalendars();
3951 $showpartlyres = VikBooking::showPartlyReserved();
3952 $showcheckinoutonly = VikBooking::showStatusCheckinoutOnly();
3953 $usepricecal = false;
3954 $inonout_allowed = true;
3955 $timeopst = VikBooking::getTimeOpenStore();
3956 if (is_array($timeopst)) {
3957 if ($timeopst[0] < $timeopst[1]) {
3958 // check-in not allowed on a day where there is already a check out (no arrivals/depatures on the same day)
3959 $inonout_allowed = false;
3960 }
3961 }
3962
3963 // week-days ordering
3964 $firstwday = (int)VikBooking::getFirstWeekDay();
3965 $days_labels = array(
3966 JText::translate('VBSUN'),
3967 JText::translate('VBMON'),
3968 JText::translate('VBTUE'),
3969 JText::translate('VBWED'),
3970 JText::translate('VBTHU'),
3971 JText::translate('VBFRI'),
3972 JText::translate('VBSAT')
3973 );
3974 $days_indexes = array();
3975 for ($i = 0; $i < 7; $i++) {
3976 $days_indexes[$i] = (6 - ($firstwday - $i) + 1) % 7;
3977 }
3978
3979 // first day timestamp of month to read in case of forward navigation
3980 $start_ts = strtotime($fromdt);
3981 $start_info = getdate($start_ts);
3982 if (!$start_ts || !$start_info) {
3983 VBOHttpDocument::getInstance()->close(500, 'Invalid date provided');
3984 }
3985 // backward navigation
3986 if ($direction == 'prev') {
3987 // we need to get the previous month
3988 $start_ts = mktime(0, 0, 0, ($start_info['mon'] - 1), 1, $start_info['year']);
3989 $start_info = getdate($start_ts);
3990 if (!$start_ts || !$start_info) {
3991 VBOHttpDocument::getInstance()->close(500, 'Invalid date calculated');
3992 }
3993 }
3994
3995 // make sure minimum date is respected
3996 $min_lim_ts = mktime(0, 0, 0, date('n'), 1, date('Y'));
3997 if ($start_ts < $min_lim_ts) {
3998 VBOHttpDocument::getInstance()->close(500, 'Dates in the past not allowed');
3999 }
4000
4001 // check the current next and prev dates to help the next AJAX navigations
4002 $nextnav_ts = strtotime($nextdt);
4003 $nextnav_info = getdate($nextnav_ts);
4004 if (!$nextnav_ts || !$nextnav_info) {
4005 VBOHttpDocument::getInstance()->close(500, 'Invalid next navigation date provided');
4006 }
4007 $prevnav_ts = strtotime($prevdt);
4008 $prevnav_info = getdate($prevnav_ts);
4009 if (!$prevnav_ts || !$prevnav_info) {
4010 VBOHttpDocument::getInstance()->close(500, 'Invalid prev navigation date provided');
4011 }
4012
4013 // make sure maximum date is respected
4014 $max_months_future = 12;
4015 $max_date_future = VikBooking::getMaxDateFuture($rid);
4016 if (!empty($max_date_future)) {
4017 $numlim = (int)substr($max_date_future, 1, (strlen($max_date_future) - 2));
4018 $numlim = $numlim < 1 ? 1 : $numlim;
4019 $quantlim = substr($max_date_future, -1, 1);
4020 if ($quantlim == 'm' || $quantlim == 'y') {
4021 $max_months_future = $numlim * ($quantlim == 'm' ? 1 : 12);
4022 $max_ts_future = strtotime("+{$max_months_future} months");
4023 $max_info = getdate($max_ts_future);
4024 $max_endts_future = mktime(23, 59, 59, $max_info['mon'], date('t', $max_info[0]), $max_info['year']);
4025 if ($start_ts > $max_endts_future) {
4026 VBOHttpDocument::getInstance()->close(500, 'Maximum date in the future exceeded');
4027 }
4028 }
4029 }
4030
4031 // get global property closing dates
4032 $cal_closing_dates = VikBooking::parseJsClosingDates();
4033 if (count($cal_closing_dates)) {
4034 foreach ($cal_closing_dates as $ccdk => $ccdv) {
4035 if (!(count($ccdv) == 2)) {
4036 continue;
4037 }
4038 $cal_closing_dates[$ccdk][0] = strtotime($ccdv[0]);
4039 $cal_closing_dates[$ccdk][1] = strtotime($ccdv[1]);
4040 }
4041 }
4042
4043 // load room details
4044 $q = "SELECT * FROM `#__vikbooking_rooms` WHERE `id`={$rid}";
4045 $dbo->setQuery($q, 0, 1);
4046 $dbo->execute();
4047 if (!$dbo->getNumRows()) {
4048 VBOHttpDocument::getInstance()->close(404, 'Room not found');
4049 }
4050 $room_details = $dbo->loadAssoc();
4051
4052 // get the future busy records
4053 $today_ts = mktime(0, 0, 0, date('n'), date('j'), date('Y'));
4054 $previousdayclass = '';
4055
4056 $q = "SELECT * FROM `#__vikbooking_busy` WHERE `idroom`={$room_details['id']} AND `checkout`>={$start_ts};";
4057 $dbo->setQuery($q);
4058 $busy = $dbo->loadAssocList();
4059
4060 // empty day element
4061 $empty_elem = new stdClass;
4062 $empty_elem->type = 'placeholder';
4063 $empty_elem->cont = '&nbsp;';
4064
4065 // build response container
4066 $calendars = array();
4067
4068 // prepare calendar object
4069 $calendar = new stdClass;
4070 $calendar->ts = $start_info[0];
4071 $calendar->mon = $start_info['mon'];
4072 $calendar->mday = $start_info['mday'];
4073 $calendar->year = $start_info['year'];
4074 $calendar->month = VikBooking::sayMonth($start_info['mon']);
4075 $calendar->wdays = array();
4076 for ($i = 0; $i < 7; $i++) {
4077 $d_ind = ($i + $firstwday) < 7 ? ($i + $firstwday) : ($i + $firstwday - 7);
4078 array_push($calendar->wdays, $days_labels[$d_ind]);
4079 }
4080
4081 // build the calendar rows by looping over the days of this month
4082 $calendar->rows = array();
4083
4084 // the row will contain all the placeholders and real days (7 elements at most)
4085 $row = array();
4086 $d_count = 0;
4087
4088 // first, we push empty dates placeholders for printing the first table cells (cells before the 1st of the month)
4089 for ($i = 0, $n = $days_indexes[$start_info['wday']]; $i < $n; $i++, $d_count++) {
4090 // push fake-day element (placeholder)
4091 array_push($row, $empty_elem);
4092 }
4093
4094 // start looping
4095 $loop_end_month = $start_info['mon'];
4096 while ($start_info['mon'] == $loop_end_month) {
4097 if ($d_count > 6) {
4098 // push the current row
4099 array_push($calendar->rows, $row);
4100
4101 // start a new row and reset cells counter
4102 $row = array();
4103 $d_count = 0;
4104 }
4105
4106 // build real-day element
4107 $elem = new stdClass;
4108 $elem->type = 'day';
4109 $elem->cont = $start_info['mday'] < 10 ? "0{$start_info['mday']}" : $start_info['mday'];
4110 $elem->dt = date($vbo_df, $start_info[0]);
4111 $elem->ymd = date('Y-m-d', $start_info[0]);
4112 $elem->ts = $start_info[0];
4113 $elem->class = 'vbtdfree';
4114 $elem->past_class = $start_info[0] < $today_ts ? ' vbtdpast' : '';
4115
4116 // check whether this day has got bookings
4117 $totfound = 0;
4118 $ischeckinday = false;
4119 $ischeckoutday = false;
4120 foreach ($busy as $b) {
4121 $info_in = getdate($b['checkin']);
4122 $checkin_ts = mktime(0, 0, 0, $info_in['mon'], $info_in['mday'], $info_in['year']);
4123 $info_out = getdate($b['checkout']);
4124 $checkout_ts = mktime(0, 0, 0, $info_out['mon'], $info_out['mday'], $info_out['year']);
4125 if ($start_info[0] >= $checkin_ts && $start_info[0] == $checkout_ts) {
4126 $ischeckoutday = true;
4127 }
4128 if ($start_info[0] >= $checkin_ts && $start_info[0] < $checkout_ts) {
4129 $totfound++;
4130 if ($start_info[0] == $checkin_ts) {
4131 $ischeckinday = true;
4132 }
4133 }
4134 }
4135 if ($totfound >= $room_details['units']) {
4136 $elem->class = "vbtdbusy";
4137 if ($ischeckinday && $showcheckinoutonly && !$usepricecal && $inonout_allowed && $previousdayclass != "vbtdbusy" && $previousdayclass != "vbtdbusy vbtdbusyforcheckin") {
4138 $elem->class = "vbtdbusy vbtdbusyforcheckin";
4139 } elseif ($ischeckinday && !$usepricecal && !$inonout_allowed && $previousdayclass != "vbtdbusy" && $previousdayclass != "vbtdbusy vbtdbusyforcheckin") {
4140 // check-out not allowed on a day where someone is already checking-in
4141 $elem->class = "vbtdbusy";
4142 }
4143 } elseif ($totfound > 0) {
4144 if ($showpartlyres) {
4145 $elem->class = "vbtdwarning";
4146 }
4147 } else {
4148 if ($ischeckoutday && !$usepricecal && $showcheckinoutonly && $inonout_allowed && !($room_details['units'] > 1)) {
4149 $elem->class = "vbtdbusy vbtdbusyforcheckout";
4150 } elseif ($ischeckoutday && !$usepricecal && !$inonout_allowed && !($room_details['units'] > 1)) {
4151 $elem->class = "vbtdbusy";
4152 }
4153 }
4154
4155 // check global closing dates
4156 if (count($cal_closing_dates)) {
4157 foreach ($cal_closing_dates as $closed_interval) {
4158 if ($start_info[0] >= $closed_interval[0] && $start_info[0] <= $closed_interval[1]) {
4159 $elem->class = "vbtdbusy";
4160 break;
4161 }
4162 }
4163 }
4164
4165 // push cell element and increase counter
4166 array_push($row, $elem);
4167 $d_count++;
4168
4169 // update previous day class
4170 $previousdayclass = $elem->class;
4171
4172 // go to next day
4173 $start_info = getdate(mktime(0, 0, 0, $start_info['mon'], ($start_info['mday'] + 1), $start_info['year']));
4174 }
4175
4176 if (count($row)) {
4177 // the last row still need to be pushed in the rows
4178 for ($i = $d_count; $i <= 6; $i++) {
4179 // fill last empty days
4180 array_push($row, $empty_elem);
4181 }
4182
4183 // push ending row
4184 array_push($calendar->rows, $row);
4185 }
4186
4187 // push this month's calendar object
4188 array_push($calendars, $calendar);
4189
4190 // check whether next request can navigate forward
4191 $can_nav_next = false;
4192 if ($direction == 'prev') {
4193 // we went prev, so we got to be able to go next
4194 $can_nav_next = true;
4195 } elseif ($direction == 'next' && strtotime("+{$max_months_future} months") > $start_info[0]) {
4196 // went next and max future date is still greater than next hypothetical month
4197 $can_nav_next = true;
4198 }
4199
4200 // check whether next request can navigate backward
4201 $can_nav_prev = false;
4202 if ($direction == 'next') {
4203 // we went next, so we got to be able to go prev
4204 $can_nav_prev = true;
4205 } elseif ($direction == 'prev' && $min_lim_ts < mktime(0, 0, 0, ($start_info['mon'] - 1), 1, $start_info['year'])) {
4206 // went prev and first day of today's month is less than (not equal to) the last month just rendered
4207 $can_nav_prev = true;
4208 }
4209
4210 // calculate next and prev dates for the next navigations to help the AJAX request
4211 $next_ymd = null;
4212 if ($can_nav_next) {
4213 if ($direction == 'next') {
4214 // next forward navigation will start from the ne month
4215 $next_ymd = date('Y-m-d', $start_info[0]);
4216 } else {
4217 // we add the total number of calendars to the month after the one lastly displayed
4218 $next_ymd = date('Y-m-d', strtotime("+" . ($numcalendars - 1) . " months", $start_info[0]));
4219 }
4220 }
4221 $prev_ymd = null;
4222 if ($can_nav_prev) {
4223 if ($direction == 'prev') {
4224 // next backward navigation will start from the last month we just displayed
4225 $prev_ymd = date('Y-m-d', mktime(0, 0, 0, ($start_info['mon'] - 1), 1, $start_info['year']));
4226 } else {
4227 // we add the total number of calendars to the month after the one lastly displayed
4228 $prev_ymd = date('Y-m-d', strtotime("-{$numcalendars} months", $start_info[0]));
4229 }
4230 }
4231
4232 // build response object
4233 $response = new stdClass;
4234 $response->calendars = $calendars;
4235 $response->can_nav_next = $can_nav_next;
4236 $response->can_nav_prev = $can_nav_prev;
4237 $response->next_ymd = $next_ymd;
4238 $response->prev_ymd = $prev_ymd;
4239
4240 echo json_encode($response);
4241 exit;
4242 }
4243
4244 /**
4245 * AJAX request for adding a new room-day note from the front-end tableaux.
4246 *
4247 * @return void
4248 *
4249 * @since 1.13.5
4250 */
4251 public function add_roomdaynote()
4252 {
4253 if (!JSession::checkToken()) {
4254 // missing CSRF-proof token
4255 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
4256 }
4257
4258 $dt = VikRequest::getString('dt', '', 'request');
4259 $idroom = VikRequest::getInt('idroom', 0, 'request');
4260 $subunit = VikRequest::getInt('subunit', 0, 'request');
4261 $type = VikRequest::getString('type', '', 'request');
4262 $type = empty($type) ? 'custom' : $type;
4263 $name = VikRequest::getString('name', '', 'request');
4264 $descr = VikRequest::getString('descr', '', 'request');
4265 $cdays = VikRequest::getInt('cdays', 0, 'request');
4266 $cdays = $cdays < 0 ? 0 : $cdays;
4267 $cdays = $cdays > 365 ? 365 : $cdays;
4268 if (empty($idroom) || empty($dt) || !strtotime($dt)) {
4269 echo 'e4j.error.1';
4270 exit;
4271 }
4272
4273 // we put the operator name in the description (if available)
4274 $operator = VikBooking::getOperatorInstance()->getOperatorAccount();
4275 if ($operator !== false) {
4276 $oper_signature = "({$operator['first_name']} {$operator['last_name']})";
4277 $descr = empty($descr) ? $oper_signature : $descr . " \n" . $oper_signature;
4278 }
4279
4280 // reload end date
4281 $end_date = $dt;
4282
4283 // build critical date object
4284 $new_note = array(
4285 'name' => $name,
4286 'type' => $type,
4287 'descr' => $descr,
4288 );
4289
4290 // get object
4291 $notes = VikBooking::getCriticalDatesInstance();
4292
4293 // store the notes for all consecutive dates
4294 for ($i = 0; $i <= $cdays; $i++) {
4295 $store_dt = $dt;
4296 if ($i > 0) {
4297 $dt_info = getdate(strtotime($store_dt));
4298 $store_dt = date('Y-m-d', mktime(0, 0, 0, $dt_info['mon'], ($dt_info['mday'] + $i), $dt_info['year']));
4299 $end_date = $store_dt;
4300 }
4301 $result = $notes->storeDayNote($new_note, $store_dt, $idroom, $subunit);
4302 if (!$result) {
4303 echo 'e4j.error.2';
4304 exit;
4305 }
4306 }
4307
4308 // reload all room day notes for this day for the AJAX response
4309 $all_notes = $notes->loadRoomDayNotes($dt, $end_date, $idroom, $subunit);
4310
4311 if (!$all_notes || !count($all_notes)) {
4312 // no notes found even after storing it
4313 echo 'e4j.error.3';
4314 exit;
4315 }
4316
4317 echo json_encode($all_notes);
4318 exit;
4319 }
4320
4321 /**
4322 * AJAX endpoint to upload customer documents during the pre-checkin.
4323 *
4324 * @since 1.14 (J) - 1.4.0 (WP)
4325 * @since 1.18.6 (J) - 1.8.6 (WP) added support for MRZ detection through Channel Manager.
4326 */
4327 public function precheckin_upload_docs()
4328 {
4329 $app = JFactory::getApplication();
4330 $dbo = JFactory::getDbo();
4331 $input = $app->input;
4332
4333 if (!JSession::checkToken()) {
4334 // missing CSRF-proof token
4335 VBOHttpDocument::getInstance($app)->close(403, JText::translate('JINVALID_TOKEN'));
4336 }
4337
4338 // gather request values
4339 $order_sid = $input->getString('sid', '');
4340 $order_ts = $input->getString('ts', '');
4341 $use_mrz = $input->getBool('mrz', false);
4342 /**
4343 * This is a simple file uploading process, but if
4344 * we wanted to automatically update the pax_data,
4345 * the request vars below would be necessary.
4346 */
4347 $room_index = $input->getInt('room_index', 0);
4348 $guest_index = $input->getInt('guest_index', 0);
4349 $pax_index = $input->getString('pax_index', '');
4350
4351 if (empty($order_sid) || empty($order_ts)) {
4352 VBOHttpDocument::getInstance($app)->close(404, 'Missing booking details');
4353 }
4354
4355 $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';";
4356 $dbo->setQuery($q);
4357 $order = $dbo->loadAssoc();
4358 if (!$order) {
4359 VBOHttpDocument::getInstance($app)->close(404, 'Booking not found');
4360 }
4361
4362 $customer = array();
4363 $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'].";";
4364 $dbo->setQuery($q);
4365 $customer = $dbo->loadObject();
4366 if (!$customer) {
4367 // one customer must be assigned to this booking for the pre-checkin
4368 VBOHttpDocument::getInstance($app)->close(404, 'No customers associated to this booking');
4369 }
4370
4371 // make sure pre-checkin is allowed
4372 $precheckin = VikBooking::precheckinEnabled();
4373 if ($precheckin) {
4374 // make sure the limit of days in advance is reflected
4375 $precheckin_mind = VikBooking::precheckinMinOffset();
4376 if ($precheckin_mind < 0) {
4377 // validation made prior to check-out date and time
4378 $precheckin = time() <= strtotime("{$precheckin_mind} days 23:59:59", $order['checkout']);
4379 } else {
4380 // classic validation prior to check-in date and time
4381 $precheckin_lim_ts = strtotime("+{$precheckin_mind} days 00:00:00");
4382 $precheckin = ($precheckin_lim_ts <= $order['checkin'] || ($precheckin_mind === 1 && time() <= $order['checkin']));
4383 }
4384 }
4385 if (!$precheckin) {
4386 VBOHttpDocument::getInstance($app)->close(403, 'Pre-checkin not allowed at this time');
4387 }
4388
4389 // get uploaded files array (use "raw" to avoid filtering the file to upload)
4390 $files = $input->files->get('docs', array(), 'raw');
4391 if (!$files) {
4392 VBOHttpDocument::getInstance($app)->close(500, 'No files to be uploaded');
4393 }
4394
4395 if (isset($files['name'])) {
4396 // we have a single associative array, we need to push it within a list,
4397 // because the upload iterates the $files array
4398 $files = [$files];
4399 }
4400
4401 // fetch documents folder path
4402 $dirpath = VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR;
4403
4404 // check if we have a valid directory
4405 if (empty($customer->docsfolder) || !is_dir($dirpath . $customer->docsfolder)) {
4406 // randomize string
4407 $customer->seed = uniqid();
4408
4409 // create blocks for hashed folder
4410 $parts = [
4411 $customer->first_name,
4412 $customer->last_name,
4413 md5(serialize($customer)),
4414 ];
4415
4416 // join fetched parts
4417 $customer->docsfolder = strtolower(implode('-', array_filter($parts)));
4418
4419 if (strlen($customer->docsfolder) < 16) {
4420 VBOHttpDocument::getInstance($app)->close(400, 'Possible security breach. Please specify as many details as possible.');
4421 }
4422
4423 // create a folder for this customer
4424 $created = JFolder::create($dirpath . $customer->docsfolder);
4425
4426 if (!$created) {
4427 VBOHttpDocument::getInstance($app)->close(403, sprintf('Unable to create the folder [%s]', $dirpath . $customer->docsfolder));
4428 }
4429
4430 unset($customer->seed);
4431
4432 // update customer docs folder
4433 $record = new stdClass;
4434 $record->id = $customer->id;
4435 $record->docsfolder = $customer->docsfolder;
4436 $dbo->updateObject('#__vikbooking_customers', $record, 'id');
4437 }
4438
4439 // prepare the response with the uploaded-file objects
4440 $response = [
4441 'uploads' => [],
4442 ];
4443 $upload_err = null;
4444
4445 // compose prefix for all files uploaded (must end with an underscrore for View's compatibility)
4446 $file_prefix = str_replace(' ', '-', JText::translate('VBOPRECHECKIN')) . '_';
4447
4448 try {
4449
4450 foreach ($files as $file) {
4451 // sanitize file name
4452 if (!empty($file['name'])) {
4453 // replace quotes and pipe, which is the separator in pax_data
4454 $file['name'] = str_replace(array("'", '"', '|'), '', $file['name']);
4455 }
4456 if (empty($file['name'])) {
4457 continue;
4458 }
4459 // always prepend "pre check-in" to the original file name
4460 $file['name'] = strtolower($file_prefix . $file['name']);
4461 // try to upload the file
4462 $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');
4463 // set a valid URL for the uploaded file
4464 $result->url = str_replace(DIRECTORY_SEPARATOR, '/', str_replace(VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR, VBO_CUSTOMERS_URI, $result->path));
4465 // push uploaded file
4466 array_push($response['uploads'], $result);
4467 }
4468
4469 } catch (Exception $e) {
4470 // do nothing, but catch the error
4471 $upload_err = $e;
4472 }
4473
4474 if (!$response['uploads']) {
4475 // raise an error
4476 if ($upload_err instanceof Exception) {
4477 VBOHttpDocument::getInstance($app)->close($upload_err->getCode() ?: 500, $upload_err->getMessage());
4478 }
4479 VBOHttpDocument::getInstance($app)->close(500, 'No files could actually be uploaded');
4480 }
4481
4482 if ($use_mrz) {
4483 // file upload completed, detect MRZ codes from uploaded documents
4484 $mrzImageUrls = array_values(array_filter(array_map(function($uploaded) {
4485 return $uploaded->url ?? '';
4486 }, $response['uploads'])));
4487
4488 // set response properties for MRZ detection result
4489 $response['mrz'] = [
4490 'data' => null,
4491 'raw' => null,
4492 'verified' => null,
4493 'error' => null,
4494 ];
4495
4496 try {
4497 // let the AI model service analyse the uploaded documents
4498 $mrzResult = (new VCMAiModelService)->mrzDetection($mrzImageUrls);
4499
4500 // let the current pax-fields data collector parse the extracted MRZ data
4501 $mrzMappedFields = VBOCheckinPax::getInstance()
4502 ->getMRZMapper()
4503 ->setVerified((bool) ($mrzResult->valid ?? false))
4504 ->mapDetectedProperties((array) ($mrzResult->data ?? null))
4505 ->getMappedFields();
4506
4507 // set MRZ validation result within the response
4508 $response['mrz']['data'] = $mrzMappedFields;
4509 $response['mrz']['raw'] = ($mrzResult->data ?? null);
4510 $response['mrz']['verified'] = (bool) $mrzResult->valid ?? false;
4511 } catch (Exception $e) {
4512 // catch the error message
4513 $response['mrz']['error'] = $e->getMessage();
4514 } catch (Throwable $e) {
4515 // catch the PHP error
4516 $response['mrz']['error'] = sprintf('Fatal error caught: %s', $e->getMessage());
4517 }
4518 }
4519
4520 // send response to output
4521 VBOHttpDocument::getInstance($app)->json($response);
4522 }
4523
4524 /**
4525 * AJAX endpoint to submit an inquiry/information request.
4526 *
4527 * @since 1.15.0 (J) - 1.5.0 (WP)
4528 */
4529 public function submit_inquiry()
4530 {
4531 if (!JSession::checkToken()) {
4532 // missing CSRF-proof token
4533 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
4534 }
4535
4536 $dbo = JFactory::getDbo();
4537 $app = JFactory::getApplication();
4538 $input = $app->input;
4539
4540 // get request values
4541 $checkin_dt = $input->getString('checkindate', '');
4542 $checkin_h = $input->getInt('checkinh', 0);
4543 $checkin_m = $input->getInt('checkinm', 0);
4544 $checkout_dt = $input->getString('checkoutdate', '');
4545 $checkout_h = $input->getInt('checkouth', 0);
4546 $checkout_m = $input->getInt('checkoutm', 0);
4547 $categories = $input->getString('categories', '');
4548 $roomsnum = $input->getInt('roomsnum', 1);
4549 $adults = $input->get('adults', array(), 'int');
4550 $children = $input->get('children', array(), 'int');
4551 $inquiry = $input->get('inquiry', array(), 'raw');
4552 $ulang = $input->getString('ulang', '');
4553
4554 $timeopst = VikBooking::getTimeOpenStore();
4555 if (empty($checkin_h) && empty($checkout_h) && is_array($timeopst)) {
4556 $opent = VikBooking::getHoursMinutes($timeopst[0]);
4557 $closet = VikBooking::getHoursMinutes($timeopst[1]);
4558 $checkin_h = $opent[0];
4559 $checkin_m = $opent[1];
4560 $checkout_h = $closet[0];
4561 $checkout_m = $closet[1];
4562 }
4563
4564 // compose the stay dates
4565 $checkin_ts = VikBooking::getDateTimestamp($checkin_dt, $checkin_h, $checkin_m);
4566 $checkout_ts = VikBooking::getDateTimestamp($checkout_dt, $checkout_h, $checkout_m);
4567
4568 if (empty($checkin_dt) || empty($checkout_dt) || empty($checkin_ts) || empty($checkout_ts)) {
4569 // invalid dates
4570 VBOHttpDocument::getInstance()->close(400, JText::translate('VBINVALIDDATES'));
4571 }
4572
4573 // validate guests
4574 if (!is_array($adults) || !count($adults)) {
4575 // invalid adults
4576 VBOHttpDocument::getInstance()->close(400, JText::translate('VBINCONGRDATA'));
4577 }
4578
4579 // prepare customer information
4580 $res_custdata = array();
4581 $t_first_name = '';
4582 $t_last_name = '';
4583 $guest_email = '';
4584 $guest_phone = '';
4585 $guest_country = '';
4586 $guest_extras = array();
4587 $guest_custom = array();
4588 foreach ($inquiry as $info_type => $info_vals) {
4589 if (empty($info_type) || $info_type == 'checkbox') {
4590 // we ignore any checkbox information
4591 continue;
4592 }
4593 foreach ($info_vals as $info_val) {
4594 if (!is_scalar($info_val) || !strlen($info_val)) {
4595 // empty or invalid field
4596 continue;
4597 }
4598 if ($info_type == 'nominative') {
4599 if (empty($t_first_name)) {
4600 $t_first_name = $info_val;
4601 $res_custdata[JText::translate('VBNAME')] = $info_val;
4602 } else {
4603 $t_last_name = $info_val;
4604 $res_custdata[JText::translate('VBLNAME')] = $info_val;
4605 }
4606 } elseif ($info_type == 'email') {
4607 $guest_email = $info_val;
4608 $res_custdata[JText::translate('ORDER_EMAIL')] = $info_val;
4609 } elseif ($info_type == 'phone') {
4610 $guest_phone = $info_val;
4611 $res_custdata[JText::translate('ORDER_PHONE')] = $info_val;
4612 } elseif ($info_type == 'country') {
4613 $guest_country = $info_val;
4614 $res_custdata[JText::translate('ORDER_STATE')] = $info_val;
4615 } elseif ($info_type == 'city') {
4616 $guest_extras['city'] = $info_val;
4617 $res_custdata[JText::translate('ORDER_CITY')] = $info_val;
4618 } else {
4619 // we treat this as a custom data field
4620 $guest_custom[$info_type] = $info_val;
4621 // inject reservation custdata string for this value
4622 if ($info_type == 'special_requests') {
4623 $res_custdata[JText::translate('ORDER_SPREQUESTS')] = $info_val;
4624 } else {
4625 $readable_ftype = ucwords(str_replace(array('_', '-'), ' ', $info_type));
4626 $res_custdata[$readable_ftype] = $info_val;
4627 }
4628 }
4629 }
4630 }
4631
4632 // validate fields
4633 if (!count($res_custdata)) {
4634 // we received no filled information
4635 VBOHttpDocument::getInstance()->close(400, JText::translate('VBINCONGRDATA'));
4636 }
4637
4638 // build the reservation raw-text for the customer data
4639 $res_custdata_str = VikBooking::buildCustData($res_custdata, "\n");
4640
4641 // store the customer record as first thing
4642 $cpin = VikBooking::getCPinIstance();
4643 $cpin->setCustomerExtraInfo($guest_extras);
4644 $cpin->saveCustomerDetails($t_first_name, $t_last_name, $guest_email, $guest_phone, $guest_country, array());
4645
4646 // build customer object for the inquiry reservation
4647 $customer = new stdClass;
4648 $customer->name = $t_first_name;
4649 $customer->lname = $t_last_name;
4650 $customer->email = $guest_email;
4651 $customer->phone = $guest_phone;
4652 $customer->country = $guest_country;
4653 $customer->lang = $ulang;
4654 $customer->custdata = $res_custdata_str;
4655 $customer->adminnotes = isset($guest_custom['special_requests']) ? $guest_custom['special_requests'] : '';
4656 /**
4657 * Always append the originally selected stay dates and guest party to the
4658 * administrator notes string so that they can be accessed all the times.
4659 */
4660 $customer->adminnotes .= !empty($customer->adminnotes) ? "\n" : '';
4661 $customer->adminnotes .= JText::translate('VBPICKUP') . ': ' . $checkin_dt . "\n";
4662 $customer->adminnotes .= JText::translate('VBRETURN') . ': ' . $checkout_dt . "\n";
4663 $customer->adminnotes .= JText::translate('VBFORMADULTS') . ': ' . array_sum($adults) . "\n";
4664 $customer->adminnotes .= JText::translate('VBFORMCHILDREN') . ': ' . array_sum($children) . "\n";
4665
4666 // prepare response object
4667 $response = new stdClass;
4668 $response->status = 0;
4669 $response->error = '';
4670
4671 // invoke availability helper class
4672 $av_helper = VikBooking::getAvailabilityInstance();
4673
4674 // turn flag on to ignore restrictions, as this is an inquiry and we must allocate the booking
4675 $av_helper->ignoreRestrictions(true);
4676
4677 // increase the default number of back and forth days for alternative date suggestions
4678 $av_helper->setBackForthDays(90);
4679
4680 // set stay dates
4681 $av_helper->setStayDates($checkin_dt, $checkout_dt);
4682
4683 // set room parties, but we expect to always have one room for the inquiry
4684 foreach ($adults as $k => $num_adults) {
4685 $num_children = isset($children[$k]) ? $children[$k] : 0;
4686 $av_helper->setRoomParty($num_adults, $num_children);
4687 }
4688
4689 // load available room rates
4690 $room_rates = $av_helper->getRates();
4691
4692 // check if availability errors occurred
4693 $has_av_error = strlen($av_helper->getError());
4694 $av_error_code = $av_helper->getErrorCode();
4695
4696 // count total fitting records
4697 $tot_records = is_array($room_rates) && !$has_av_error ? count($room_rates) : 0;
4698
4699 // build history extra data base object
4700 $ymd_stay_dates = $av_helper->getStayDates();
4701 $hist_extra_data = new stdClass;
4702 $hist_extra_data->checkin_date = $ymd_stay_dates[0];
4703 $hist_extra_data->checkout_date = $ymd_stay_dates[1];
4704 $hist_extra_data->adults = array_sum($adults);
4705 $hist_extra_data->children = array_sum($children);
4706
4707 if ($tot_records > 0) {
4708 // we can create an inquiry pending reservation for a room rate
4709 $inquiry_res_id = 0;
4710 foreach ($room_rates as $rid => $rates) {
4711 foreach ($rates as $room_rplan) {
4712 if (!is_array($room_rplan) || !isset($room_rplan['idroom'])) {
4713 continue;
4714 }
4715 // we grab the cheapest room and cheapest rate plan (the first room-rate plan)
4716 $inquiry_res_id = $av_helper->createInquiryReservation($room_rplan, $customer);
4717 break;
4718 }
4719 // make sure to create just one inquiry reservation for the cheapest room available
4720 if ($inquiry_res_id) {
4721 break;
4722 }
4723 }
4724
4725 if ($inquiry_res_id) {
4726 // assign booking to customer
4727 $cpin->saveCustomerBooking($inquiry_res_id);
4728
4729 // send email notification to admin
4730 VikBooking::sendBookingEmail($inquiry_res_id, array('admin'));
4731
4732 // trigger SMS sending
4733 VikBooking::sendBookingSMS($inquiry_res_id);
4734
4735 // booking history (set inquiry availability type to 1)
4736 $hist_extra_data->av_type = 1;
4737 VikBooking::getBookingHistoryInstance()->setBid($inquiry_res_id)->setExtraData($hist_extra_data)->store('IR', $customer->adminnotes);
4738 }
4739
4740 // update the response status no matter what
4741 $response->status = 1;
4742
4743 // output response and terminate the request
4744 VBOHttpDocument::getInstance()->json($response);
4745 }
4746
4747 // rely on suggestions in case of no rooms available
4748 if (!is_array($room_rates) || $has_av_error) {
4749 // try to get the suggestions when no availability
4750 list($alternative_dates, $alternative_parties) = $av_helper->findSuggestions();
4751
4752 $inquiry_res_id = 0;
4753 $alt_suggestion = '';
4754
4755 if (count($alternative_dates)) {
4756 $inquiry_res_id = $av_helper->allocateAltDatesInquiry($alternative_dates, $customer);
4757 $alt_suggestion = JText::translate('VBO_ALT_DATES_INQ');
4758 // set inquiry availability type to 2 for alternative dates
4759 $hist_extra_data->av_type = 2;
4760 } elseif (count($alternative_parties)) {
4761 $inquiry_res_id = $av_helper->allocateAltPartyInquiry($alternative_parties, $customer);
4762 $alt_suggestion = JText::translate('VBO_ALT_PARTY_INQ');
4763 // set inquiry availability type to 3 for alternative party
4764 $hist_extra_data->av_type = 3;
4765 }
4766
4767 if ($inquiry_res_id) {
4768 // assign booking to customer
4769 $cpin->saveCustomerBooking($inquiry_res_id);
4770
4771 // send email notification to admin
4772 VikBooking::sendBookingEmail($inquiry_res_id, array('admin'));
4773
4774 // trigger SMS sending
4775 VikBooking::sendBookingSMS($inquiry_res_id);
4776
4777 // booking history
4778 $history_obj = VikBooking::getBookingHistoryInstance()->setBid($inquiry_res_id);
4779 // store first history record
4780 $history_obj->store('IR', $customer->adminnotes);
4781 // store history record mentioning the suggestion used
4782 $history_obj->setExtraData($hist_extra_data)->store('IR', $alt_suggestion);
4783 }
4784
4785 // update the response status no matter what
4786 $response->status = 1;
4787
4788 // output response and terminate the request
4789 VBOHttpDocument::getInstance()->json($response);
4790 }
4791
4792 /**
4793 * If we reach this point it means that no rooms were available and no rooms/dates could be
4794 * suggested as an alternative party. Therefore, we allocate the reservation on a "dummy room".
4795 */
4796 $all_rooms = $av_helper->loadRooms();
4797 if (count($all_rooms)) {
4798 // grab the first "dummy room"
4799 $dummy_room_id = key($all_rooms);
4800 $room_rplan = array(
4801 'idroom' => $dummy_room_id,
4802 );
4803
4804 // create inquiry reservation for a dummy room
4805 $inquiry_res_id = $av_helper->createInquiryReservation($room_rplan, $customer);
4806 $alt_suggestion = JText::translate('VBO_ALT_DUMMY_INQ');
4807 // set inquiry availability type to 4 for "dummy room"
4808 $hist_extra_data->av_type = 4;
4809
4810 if ($inquiry_res_id) {
4811 // assign booking to customer
4812 $cpin->saveCustomerBooking($inquiry_res_id);
4813
4814 // send email notification to admin
4815 VikBooking::sendBookingEmail($inquiry_res_id, array('admin'));
4816
4817 // trigger SMS sending
4818 VikBooking::sendBookingSMS($inquiry_res_id);
4819
4820 // booking history
4821 $history_obj = VikBooking::getBookingHistoryInstance()->setBid($inquiry_res_id);
4822 // store first history record
4823 $history_obj->store('IR', $customer->adminnotes);
4824 // store history record mentioning the suggestion used
4825 $history_obj->setExtraData($hist_extra_data)->store('IR', $alt_suggestion);
4826
4827 // update the response status
4828 $response->status = 1;
4829
4830 // output response and terminate the request
4831 VBOHttpDocument::getInstance()->json($response);
4832 }
4833 }
4834
4835 // if not even a dummy room could be allocated, it means Vik Booking isn't set up
4836 $response->error = $av_helper->explainErrorCode();
4837 if (empty($response->error)) {
4838 $response->error = 'No rooms have been configured on this site yet';
4839 }
4840
4841 // output response and terminate the request
4842 VBOHttpDocument::getInstance()->json($response);
4843 }
4844
4845 /**
4846 * AJAX endpoint to load the states of a given country.
4847 *
4848 * @return void
4849 *
4850 * @since 1.16.0 (J) - 1.6.0 (WP)
4851 */
4852 public function states_load_from_country()
4853 {
4854 $dbo = JFactory::getDbo();
4855
4856 $id_country = VikRequest::getInt('id_country', 0, 'request');
4857 $country_3_code = VikRequest::getString('country_3_code', '', 'request');
4858 $country_2_code = VikRequest::getString('country_2_code', '', 'request');
4859 $country_name = VikRequest::getString('country_name', '', 'request');
4860
4861 if (empty($id_country) && empty($country_3_code) && empty($country_2_code) && empty($country_name)) {
4862 VBOHttpDocument::getInstance()->close(500, 'Missing country identifier');
4863 }
4864
4865 if (!empty($id_country)) {
4866 $q = "SELECT * FROM `#__vikbooking_states` WHERE `id_country`=" . $id_country;
4867 $dbo->setQuery($q);
4868 $dbo->execute();
4869 if (!$dbo->getNumRows()) {
4870 // no records found for this country
4871 VBOHttpDocument::getInstance()->json([]);
4872 }
4873 // output the JSON encoded list of states found
4874 VBOHttpDocument::getInstance()->json($dbo->loadAssocList());
4875 }
4876
4877 // find country ID by name or code
4878 $field_name = $dbo->qn('country_name');
4879 $field_value = $country_name;
4880 if (!empty($country_3_code)) {
4881 $field_name = $dbo->qn('country_3_code');
4882 $field_value = $country_3_code;
4883 }
4884 if (!empty($country_2_code)) {
4885 $field_name = $dbo->qn('country_2_code');
4886 $field_value = $country_2_code;
4887 }
4888
4889 $q = "SELECT `id` FROM `#__vikbooking_countries` WHERE {$field_name}=" . $dbo->quote($field_value);
4890 $dbo->setQuery($q, 0, 1);
4891 $dbo->execute();
4892 if (!$dbo->getNumRows()) {
4893 // country not found
4894 VBOHttpDocument::getInstance()->close(404, sprintf('Country [%s] not found', $field_value));
4895 }
4896
4897 $id_country = $dbo->loadResult();
4898
4899 $q = "SELECT * FROM `#__vikbooking_states` WHERE `id_country`=" . $id_country;
4900 $dbo->setQuery($q);
4901 $dbo->execute();
4902 if (!$dbo->getNumRows()) {
4903 // no records found for this country
4904 VBOHttpDocument::getInstance()->json([]);
4905 }
4906 // output the JSON encoded list of states found
4907 VBOHttpDocument::getInstance()->json($dbo->loadAssocList());
4908 }
4909
4910 /**
4911 * AJAX endpoint to perform the room upgrade operation.
4912 *
4913 * @return void
4914 *
4915 * @since 1.16.0 (J) - 1.6.0 (WP)
4916 */
4917 public function upgrade_room()
4918 {
4919 $dbo = JFactory::getDbo();
4920
4921 $bid = VikRequest::getInt('bid', 0, 'request');
4922 $sid = VikRequest::getString('sid', '', 'request');
4923 $ts = VikRequest::getString('ts', '', 'request');
4924 $room_index = VikRequest::getInt('room_index', 0, 'request');
4925 $room_id = VikRequest::getInt('room_id', 0, 'request');
4926
4927 if (empty($room_id)) {
4928 VBOHttpDocument::getInstance()->close(500, 'Invalid data provided');
4929 }
4930
4931 $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'";
4932 $dbo->setQuery($q, 0, 1);
4933 $dbo->execute();
4934 if (!$dbo->getNumRows()) {
4935 VBOHttpDocument::getInstance()->close(404, JText::translate('VBORDERNOTFOUND'));
4936 }
4937
4938 $booking = $dbo->loadAssoc();
4939
4940 $booking_rooms = VikBooking::loadOrdersRoomsData($booking['id']);
4941 if (!$booking_rooms || !isset($booking_rooms[$room_index])) {
4942 VBOHttpDocument::getInstance()->close(404, JText::translate('VBORDERNOTFOUND'));
4943 }
4944
4945 // access the room helper object
4946 $room_helper = VBORoomHelper::getInstance([
4947 'booking' => $booking,
4948 'rooms' => $booking_rooms,
4949 ]);
4950
4951 // load the upgrade options for this booking
4952 $upgrade_options = $room_helper->getUpgradeOptions();
4953
4954 if (!$upgrade_options || !isset($upgrade_options['upgrade'][$room_index]) || !isset($upgrade_options['upgrade'][$room_index]['r_costs'][$room_id])) {
4955 // the room for the upgrade is not available
4956 VBOHttpDocument::getInstance()->close(500, 'Could not upgrade to the selected room. Please reload the page and try again');
4957 }
4958
4959 $upgrade_room_rate = $upgrade_options['upgrade'][$room_index]['r_costs'][$room_id];
4960
4961 // calculate the cost difference, if any
4962 $current_room_cost = $booking_rooms[$room_index]['cust_cost'] > 0 ? (float)$booking_rooms[$room_index]['cust_cost'] : (float)$booking_rooms[$room_index]['room_cost'];
4963 $upgrade_room_cost = $current_room_cost;
4964 $upgrade_difference = 0;
4965 $upgtax_difference = 0;
4966
4967 if ($current_room_cost < $upgrade_room_rate['upgrade_cost']) {
4968 // we update the total booking amount only if the upgrade room is more expensive
4969 $upgrade_room_cost = $upgrade_room_rate['upgrade_cost'];
4970 $upgrade_difference = $upgrade_room_rate['upgrade_cost'] - $current_room_cost;
4971 // handle taxes
4972 $current_tariff_data = VBORoomHelper::getInstance()->getTariffData($booking_rooms[$room_index]['idtar']);
4973 if ($current_tariff_data) {
4974 // current room tax
4975 $current_cost_plus_tax = VikBooking::sayCostPlusIva($current_room_cost, $current_tariff_data['idprice']);
4976 $current_cost_minus_tax = VikBooking::sayCostMinusIva($current_room_cost, $current_tariff_data['idprice']);
4977 $current_room_tax = $current_cost_plus_tax - $current_cost_minus_tax;
4978 // upgrade room tax
4979 $upgrade_cost_plus_tax = VikBooking::sayCostPlusIva($upgrade_room_rate['upgrade_cost'], $upgrade_room_rate['idprice']);
4980 $upgrade_cost_minus_tax = VikBooking::sayCostMinusIva($upgrade_room_rate['upgrade_cost'], $upgrade_room_rate['idprice']);
4981 $upgrade_room_tax = $upgrade_cost_plus_tax - $upgrade_cost_minus_tax;
4982 // calculate tax difference
4983 $upgtax_difference = $upgrade_room_tax - $current_room_tax;
4984 }
4985
4986 }
4987
4988 // update booking total amounts as first
4989 if ($upgrade_difference > 0) {
4990 $upd_booking = new stdClass;
4991 $upd_booking->id = $booking['id'];
4992 $upd_booking->total = (float)($booking['total'] + $upgrade_difference);
4993 $upd_booking->tot_taxes = (float)($booking['tot_taxes'] + $upgtax_difference);
4994
4995 $dbo->updateObject('#__vikbooking_orders', $upd_booking, 'id');
4996 }
4997
4998 // perform the room upgrade (switch)
4999 $broom_record = new stdClass;
5000 $broom_record->id = $booking_rooms[$room_index]['id'];
5001 $broom_record->idorder = $booking_rooms[$room_index]['idorder'];
5002 $broom_record->idroom = $room_id;
5003 $broom_record->idtar = $upgrade_room_rate['id'];
5004 $broom_record->room_cost = $upgrade_room_cost;
5005
5006 $dbo->updateObject('#__vikbooking_ordersrooms', $broom_record, ['id', 'idorder']);
5007
5008 // Booking History
5009 $hist_descr = !empty($booking_rooms[$room_index]['room_name']) ? $booking_rooms[$room_index]['room_name'] . ' ' : '';
5010 $hist_descr .= '-&gt; ' . $upgrade_options['rooms'][$room_id]['name'];
5011 VikBooking::getBookingHistoryInstance()->setBid($booking['id'])->store('UR', $hist_descr);
5012
5013 // output the JSON encoded successful response
5014 VBOHttpDocument::getInstance()->json(['success' => 1]);
5015 }
5016
5017 /**
5018 * End-point used to ping the execution of the scheduled crontab runners.
5019 *
5020 * @return void
5021 *
5022 * @since 1.17 (J) - 1.7 (WP)
5023 */
5024 public function crontab()
5025 {
5026 VBOFactory::getCrontabSimulator()->run();
5027 exit;
5028 }
5029
5030 /**
5031 * End-point used to route at runtime the link to the requested room details page.
5032 * Originally introduced to improve landing page URL accuracy with Google VR.
5033 *
5034 * @return void
5035 *
5036 * @since 1.18.3 (J) - 1.8.3 (WP)
5037 */
5038 public function route_listing_details()
5039 {
5040 $app = JFactory::getApplication();
5041
5042 $listing_id = $app->input->getUint('listing_id', 0);
5043
5044 if (!$listing_id) {
5045 VBOHttpDocument::getInstance($app)->close(400, 'Missing listing ID.');
5046 }
5047
5048 // make sure the requested listing exists
5049 $listing_data = VikBooking::getRoomInfo($listing_id);
5050
5051 if (!$listing_data) {
5052 VBOHttpDocument::getInstance($app)->close(404, 'Listing not found.');
5053 }
5054
5055 if (empty($listing_data['avail'])) {
5056 VBOHttpDocument::getInstance($app)->close(404, 'Listing currently unavailable.');
5057 }
5058
5059 // get user country and locale (preferred language), if available, to find the best language
5060 $user_country = $app->input->getString('country', '');
5061 $user_lang = $app->input->getString('ulang', '');
5062 $user_country = empty($user_country) && !empty($user_lang) ? $user_lang : $user_country;
5063 $best_lang = (!empty($user_country) || !empty($user_lang)) && class_exists('VikChannelManager') ? VikChannelManager::guessBookingLangFromCountry($user_country, $user_lang) : '';
5064
5065 /**
5066 * Adjust best language in case the default website lang is different than English and
5067 * the visitor speaks a foreign language. This way we make English the best language.
5068 * I.E. Website default lang is IT, visitor's lang is DE, we make it land on EN because DE isn't available.
5069 */
5070 $current_lang = JFactory::getLanguage()->getTag();
5071 if (empty($best_lang) && !empty($user_lang) && substr(strtolower($user_lang), 0, 2) != 'en' && substr(strtolower($current_lang), 0, 2) != 'en') {
5072 // check if English is available
5073 foreach (VikBooking::getVboApplication()->getKnownLanguages() as $ltag => $ldet) {
5074 if (substr(strtolower($ltag), 0, 2) == 'en') {
5075 // grab this English-esque language
5076 $best_lang = $ltag;
5077 break;
5078 }
5079 }
5080 }
5081
5082 // build query arguments for routing
5083 $route_query_args = [
5084 'option' => 'com_vikbooking',
5085 'view' => 'roomdetails',
5086 'roomid' => $listing_id,
5087 ];
5088
5089 // route the best URI (if possible)
5090 try {
5091 // find the best page id for the URL according to the CMS
5092 $itemid = null;
5093
5094 if (VBOPlatformDetection::isWordPress()) {
5095 /**
5096 * @wponly route the best Shortcode for the booking process ("room details" or "search form").
5097 * the best booking language is passed over the model to find the best shortcode.
5098 */
5099 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
5100
5101 // grab all Shortcodes and parse them to find the best one for this room, if any
5102 $shortcodes = $model->all();
5103 foreach ($shortcodes as $shortcode) {
5104 if ($shortcode->type != 'roomdetails' || empty($shortcode->post_id)) {
5105 continue;
5106 }
5107 $page_params = json_decode($shortcode->json);
5108 if (!is_object($page_params) || !isset($page_params->roomid) || (int) $page_params->roomid != $listing_id) {
5109 continue;
5110 }
5111 if (!empty($best_lang) && $shortcode->lang == $best_lang) {
5112 // always give higher priority to the exact lang
5113 $itemid = $shortcode->post_id;
5114 }
5115 if (empty($itemid)) {
5116 // in case no perfect lang found, use this post id for this room-type
5117 $itemid = $shortcode->post_id;
5118 }
5119 }
5120
5121 if (empty($itemid)) {
5122 // default to the best shortcode for this lang of type "search form"
5123 $itemid = $model->best(['vikbooking'], $best_lang);
5124 if (!$itemid) {
5125 // if we really get to this point, try to fetch the first non-empty shortcode, if any
5126 $shortcodes = $model->all($columns = 'post_id', $full = true);
5127 if ($shortcodes) {
5128 // grab the very first active shortcode, no matter what's the type of it
5129 $itemid = $shortcodes[0]->post_id;
5130 }
5131 }
5132 }
5133 } else {
5134 /**
5135 * @joomlaonly inject the language to the query arguments list before routing
5136 */
5137 if (!empty($best_lang)) {
5138 $route_query_args['lang'] = $best_lang;
5139 }
5140
5141 if (class_exists('VCMFactory')) {
5142 $best_menuitem_id = VCMFactory::getPlatform()->getPagerouter()->findProperPageId(['roomdetails'], ['roomid' => $room_type_id, 'lang' => $best_lang]);
5143
5144 if ($best_menuitem_id) {
5145 $itemid = $best_menuitem_id;
5146 } else {
5147 $itemid = VCMFactory::getPlatform()->getPagerouter()->findProperPageId(['vikbooking'], $best_lang);
5148 }
5149 }
5150 }
5151
5152 // route final URL with all arguments
5153 $routed_url = VikBooking::externalroute($route_query_args, false, $itemid);
5154 } catch (Throwable $e) {
5155 // raise an error
5156 VBOHttpDocument::getInstance($app)->close(500, 'Could not route to the requested page.');
5157 }
5158
5159 // redirect the request to the proper page
5160 $app->redirect($routed_url);
5161 $app->close();
5162 }
5163 }
5164