PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.7
VikBooking Hotel Booking Engine & PMS v1.8.7
1.8.16 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 All 37 releases
vikbooking / site / controller.php

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

5,170 lines 198.0 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 // update booking record
2383 if (!$dbo->updateObject('#__vikbooking_orders', $booking_record, 'id')) {
2384 // payments log content may break the update
2385 unset($booking_record->paymentlog);
2386 // try once more
2387 $dbo->updateObject('#__vikbooking_orders', $booking_record, 'id');
2388 }
2389
2390 // assign room specific unit
2391 $set_room_indexes = VikBooking::autoRoomUnit();
2392 $room_indexes_usemap = [];
2393 if ($set_room_indexes === true) {
2394 $q = "SELECT `id`,`idroom`,`roomindex` FROM `#__vikbooking_ordersrooms` WHERE `idorder`=".(int)$row['id'].";";
2395 $dbo->setQuery($q);
2396 $orooms = $dbo->loadAssocList();
2397 foreach ($orooms as $oroom) {
2398 if (!empty($oroom['roomindex'])) {
2399 // room specific unit has already been assigned
2400 continue;
2401 }
2402 $room_indexes = VikBooking::getRoomUnitNumsAvailable($row, $oroom['idroom']);
2403 $use_ind_key = 0;
2404 if ($room_indexes) {
2405 if (!array_key_exists($oroom['idroom'], $room_indexes_usemap)) {
2406 $room_indexes_usemap[$oroom['idroom']] = $use_ind_key;
2407 } else {
2408 $use_ind_key = $room_indexes_usemap[$oroom['idroom']];
2409 }
2410 $q = "UPDATE `#__vikbooking_ordersrooms` SET `roomindex`=".(int)$room_indexes[$use_ind_key]." WHERE `id`=".(int)$oroom['id'].";";
2411 $dbo->setQuery($q);
2412 $dbo->execute();
2413 // update rooms references for the customer email sending function
2414 foreach ($rooms as $rnum => $rr) {
2415 if ($rr['or_id'] == $oroom['id']) {
2416 $rooms[$rnum]['roomindex'] = (int)$room_indexes[$use_ind_key];
2417 break;
2418 }
2419 }
2420 $room_indexes_usemap[$oroom['idroom']]++;
2421 }
2422 }
2423 }
2424
2425 // unlock room(s) for other imminent bookings
2426 $q = "DELETE FROM `#__vikbooking_tmplock` WHERE `idorder`=" . intval($row['id']) . ";";
2427 $dbo->setQuery($q);
2428 $dbo->execute();
2429
2430 // customer booking
2431 $q = "SELECT `idcustomer` FROM `#__vikbooking_customers_orders` WHERE `idorder`=".(int)$row['id'].";";
2432 $dbo->setQuery($q);
2433 $customer_id = $dbo->loadResult();
2434 if ($customer_id) {
2435 $cpin = VikBooking::getCPinIstance();
2436 $cpin->updateBookingCommissions($row['id'], $customer_id);
2437 }
2438
2439 // check if some of the rooms booked have shared calendars
2440 VikBooking::updateSharedCalendars($row['id'], array(), $row['checkin'], $row['checkout']);
2441
2442 /**
2443 * Trigger event to allow third-party plugins to choose whether payment notifications should be sent
2444 *
2445 * @since 1.16.10 (J) - 1.6.10 (WP)
2446 */
2447 $send_notifications = true;
2448 $should_send = VBOFactory::getPlatform()->getDispatcher()->filter('onPaymentReceivedShouldSendNotifications', [$row]);
2449 if (is_array($should_send) && in_array(false, $should_send, true)) {
2450 $send_notifications = false;
2451 }
2452
2453 if ($send_notifications) {
2454 // send email notification to guest and admin
2455 VikBooking::sendBookingEmail($row['id'], array('guest', 'admin'));
2456
2457 // SMS
2458 VikBooking::sendBookingSMS($row['id']);
2459 }
2460
2461 /**
2462 * Payment gateways may set and return the transaction information
2463 * to eventually support a later transaction of type refund.
2464 *
2465 * @since 1.14 (J) - 1.4.0 (WP)
2466 * @since 1.16.2 (J) - 1.6.2 (WP) we attempt to always store the amount paid with this transaction.
2467 * @since 1.16.9 (J) - 1.6.9 (WP) we attempt to store the amount of payment processing fees for the transaction.
2468 * @since 1.17.6 (J) - 1.7.6 (WP) added support to separate payment for damage deposit.
2469 */
2470 $tn_data = $array_result['transaction'] ?? null;
2471 if (isset($array_result['tot_paid']) && $array_result['tot_paid']) {
2472 // check event data payload to store
2473 if (is_array($tn_data)) {
2474 // set key
2475 $tn_data['amount_paid'] = (float) $array_result['tot_paid'];
2476 } elseif (is_object($tn_data)) {
2477 // set property
2478 $tn_data->amount_paid = (float) $array_result['tot_paid'];
2479 } elseif (!$tn_data) {
2480 // build an array (we add the payment name because we know there is no other transaction data)
2481 $tn_data = [
2482 'amount_paid' => (float) $array_result['tot_paid'],
2483 'payment_method' => $payment['name'],
2484 ];
2485 }
2486 }
2487 if ($tn_data && isset($array_result['tot_fees']) && $array_result['tot_fees']) {
2488 // check event data payload to store
2489 if (is_array($tn_data)) {
2490 // set key
2491 $tn_data['processing_fees'] = (float) $array_result['tot_fees'];
2492 } elseif (is_object($tn_data)) {
2493 // set property
2494 $tn_data->processing_fees = (float) $array_result['tot_fees'];
2495 }
2496 }
2497 if ($app->input->getBool('dd') && ($damage_deposit_payment['damagedep_gross'] ?? null)) {
2498 if ($tn_data) {
2499 $tn_data = (array) $tn_data;
2500 } else {
2501 $tn_data = [];
2502 }
2503 // damage deposit may be authorized without an amount paid returned
2504 $tn_data['damage_deposit'] = (float) $damage_deposit_payment['damagedep_gross'];
2505 }
2506
2507 // Booking History
2508 VikBooking::getBookingHistoryInstance()->setBid($row['id'])->setExtraData($tn_data)->store('P' . ($row['paymcount'] > 0 ? 'N' : '0'), $payment['name']);
2509
2510 // invoke VikChannelManager
2511 if (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "synch.vikbooking.php")) {
2512 require_once(VCM_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "synch.vikbooking.php");
2513 $vcm = new SynchVikBooking($row['id']);
2514 $vcm->setPushType('new')->sendRequest();
2515 }
2516 $vcmchanneldata = $session->get('vcmChannelData', '');
2517 if (!empty($vcmchanneldata)) {
2518 $session->set('vcmChannelData', '');
2519 }
2520 //end invoke VikChannelManager
2521 if (method_exists($obj, 'afterValidation')) {
2522 $obj->afterValidation(1);
2523 }
2524 } else {
2525 if (empty($array_result['skip_email'])) {
2526 $vbo_app = VikBooking::getVboApplication();
2527 $adsendermail = VikBooking::getSenderMail();
2528 $vbo_app->sendMail($adsendermail, $adsendermail, $recipient_mail, $adsendermail, JText::translate('VBPAYMENTNOTVER'), JText::translate('VBSERVRESP') . ":\n\n" . $array_result['log'], false);
2529 }
2530 if (!empty($array_result['log'])) {
2531 $q = "UPDATE `#__vikbooking_orders` SET `paymentlog`=".$dbo->quote($newpaymentlog)." WHERE `id`='" . $row['id'] . "';";
2532 $dbo->setQuery($q);
2533 $dbo->execute();
2534 }
2535 if (method_exists($obj, 'afterValidation')) {
2536 $obj->afterValidation(0);
2537 }
2538 }
2539 }
2540
2541 public function currencyconverter()
2542 {
2543 if (!JSession::checkToken()) {
2544 // missing CSRF-proof token
2545 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
2546 }
2547
2548 $session = JFactory::getSession();
2549 $pprices = VikRequest::getVar('prices', array(0));
2550 $pfromsymbol = VikRequest::getString('fromsymbol', '', 'request');
2551 $ptocurrency = VikRequest::getString('tocurrency', '', 'request');
2552 $pfromcurrency = VikRequest::getString('fromcurrency', '', 'request');
2553 $default_cur = !empty($pfromcurrency) ? $pfromcurrency : VikBooking::getCurrencyName();
2554 $response = array();
2555 if (!empty($default_cur) && !empty($pprices) && count($pprices) > 0 && !empty($ptocurrency)) {
2556 require_once(VBO_SITE_PATH . DS . "helpers" . DS ."currencyconverter.php");
2557 if ($default_cur != $ptocurrency) {
2558 $format = VikBooking::getNumberFormatData();
2559 $converter = new VboCurrencyConverter($default_cur, $ptocurrency, $pprices, explode(':', $format));
2560 $exchanged = $converter->convert();
2561 if (count($exchanged) > 0) {
2562 $response = $exchanged;
2563 $session->set('vboLastCurrency', $ptocurrency);
2564 } else {
2565 $conv_error = $converter->getError();
2566 $response['error'] = !empty($conv_error) ? $conv_error : JText::translate('VBERRCURCONVINVALIDDATA');
2567 }
2568 } else {
2569 $session->set('vboLastCurrency', $ptocurrency);
2570 foreach ($pprices as $i => $price) {
2571 $response[$i]['symbol'] = $pfromsymbol;
2572 $response[$i]['price'] = $price;
2573 }
2574 }
2575 } else {
2576 $response['error'] = JText::translate('VBERRCURCONVNODATA');
2577 }
2578 if(array_key_exists('error', $response)) {
2579 $session->set('vboLastCurrency', $ptocurrency);
2580 }
2581 echo json_encode($response);
2582 exit;
2583 }
2584
2585 public function signature()
2586 {
2587 VikRequest::setVar('view', 'signature');
2588 parent::display();
2589 }
2590
2591 public function storesignature()
2592 {
2593 if (!JSession::checkToken()) {
2594 // missing CSRF-proof token
2595 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
2596 }
2597
2598 $sid = VikRequest::getString('sid', '', 'request');
2599 $ts = VikRequest::getString('ts', '', 'request');
2600 $psignature = VikRequest::getString('signature', '', 'request', VIKREQUEST_ALLOWRAW);
2601 $ppad_width = VikRequest::getInt('pad_width', '', 'request');
2602 $ppad_ratio = VikRequest::getInt('pad_ratio', '', 'request');
2603 $pitemid = VikRequest::getInt('Itemid', '', 'request');
2604 $ptmpl = VikRequest::getString('tmpl', '', 'request');
2605 $dbo = JFactory::getDBO();
2606 $mainframe = JFactory::getApplication();
2607 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `ts`=" . $dbo->quote($ts) . " AND `sid`=" . $dbo->quote($sid) . " AND `status`='confirmed';";
2608 $dbo->setQuery($q);
2609 $dbo->execute();
2610 if ($dbo->getNumRows() < 1) {
2611 VikError::raiseWarning('', 'Booking not found');
2612 $mainframe->redirect('index.php');
2613 exit;
2614 }
2615 $row = $dbo->loadAssoc();
2616 $tonight = mktime(23, 59, 59, date('n'), date('j'), date('Y'));
2617 if ($tonight > $row['checkout']) {
2618 VikError::raiseWarning('', 'Check-out date is in the past');
2619 $mainframe->redirect('index.php');
2620 exit;
2621 }
2622 $customer = array();
2623 $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'].";";
2624 $dbo->setQuery($q);
2625 $dbo->execute();
2626 if ($dbo->getNumRows() > 0) {
2627 $customer = $dbo->loadAssoc();
2628 }
2629 if (!(count($customer) > 0)) {
2630 VikError::raiseWarning('', 'Customer not found');
2631 $mainframe->redirect('index.php');
2632 exit;
2633 }
2634 //check if the signature has been submitted
2635 $signature_data = '';
2636 $cont_type = '';
2637 if (!empty($psignature)) {
2638 /**
2639 * Implemented safe filtering of base64-encoded signature image
2640 * to obtain content and file extension.
2641 *
2642 * @since 1.15.1 (J) - 1.5.4 (WP)
2643 */
2644 if (preg_match("/^data:image\/(png|jpe?g|svg);base64,([A-Za-z0-9\/=+]+)$/", $psignature, $safe_match)) {
2645 $signature_data = base64_decode($safe_match[2]);
2646 $cont_type = $safe_match[1];
2647 }
2648 }
2649 $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);
2650 if (empty($signature_data)) {
2651 VikError::raiseWarning('', JText::translate('VBOSIGNATUREISEMPTY'));
2652 $mainframe->redirect($ret_link);
2653 exit;
2654 }
2655 //write file
2656 $sign_fname = $row['id'].'_'.$row['sid'].'_'.$customer['id'].'.'.$cont_type;
2657 $filepath = VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'resources' . DIRECTORY_SEPARATOR . 'idscans' . DIRECTORY_SEPARATOR . $sign_fname;
2658 $fp = fopen($filepath, 'w+');
2659 $bytes = fwrite($fp, $signature_data);
2660 fclose($fp);
2661 if ($bytes !== false && $bytes > 0) {
2662 //update the signature in the DB
2663 $q = "UPDATE `#__vikbooking_customers_orders` SET `signature`=".$dbo->quote($sign_fname)." WHERE `idorder`=".(int)$row['id'].";";
2664 $dbo->setQuery($q);
2665 $dbo->execute();
2666 $mainframe->enqueueMessage(JText::translate('VBOSIGNATURETHANKS'));
2667 //resize image for screens with high resolution
2668 if ($ppad_ratio > 1) {
2669 $new_width = floor(($ppad_width / 2));
2670 $creativik = new vikResizer();
2671 $creativik->proportionalImage($filepath, $filepath, $new_width, $new_width);
2672 }
2673 //
2674 } else {
2675 VikError::raiseWarning('', JText::translate('VBOERRSTORESIGNFILE'));
2676 }
2677 $mainframe->redirect($ret_link);
2678 exit;
2679 }
2680
2681 public function validatepin()
2682 {
2683 if (!JSession::checkToken()) {
2684 // missing CSRF-proof token
2685 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
2686 }
2687
2688 $cpin = VikBooking::getCPinIstance();
2689
2690 $ppin = VikRequest::getString('pin', '', 'request');
2691
2692 $response = [];
2693
2694 $customer = $cpin->getCustomerByPin($ppin);
2695 if ($customer) {
2696 $response = $customer;
2697 $response['success'] = 1;
2698
2699 if ($cpin->getCustomerCoupon($customer)) {
2700 // set flag indicating that the customer has got dedicated discounts
2701 $response['has_discounts'] = 1;
2702 }
2703 }
2704
2705 echo json_encode($response);
2706 exit;
2707 }
2708
2709 public function docancelbooking()
2710 {
2711 if (!JSession::checkToken()) {
2712 // missing CSRF-proof token
2713 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
2714 }
2715
2716 $dbo = JFactory::getDbo();
2717 $mainframe = JFactory::getApplication();
2718
2719 $psid = VikRequest::getString('sid', '', 'request');
2720 $pidorder = VikRequest::getString('idorder', '', 'request');
2721
2722 if (!empty($psid) && !empty($pidorder)) {
2723 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=".intval($pidorder)." AND `sid`=".$dbo->quote($psid)." AND `status`='confirmed';";
2724 $dbo->setQuery($q);
2725 $order = $dbo->loadAssocList();
2726 if ($order) {
2727 $pemail = VikRequest::getString('email', '', 'request');
2728 $preason = VikRequest::getString('reason', '', 'request');
2729 if (!empty($pemail) && !empty($preason)) {
2730 $to = VikBooking::getAdminMail();
2731 if (strpos($to, ',') !== false) {
2732 $all_recipients = explode(',', $to);
2733 foreach ($all_recipients as $k => $v) {
2734 if (empty($v)) {
2735 unset($all_recipients[$k]);
2736 }
2737 }
2738 if (count($all_recipients) > 0) {
2739 $to = $all_recipients;
2740 }
2741 }
2742 //check if the booking can be cancelled
2743 $days_to_arrival = 0;
2744 $is_refundable = 0;
2745 $daysadv_refund_arr = array();
2746 $daysadv_refund = 0;
2747 $now_info = getdate();
2748 $checkin_info = getdate($order[0]['checkin']);
2749 if ($now_info[0] < $checkin_info[0]) {
2750 while ($now_info[0] < $checkin_info[0]) {
2751 if (!($now_info['mday'] != $checkin_info['mday'] || $now_info['mon'] != $checkin_info['mon'] || $now_info['year'] != $checkin_info['year'])) {
2752 break;
2753 }
2754 $days_to_arrival++;
2755 $now_info = getdate(mktime(0, 0, 0, $now_info['mon'], ($now_info['mday'] + 1), $now_info['year']));
2756 }
2757 }
2758 $tars = array();
2759 $is_package = !empty($order[0]['pkg']) ? true : false;
2760 $orderrooms = array();
2761 $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;";
2762 $dbo->setQuery($q);
2763 $orderrooms = $dbo->loadAssocList();
2764 if ($orderrooms) {
2765 foreach($orderrooms as $kor => $or) {
2766 $num = $kor + 1;
2767 if($is_package === true || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
2768 //package or custom cost set from the back-end
2769 continue;
2770 }
2771 $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'] . "';";
2772 $dbo->setQuery($q);
2773 $tar = $dbo->loadAssocList();
2774 if ($tar) {
2775 $tars[$num] = $tar[0];
2776 }
2777 }
2778 }
2779 foreach ($tars as $num => $tar) {
2780 if ($tar['free_cancellation'] < 1) {
2781 //if at least one rate plan is non-refundable, the whole reservation cannot be cancelled
2782 $is_refundable = 0;
2783 $daysadv_refund_arr = array();
2784 break;
2785 }
2786 $is_refundable = 1;
2787 $daysadv_refund_arr[] = $tar['canc_deadline'];
2788 }
2789 //get the rate plan with the lowest cancellation deadline
2790 $daysadv_refund = count($daysadv_refund_arr) > 0 ? min($daysadv_refund_arr) : $daysadv_refund;
2791 $resmodcanc = VikBooking::getReservationModCanc();
2792 $resmodcanc = $days_to_arrival < 1 ? 0 : $resmodcanc;
2793 $resmodcancmin = VikBooking::getReservationModCancMin();
2794 $canc_allowed = ($resmodcanc > 1 && $resmodcanc != 2 && $is_refundable > 0 && $daysadv_refund <= $days_to_arrival && $days_to_arrival >= $resmodcancmin);
2795 if (!$canc_allowed) {
2796 VikError::raiseWarning('', JText::translate('VBOERRCANNOTCANCBOOK'));
2797 $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));
2798 exit;
2799 }
2800 //make the cancellation in the db and update the administrator notes with the reason specified by the customer
2801 $new_adminotes = JText::translate('VBOBOOKCANCELLEDEMAILSUBJ').' ('.$pemail.")\n".$preason."\n\n".$order[0]['adminnotes'];
2802 $q = "UPDATE `#__vikbooking_orders` SET `status`='cancelled',`adminnotes`=".$dbo->quote($new_adminotes)." WHERE `id`=".(int)$order[0]['id'].";";
2803 $dbo->setQuery($q);
2804 $dbo->execute();
2805 $q = "SELECT * FROM `#__vikbooking_ordersbusy` WHERE `idorder`=".(int)$order[0]['id'].";";
2806 $dbo->setQuery($q);
2807 $ordbusy = $dbo->loadAssocList();
2808 if ($ordbusy) {
2809 foreach ($ordbusy as $ob) {
2810 $q = "DELETE FROM `#__vikbooking_busy` WHERE `id`=".(int)$ob['idbusy'].";";
2811 $dbo->setQuery($q);
2812 $dbo->execute();
2813 }
2814 }
2815 $q = "DELETE FROM `#__vikbooking_ordersbusy` WHERE `idorder`=".(int)$order[0]['id'].";";
2816 $dbo->setQuery($q);
2817 $dbo->execute();
2818
2819 if ($order[0]['split_stay']) {
2820 // attempt to remove the transient record
2821 VBOFactory::getConfig()->remove('split_stay_' . $order[0]['id']);
2822 }
2823
2824 // Booking History
2825 $history_obj = VikBooking::getBookingHistoryInstance()->setBid($order[0]['id']);
2826 $history_obj->store('CW', $preason);
2827
2828 /**
2829 * Check if the amount paid can be refunded.
2830 *
2831 * @since 1.14 (J) - 1.4.0 (WP)
2832 */
2833 $admin_refund_error = '';
2834 $currencysymb = VikBooking::getCurrencySymb();
2835 $payment = VikBooking::getPayment($order[0]['idpayment']);
2836 $tn_driver = is_array($payment) ? $payment['file'] : null;
2837
2838 // transaction data validation callback
2839 $tn_data_callback = function($data) use ($tn_driver) {
2840 return (is_object($data) && isset($data->driver) && basename($data->driver, '.php') == basename($tn_driver, '.php'));
2841 };
2842 // get previous transactions
2843 $prev_tn_data = $history_obj->getEventsWithData(array('P0', 'PN'), $tn_data_callback);
2844
2845 if (is_array($prev_tn_data) && count($prev_tn_data) && $order[0]['totpaid'] > 0) {
2846 // previous transactions found and total paid > 0
2847 $refund_amount = $order[0]['totpaid'];
2848
2849 // push refund information for the payment gateway
2850 $order[0]['total_to_refund'] = $refund_amount;
2851 $order[0]['transaction'] = $prev_tn_data;
2852 $order[0]['refund_reason'] = $preason;
2853
2854 // push the transaction currency information
2855 $order[0]['transaction_currency'] = VikBooking::getCurrencyCodePp();
2856
2857 /**
2858 * Trigger event to allow third-party plugins to manipulate the transaction data.
2859 *
2860 * @since 1.18.5 (J) - 1.8.5 (WP)
2861 */
2862 VBOFactory::getPlatform()->getDispatcher()->trigger('onInitRefundTransaction', [&$order[0], &$payment['params']]);
2863
2864 /**
2865 * @wponly The payment gateway is loaded
2866 * through the apposite dispatcher.
2867 */
2868 JLoader::import('adapter.payment.dispatcher');
2869 $obj = JPaymentDispatcher::getInstance('vikbooking', $payment['file'], $order[0], $payment['params']);
2870
2871 // check if refund is supported by this gateway
2872 if (method_exists($obj, 'isRefundSupported') && $obj->isRefundSupported()) {
2873 // perform the refund transaction
2874 $array_result = $obj->refund();
2875
2876 if ($array_result['verified'] != 1) {
2877 // refund failed
2878 $admin_refund_error .= "\nRefund transaction failed\n";
2879 // get the refund error message
2880 $admin_refund_error .= !empty($array_result['log']) && is_string($array_result['log']) ? $array_result['log'] : '';
2881 } else {
2882 // refund was successful
2883
2884 /**
2885 * The history event extra data will contain the "amount_paid" (refunded).
2886 *
2887 * @since 1.16.9 (J) - 1.6.9 (WP)
2888 */
2889 if (!empty($array_result['tot_paid'])) {
2890 // overwrite the requested amount with the returned one
2891 $refund_amount = (float)$array_result['tot_paid'];
2892 }
2893 $history_obj->setExtraData([
2894 'amount_paid' => $refund_amount,
2895 ]);
2896
2897 // update total paid, total and refund columns for the booking
2898 $booking = new stdClass;
2899 $booking->id = $order[0]['id'];
2900 if ($order[0]['totpaid'] > 0) {
2901 $booking->totpaid = (float)($order[0]['totpaid'] - $refund_amount);
2902 }
2903 if ($order[0]['total'] > 0) {
2904 $booking->total = (float)($order[0]['total'] - $refund_amount);
2905 }
2906 $booking->refund = (float)$order[0]['refund'] + $refund_amount;
2907 // update record in db
2908 $dbo->updateObject('#__vikbooking_orders', $booking, 'id');
2909
2910 // store the refund event
2911 $event_descr = [
2912 '(' . $payment['name'] . ')',
2913 $currencysymb . ' ' . VikBooking::numberFormat($refund_amount),
2914 ];
2915 $history_obj->store('RF', implode("\n", $event_descr));
2916 }
2917 }
2918 }
2919
2920 // invoke VikChannelManager
2921 if (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . "helpers" . DIRECTORY_SEPARATOR . "synch.vikbooking.php")) {
2922 $vcm_obj = VikBooking::getVcmInvoker();
2923 $vcm_obj->setOids(array($order[0]['id']))->setSyncType('cancel');
2924 $vcm_obj->doSync();
2925 }
2926 // end invoke VikChannelManager
2927
2928 //send email to the administrator
2929 $subject = JText::translate('VBOBOOKCANCELLEDEMAILSUBJ');
2930 // @wponly we do not need to pass the "best item id"
2931 $uri = VikBooking::externalroute("index.php?option=com_vikbooking&view=booking&sid=" . $order[0]['sid'] . "&ts=" . $order[0]['ts'], false);
2932 $msg = JText::sprintf('VBOBOOKCANCELLEDEMAILHEAD', $order[0]['id'], $uri) . "\n\n" . $preason . $admin_refund_error;
2933 $vbo_app = VikBooking::getVboApplication();
2934 $adsendermail = VikBooking::getSenderMail();
2935 $vbo_app->sendMail($adsendermail, $adsendermail, $to, $pemail, $subject, $msg, false);
2936
2937 // SMS
2938 VikBooking::sendBookingSMS($order[0]['id']);
2939
2940 // send cancellation email notification to guest
2941 VikBooking::sendBookingEmail($order[0]['id'], ['guest']);
2942
2943 // go back to the booking details page to show the new status
2944 $mainframe->enqueueMessage(JText::translate('VBOBOOKCANCELLEDRESP'));
2945 $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));
2946 } else {
2947 VikError::raiseWarning('', JText::translate('VBOERRMISSDATA'));
2948 $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));
2949 }
2950 } else {
2951 $mainframe->redirect("index.php");
2952 }
2953 } else {
2954 $mainframe->redirect("index.php");
2955 }
2956 }
2957
2958 public function cancelmodification()
2959 {
2960 $psid = VikRequest::getString('sid', '', 'request');
2961 $pidorder = VikRequest::getString('id', '', 'request');
2962 $dbo = JFactory::getDBO();
2963 $session = JFactory::getSession();
2964 $mainframe = JFactory::getApplication();
2965 if (!empty($psid) && !empty($pidorder)) {
2966 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=".intval($pidorder)." AND `sid`=".$dbo->quote($psid)." AND `status`='confirmed';";
2967 $dbo->setQuery($q);
2968 $dbo->execute();
2969 if ($dbo->getNumRows() == 1) {
2970 $order = $dbo->loadAssocList();
2971 //unset the session value and redirect
2972 $session->set('vboModBooking', '');
2973 $mainframe->redirect(JRoute::rewrite("index.php?option=com_vikbooking&view=booking&sid=".$order[0]['sid']."&ts=".$order[0]['ts'], false));
2974 } else {
2975 $mainframe->redirect("index.php");
2976 }
2977 } else {
2978 $mainframe->redirect("index.php");
2979 }
2980 }
2981
2982 public function tac_av_l()
2983 {
2984 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'tac.vikbooking.php');
2985
2986 //Channel Rates Module
2987 $pvbomodule = VikRequest::getInt('vbomodule', 0, 'request');
2988 $pshow_tax = VikRequest::getInt('show_tax', 0, 'request');
2989 $pdef_rplan = VikRequest::getInt('def_rplan', 0, 'request');
2990 $pchannels_sel = VikRequest::getVar('channels_sel', array());
2991 $pcheckin = VikRequest::getString('checkin', '', 'request');
2992 $pcheckout = VikRequest::getString('checkout', '', 'request');
2993 if ($pvbomodule > 0 && !empty($pcheckin) && !empty($pcheckout)) {
2994 //this is an ajax request, probably made by the module Vik Booking Channel Rates
2995 //we need to prepare some variables before calling the method.
2996 $start_date = date('Y-m-d', VikBooking::getDateTimestamp($pcheckin, 12, 0));
2997 $end_date = date('Y-m-d', VikBooking::getDateTimestamp($pcheckout, 10, 0));
2998 //set (only some) request variables (the rest is sent via Ajax)
2999 VikRequest::setVar('e4jauth', md5('vbo.e4j.vbo'));
3000 VikRequest::setVar('req_type', 'hotel_availability');
3001 VikRequest::setVar('start_date', $start_date);
3002 VikRequest::setVar('end_date', $end_date);
3003 //make call to get the result
3004 TACVBO::$getArray = true;
3005 $website_rates = TACVBO::tac_av_l([
3006 // always force adults and children to be injected as arguments to avoid request conflicts
3007 'adults' => VikRequest::getVar('adults', array()),
3008 'children' => VikRequest::getVar('children', array()) ?: [0],
3009 ]);
3010 //validate response
3011 if (!is_array($website_rates)) {
3012 //error returned
3013 echo json_encode(array('e4j.error' => $website_rates));
3014 exit;
3015 }
3016 if (is_array($website_rates) && isset($website_rates['e4j.error'])) {
3017 //another type of error returned
3018 echo json_encode($website_rates);
3019 exit;
3020 }
3021 if (is_array($website_rates) && !(count($website_rates) > 0)) {
3022 //empty response
3023 echo json_encode(array('e4j.error' => 'empty response'));
3024 exit;
3025 }
3026 //get the list of channels connected, filtered by ID
3027 $channels_map = VikBooking::getChannelsMap($pchannels_sel);
3028 //get the array with the lowest and preferred room rate
3029 $best_room_rate = VikBooking::getBestRoomRate($website_rates, $pdef_rplan);
3030 //get the charge/discount value for the OTAs rates from the Bulk Rates Cache of VCM
3031 $otas_rates_val = VikBooking::getOtasRatesVal($best_room_rate, true);
3032
3033 $otas_rmod = '';
3034 $otas_rmodpcent = 0;
3035 $otas_rmodval = 0;
3036 $otas_rmod_channels = array();
3037 if (!empty($otas_rates_val)) {
3038 if (is_array($otas_rates_val)) {
3039 $otas_rmod_channels = $otas_rates_val;
3040 $use_rates_val = $otas_rates_val[0];
3041 } else {
3042 // string
3043 $use_rates_val = $otas_rates_val;
3044 }
3045 $otas_rmod = substr($use_rates_val, 0, 1); // + or - (charge or discount)
3046 $otas_rmodpcent = substr($use_rates_val, -1) == '%' ? 1 : 0;
3047 $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)));
3048 }
3049 if (!count($best_room_rate)) {
3050 // nothing to parse
3051 echo json_encode(array('e4j.error' => 'no rates'));
3052 exit;
3053 }
3054 // build the response
3055 $final_cost = $pshow_tax > 0 ? ($best_room_rate['cost'] + $best_room_rate['taxes']) : $best_room_rate['cost'];
3056 $rates_resp = array(
3057 'website' => VikBooking::numberFormat($final_cost)
3058 );
3059 if (count($channels_map)) {
3060 $rates_resp['channels'] = array();
3061 }
3062 foreach ($channels_map as $ch) {
3063 $ch_final_cost = $final_cost;
3064
3065 /**
3066 * Check if an alteration for this channel has been specified.
3067 *
3068 * @since 1.15.0 (J) - 1.5.0 (WP)
3069 */
3070 $use_otas_rmod = $otas_rmod;
3071 $use_otas_rmodpcent = $otas_rmodpcent;
3072 $use_otas_rmodval = $otas_rmodval;
3073 if (is_array($otas_rmod_channels) && isset($otas_rmod_channels[$ch['id']])) {
3074 $use_rates_val = $otas_rmod_channels[$ch['id']];
3075 $use_otas_rmod = substr($use_rates_val, 0, 1); //+ or - (charge or discount)
3076 $use_otas_rmodpcent = substr($use_rates_val, -1) == '%' ? 1 : 0;
3077 $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)));
3078 }
3079
3080 if (!empty($use_otas_rmod)) {
3081 if ($use_otas_rmod == '+') {
3082 // charge
3083 if ($use_otas_rmodpcent > 0) {
3084 // percentage
3085 $ch_final_cost = $ch_final_cost * (100 + $use_otas_rmodval) / 100;
3086 } else {
3087 // absolute
3088 $ch_final_cost += $use_otas_rmodval * (!empty($best_room_rate['days']) ? $best_room_rate['days'] : 1);
3089 }
3090 } else {
3091 // discount (must be a fool)
3092 if ($use_otas_rmodpcent > 0) {
3093 // percentage
3094 $ch_final_cost = $ch_final_cost / (($use_otas_rmodval / 100) + 1);
3095 } else {
3096 // absolute
3097 $ch_final_cost -= $use_otas_rmodval * (!empty($best_room_rate['days']) ? $best_room_rate['days'] : 1);
3098 }
3099 }
3100 }
3101
3102 $rates_resp['channels'][$ch['id']] = VikBooking::numberFormat($ch_final_cost);
3103 }
3104 // output the response
3105 echo json_encode($rates_resp);
3106 exit;
3107 }
3108
3109 // proceed with the standard request (that will exit the process)
3110 TACVBO::tac_av_l();
3111 }
3112
3113 /**
3114 * Front-end authentication for the operators
3115 * through their authentication code.
3116 *
3117 * @since 1.11
3118 */
3119 public function operatorlogin()
3120 {
3121 /**
3122 * Secure the login form token.
3123 */
3124 if (!JSession::checkToken()) {
3125 // missing CSRF-proof token
3126 throw new Exception('The security token did not match.', 403);
3127 }
3128
3129 $app = JFactory::getApplication();
3130 $pauthcode = VikRequest::getString('authcode', '', 'request');
3131 $pitemid = VikRequest::getInt('Itemid', '', 'request');
3132 /**
3133 * We add "&auth=1" to the query string just to avoid caching for a redirect to the same login page URI.
3134 *
3135 * @since September 9th 2020
3136 */
3137 $goto = JRoute::rewrite('index.php?option=com_vikbooking&view=operators&auth=1'.(!empty($pitemid) ? '&Itemid='.$pitemid : ''), false);
3138
3139 if (empty($pauthcode) || !VikBooking::getOperatorInstance()->authOperator($pauthcode)) {
3140 // print warning message
3141 VikError::raiseWarning('', JText::translate('VBOOPERINVAUTHCODE'));
3142 }
3143
3144 $app->redirect($goto);
3145 }
3146
3147 /**
3148 * Front-end logout for the operators.
3149 *
3150 * @since 1.11
3151 */
3152 public function operatorlogout()
3153 {
3154 $app = JFactory::getApplication();
3155 $pitemid = VikRequest::getInt('Itemid', '', 'request');
3156 $goto = JRoute::rewrite('index.php?option=com_vikbooking&view=operators'.(!empty($pitemid) ? '&Itemid='.$pitemid : ''), false);
3157
3158 VikBooking::getOperatorInstance()->logoutOperator();
3159
3160 $app->redirect($goto);
3161 }
3162
3163 /**
3164 * Front-end Pre Check-in submit of the guests details.
3165 *
3166 * @since 1.12
3167 */
3168 public function storeprecheckin()
3169 {
3170 if (!JSession::checkToken()) {
3171 // missing CSRF-proof token
3172 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
3173 }
3174
3175 $dbo = JFactory::getDbo();
3176 $app = JFactory::getApplication();
3177
3178 $sid = $app->input->getString('sid', '');
3179 $ts = $app->input->getString('ts', '');
3180 $pguests = $app->input->get('guests', [], 'array');
3181 $pitemid = $app->input->getInt('Itemid', 0);
3182
3183 $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';";
3184 $dbo->setQuery($q);
3185 $order = $dbo->loadAssoc();
3186 if (!$order) {
3187 throw new Exception('Booking not found', 404);
3188 }
3189
3190 $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;";
3191 $dbo->setQuery($q);
3192 $orderrooms = $dbo->loadAssocList();
3193 if (!$orderrooms) {
3194 throw new Exception('No rooms found', 404);
3195 }
3196
3197 // access the customer record
3198 $customer = VikBooking::getCPinInstance()->getCustomerFromBooking($order['id']);
3199
3200 $q = "SELECT * FROM `#__vikbooking_customers_orders` WHERE `idorder`=".(int)$order['id'].";";
3201 $dbo->setQuery($q);
3202 $custorder = $dbo->loadAssoc();
3203 if (!$custorder) {
3204 throw new Exception('No customer found', 404);
3205 }
3206
3207 // booking details page
3208 $goto = JRoute::rewrite('index.php?option=com_vikbooking&view=booking&sid=' . $order['sid'] . '&ts=' . $order['ts'] . (!empty($pitemid) ? '&Itemid=' . $pitemid : ''), false);
3209 if (empty($order['sid']) && !empty($order['idorderota'])) {
3210 // booking details page for OTA bookings
3211 $goto = JRoute::rewrite('index.php?option=com_vikbooking&view=booking&sid=' . $order['idorderota'] . '&ts=' . $order['ts'] . (!empty($pitemid) ? '&Itemid=' . $pitemid : ''), false);
3212 }
3213
3214 // make sure pre-checkin is allowed
3215 $precheckin = VikBooking::precheckinEnabled();
3216 if ($precheckin) {
3217 // make sure the limit of days in advance is reflected
3218 $precheckin_mind = VikBooking::precheckinMinOffset();
3219 if ($precheckin_mind < 0) {
3220 // validation made prior to check-out date and time
3221 $precheckin = time() <= strtotime("{$precheckin_mind} days 23:59:59", $order['checkout']);
3222 } else {
3223 // classic validation prior to check-in date and time
3224 $precheckin_lim_ts = strtotime("+{$precheckin_mind} days 00:00:00");
3225 $precheckin = ($precheckin_lim_ts <= $order['checkin'] || ($precheckin_mind === 1 && time() <= $order['checkin']));
3226 }
3227 }
3228 if (!$precheckin) {
3229 // raise error and redirect in case of website or OTA booking
3230 VikError::raiseWarning('', 'Pre-checkin not allowed at this time');
3231 $app->redirect($goto);
3232 exit;
3233 }
3234
3235 // build guest details
3236 $guests_details = array();
3237
3238 // list of keys for the guests details collected via front-end
3239 $front_keys = array();
3240
3241 foreach ($pguests as $ind => $adults) {
3242 foreach ($adults as $aduind => $details) {
3243 foreach ($details as $detkey => $detval) {
3244 if (!in_array($detkey, $front_keys)) {
3245 // push the key of the guest details for later comparison
3246 array_push($front_keys, $detkey);
3247 }
3248 if (strlen($detval)) {
3249 // push value only if not empty
3250 if (!isset($guests_details[$ind])) {
3251 $guests_details[$ind] = array();
3252 }
3253 if (!isset($guests_details[$ind][$aduind])) {
3254 $guests_details[$ind][$aduind] = array();
3255 }
3256 $guests_details[$ind][$aduind][$detkey] = $detval;
3257 }
3258 }
3259 }
3260 }
3261
3262 /**
3263 * Compare the current data collected to the back-end pax_data in case there are some
3264 * fields dedicated to just the back-end for the admins (like extra_notes), and merge.
3265 */
3266 $curpaxdata = json_decode($custorder['pax_data'], true);
3267 if (is_array($curpaxdata) && count($curpaxdata)) {
3268 foreach ($curpaxdata as $ind => $adults) {
3269 if (!isset($guests_details[$ind])) {
3270 // current pax data include a room not present, set it to not lose it
3271 $guests_details[$ind] = $adults;
3272 }
3273 foreach ($adults as $aduind => $details) {
3274 if (!isset($guests_details[$ind][$aduind])) {
3275 // current pax data include a guest not present, set it to not lose it
3276 $guests_details[$ind][$aduind] = $details;
3277 }
3278 foreach ($details as $detkey => $detval) {
3279 if (!in_array($detkey, $front_keys)) {
3280 // merge this key probably reserved to the back-end
3281 $guests_details[$ind][$aduind][$detkey] = $detval;
3282 }
3283 }
3284 }
3285 }
3286 }
3287
3288 // update checkin information
3289 $q = "UPDATE `#__vikbooking_customers_orders` SET `pax_data`=".$dbo->quote(json_encode($guests_details))." WHERE `id`=".(int)$custorder['id'].";";
3290 $dbo->setQuery($q);
3291 $dbo->execute();
3292
3293 // Booking History
3294 VikBooking::getBookingHistoryInstance()->setBid($order['id'])->store('PC');
3295
3296 /**
3297 * Invoke the callback on the pax registration driver.
3298 *
3299 * @since 1.17.5 (J) - 1.7.5 (WP)
3300 */
3301 VBOCheckinPax::callbackPrecheckinDataStored(
3302 VBOFactory::getConfig()->getString('checkindata', 'basic'),
3303 (array) $guests_details,
3304 (array) $order,
3305 (array) $customer
3306 );
3307
3308 // print success message and redirect
3309 $app->enqueueMessage(JText::translate('VBOSUBMITPRECHECKINTNKS'));
3310 $app->redirect($goto);
3311 }
3312
3313 /**
3314 * Upsell extra services/options.
3315 *
3316 * @since 1.13 (J) - 1.3.0 (WP)
3317 */
3318 public function upsellextras()
3319 {
3320 $dbo = JFactory::getDbo();
3321 $app = JFactory::getApplication();
3322 $sid = VikRequest::getString('sid', '', 'request');
3323 $ts = VikRequest::getString('ts', '', 'request');
3324 $pitemid = VikRequest::getInt('Itemid', 0, 'request');
3325 $paddopt = VikRequest::getVar('addopt', array());
3326
3327 if (!$paddopt) {
3328 throw new Exception('No extra services selected', 404);
3329 }
3330
3331 // find the involved reservation, direct or OTA
3332 $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) . ";";
3333 $dbo->setQuery($q);
3334 $order = $dbo->loadAssoc();
3335 if (!$order) {
3336 throw new Exception('Booking not found', 404);
3337 }
3338
3339 // obtain the involved rooms (include room "params")
3340 $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;";
3341 $dbo->setQuery($q);
3342 $orderrooms = $dbo->loadAssocList();
3343 if (!$orderrooms) {
3344 throw new Exception('No rooms found', 404);
3345 }
3346
3347 // availability helper
3348 $av_helper = VikBooking::getAvailabilityInstance();
3349
3350 // room stay dates in case of split stay
3351 $room_stay_dates = [];
3352 if ($order['split_stay']) {
3353 if ($order['status'] == 'confirmed') {
3354 $room_stay_dates = $av_helper->loadSplitStayBusyRecords($order['id']);
3355 } else {
3356 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $order['id'], []);
3357 }
3358 }
3359
3360 // load all valid and existing options as a security measure
3361 $alloptions = [];
3362 $q = "SELECT * FROM `#__vikbooking_optionals`;";
3363 $dbo->setQuery($q);
3364 $records = $dbo->loadAssocList();
3365 if (!$records) {
3366 throw new Exception('No options found', 404);
3367 }
3368 foreach ($records as $v) {
3369 $alloptions[$v['id']] = $v;
3370 }
3371
3372 /**
3373 * Custom check-in/out times due to late check-out/early check-in options or custom listing settings.
3374 *
3375 * @since 1.17.2 (J) - 1.7.2 (WP)
3376 * @since 1.18.3 (J) - 1.8.3 (WP) added support to check-in/out times at listing-level.
3377 */
3378 $custom_checkinout = [];
3379 if (count($orderrooms) === 1) {
3380 $listing_custom_checkin = VikBooking::getRoomParam('checkin', ($orderrooms[(key($orderrooms))]['room_params'] ?? ''));
3381 $listing_custom_checkout = VikBooking::getRoomParam('checkout', ($orderrooms[(key($orderrooms))]['room_params'] ?? ''));
3382 if ($listing_custom_checkin) {
3383 // set listing-level check-in time in seconds
3384 $listing_custom_checkin_parts = explode(':', $listing_custom_checkin);
3385 $listing_custom_checkin = (intval($listing_custom_checkin_parts[0]) * 3600) + (intval($listing_custom_checkin_parts[1]) * 60);
3386 }
3387 if ($listing_custom_checkout) {
3388 // set listing-level check-out time in seconds
3389 $listing_custom_checkout_parts = explode(':', $listing_custom_checkout);
3390 $listing_custom_checkout = (intval($listing_custom_checkout_parts[0]) * 3600) + (intval($listing_custom_checkout_parts[1]) * 60);
3391 }
3392 if ($listing_custom_checkin || $listing_custom_checkout) {
3393 // set listing-level check-in/out times in seconds
3394 $custom_checkinout = [
3395 (int) $listing_custom_checkin,
3396 (int) $listing_custom_checkout,
3397 ];
3398 }
3399 }
3400
3401 /**
3402 * Gather the damage deposit options to be paid separately.
3403 *
3404 * @since 1.18.6 (J) - 1.8.6 (WP)
3405 */
3406 $separate_dd_options = [];
3407
3408 // build the extras booked
3409 $extras_booked = [];
3410 foreach ($orderrooms as $kor => $or) {
3411 if (!isset($paddopt[$kor]) || !$paddopt[$kor]) {
3412 continue;
3413 }
3414
3415 // determine proper nights of stay
3416 $room_stay_nights = $order['days'];
3417 if ($order['split_stay'] && count($room_stay_dates) && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
3418 $room_stay_checkin = !empty($room_stay_dates[$kor]['checkin_ts']) ? $room_stay_dates[$kor]['checkin_ts'] : $room_stay_dates[$kor]['checkin'];
3419 $room_stay_checkout = !empty($room_stay_dates[$kor]['checkout_ts']) ? $room_stay_dates[$kor]['checkout_ts'] : $room_stay_dates[$kor]['checkout'];
3420 $room_stay_nights = $av_helper->countNightsOfStay($room_stay_checkin, $room_stay_checkout);
3421 }
3422
3423 $extraoptstr = '';
3424 foreach ($paddopt[$kor] as $optid => $quant) {
3425 if (strpos($or['optionals'], $optid . ':') === 0 || strpos($or['optionals'], ';' . $optid . ':') > 0) {
3426 // this option has already been booked, skip it
3427 continue;
3428 }
3429 if (!isset($alloptions[$optid])) {
3430 // this option ID does not exist, skip it
3431 continue;
3432 }
3433 $extraoptstr .= $optid . ':' . (int)$quant . ';';
3434
3435 // option params
3436 $opt_params = !empty($alloptions[$optid]['oparams']) ? (array) json_decode($alloptions[$optid]['oparams'], true) : [];
3437
3438 /**
3439 * Custom check-in/out times due to late check-out/early check-in options.
3440 *
3441 * @since 1.17.2 (J) - 1.7.2 (WP)
3442 */
3443 if (($opt_params['custom_checkinout'] ?? 0) && (($opt_params['set_checkin'] ?? 0) || ($opt_params['set_checkout'] ?? 0))) {
3444 $custom_checkinout = [
3445 ($opt_params['set_checkin'] ?? 0),
3446 ($opt_params['set_checkout'] ?? 0),
3447 ];
3448 }
3449
3450 /**
3451 * Check if this is a damage deposit with a separate payment window.
3452 *
3453 * @since 1.18.6 (J) - 1.8.6 (WP)
3454 */
3455 if (($opt_params['damagedep'] ?? 0) && !empty($opt_params['damagedep_settings']['paywhen'])) {
3456 // damage deposit option with separate payment defined
3457 $future_payable_dd = true;
3458 if (!empty($opt_params['damagedep_settings']['bmaxlos']) && ($order['days'] ?? 1) > $opt_params['damagedep_settings']['bmaxlos']) {
3459 // maximum nights of stay validation failed
3460 $future_payable_dd = false;
3461 } elseif (empty($opt_params['damagedep_settings']['payid'])) {
3462 // separate payment method ID validation failed
3463 $future_payable_dd = false;
3464 }
3465 if ($future_payable_dd === true) {
3466 // push damage deposit option ID to be paid separately
3467 $separate_dd_options[] = (int) $optid;
3468 }
3469 }
3470
3471 // push option booked
3472 $extras_booked[] = [
3473 'id' => $optid,
3474 'idroom' => $or['idroom'],
3475 'name' => $alloptions[$optid]['name'],
3476 'quant' => $quant,
3477 'room_cost' => (!empty($or['cust_cost']) ? $or['cust_cost'] : $or['room_cost']),
3478 'room_name' => $or['room_name'],
3479 'optcost' => $alloptions[$optid]['cost'],
3480 'adults' => $or['adults'],
3481 'children' => $or['children'],
3482 'nights' => $room_stay_nights,
3483 ];
3484 }
3485
3486 // update options for this room record
3487 $newoptstr = $or['optionals'] . $extraoptstr;
3488 $q = "UPDATE `#__vikbooking_ordersrooms` SET `optionals`=" . $dbo->quote($newoptstr) . " WHERE `id`={$or['id']};";
3489 $dbo->setQuery($q);
3490 $dbo->execute();
3491 }
3492
3493 // increase booking total amount and build event log for the history
3494 $currency = VikBooking::getCurrencySymb();
3495 $totrooms = count($orderrooms);
3496 $increase = 0;
3497 $future_dd = 0;
3498 $add_tax = 0;
3499 $extraslog = [];
3500 foreach ($extras_booked as $extra) {
3501 $o = $alloptions[(int)$extra['id']];
3502 if ((int)$o['pcentroom']) {
3503 // make sure we have a cost for the room, or we should skip this type of option for "incomplete" bookings
3504 if (empty($extra['room_cost'])) {
3505 continue;
3506 }
3507 $o['cost'] = ($extra['room_cost'] * $o['cost'] / 100);
3508 }
3509 $optcost = intval($o['perday']) == 1 ? ($o['cost'] * $extra['nights']) : $o['cost'];
3510 if (!empty($o['maxprice']) && $o['maxprice'] > 0 && $optcost > $o['maxprice']) {
3511 $optcost = $o['maxprice'];
3512 }
3513 if ($o['perperson'] == 1) {
3514 $optcost = $optcost * $extra['adults'];
3515 }
3516 $optcost *= $extra['quant'];
3517
3518 /**
3519 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
3520 *
3521 * @since 1.17.7 (J) - 1.7.7 (WP)
3522 */
3523 $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$optcost, &$o, $order, $extra]);
3524 if ($custom_calculation) {
3525 $optcost = (float) $custom_calculation[0];
3526 }
3527
3528 $floatoptprice = VikBooking::sayOptionalsPlusIva($optcost, $o['idiva']);
3529 $netoptprice = VikBooking::sayOptionalsMinusIva($optcost, $o['idiva']);
3530 $increase += $floatoptprice;
3531 $add_tax += $floatoptprice - $netoptprice;
3532 array_push($extraslog, ($totrooms > 1 ? $extra['room_name'] . ': ' : '') . $extra['name'] . ($extra['quant'] > 1 ? ' (x' . $extra['quant'] . ')' : '') . ' ' . $currency . ' ' . VikBooking::numberFormat($floatoptprice));
3533
3534 if (in_array((int) $extra['id'], $separate_dd_options)) {
3535 // this is a damage deposit option that will be paid separately
3536 $future_dd += $floatoptprice;
3537 }
3538 }
3539
3540 $newtotbooking = $order['total'] + $increase;
3541 $new_tot_taxes = $order['tot_taxes'] + $add_tax;
3542
3543 /**
3544 * Important: the 'paymcount' should be increased only if the status is
3545 * "confirmed" or no rooms may be occupied when receiving a payment.
3546 */
3547 $q = $dbo->getQuery(true)
3548 ->update($dbo->qn('#__vikbooking_orders'))
3549 ->set($dbo->qn('total') . ' = ' . $dbo->q($newtotbooking))
3550 ->set($dbo->qn('paymcount') . ' = ' . ($order['status'] == 'confirmed' && (int)$order['paymcount'] < 1 ? '1' : $order['paymcount']))
3551 ->set($dbo->qn('tot_taxes') . ' = ' . $dbo->q($new_tot_taxes))
3552 ->set($dbo->qn('payable') . ' = ' . $dbo->q(((float)$order['payable'] + $increase - $future_dd)))
3553 ->where($dbo->qn('id') . ' = ' . (int) $order['id']);
3554
3555 /**
3556 * Custom check-in/out times due to late check-out/early check-in options.
3557 *
3558 * @since 1.17.2 (J) - 1.7.2 (WP)
3559 */
3560 if ($custom_checkinout) {
3561 $checkin_info = getdate($order['checkin']);
3562 $checkout_info = getdate($order['checkout']);
3563 // overwrite check-in and/or check-out timestamp(s)
3564 if ($custom_checkinout[0] >= 3600) {
3565 // overwrite check-in timestamp
3566 $time_hours = floor($custom_checkinout[0] / 3600);
3567 $time_minutes = floor(($custom_checkinout[0] - ($time_hours * 3600)) / 60);
3568 $new_booking_checkin = mktime($time_hours, $time_minutes, 0, $checkin_info['mon'], $checkin_info['mday'], $checkin_info['year']);
3569 // update db record field
3570 $q->set($dbo->qn('checkin') . ' = ' . $dbo->q($new_booking_checkin));
3571 }
3572 if ($custom_checkinout[1] >= 3600) {
3573 // overwrite check-out timestamp
3574 $time_hours = floor($custom_checkinout[1] / 3600);
3575 $time_minutes = floor(($custom_checkinout[1] - ($time_hours * 3600)) / 60);
3576 $new_booking_checkout = mktime($time_hours, $time_minutes, 0, $checkout_info['mon'], $checkout_info['mday'], $checkout_info['year']);
3577 // update db record field
3578 $q->set($dbo->qn('checkout') . ' = ' . $dbo->q($new_booking_checkout));
3579 }
3580 }
3581
3582 // update booking record on db
3583 $dbo->setQuery($q);
3584 $dbo->execute();
3585
3586 // Booking History
3587 VikBooking::getBookingHistoryInstance($order['id'])->store('UE', implode("\n", $extraslog));
3588
3589 /**
3590 * Trigger event to allow third-party plugins to choose whether upselling notifications should be sent.
3591 *
3592 * @since 1.17.2 (J) - 1.7.2 (WP)
3593 */
3594 $send_notifications = true;
3595 $should_send = VBOFactory::getPlatform()->getDispatcher()->filter('onUpsellingReceivedShouldSendNotifications', [$order]);
3596 if (is_array($should_send) && in_array(false, $should_send, true)) {
3597 $send_notifications = false;
3598 }
3599
3600 if ($send_notifications) {
3601 // send email notification to guest and admin
3602 VikBooking::sendBookingEmail($order['id'], array('guest', 'admin'));
3603 }
3604
3605 $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);
3606 $app->enqueueMessage(JText::translate('VBOUPSELLRESULTOK'));
3607 $app->redirect($goto);
3608 $app->close();
3609 }
3610
3611 /**
3612 * Submits a new review.
3613 *
3614 * @since 1.3.0
3615 */
3616 public function sendreview()
3617 {
3618 $dbo = JFactory::getDbo();
3619 $app = JFactory::getApplication();
3620 $vbo_tn = VikBooking::getTranslator();
3621 $sid = VikRequest::getString('sid', '', 'request');
3622 $ts = VikRequest::getString('ts', '', 'request');
3623 $ratingmess = VikRequest::getString('ratingmess', '', 'request');
3624 $rating = VikRequest::getVar('rating', array(), 'request', 'array');
3625 $pitemid = VikRequest::getInt('Itemid', 0, 'request');
3626
3627 $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';";
3628 $dbo->setQuery($q);
3629 $dbo->execute();
3630 if (!$dbo->getNumRows()) {
3631 throw new Exception('Booking not found', 404);
3632 }
3633 $order = $dbo->loadAssoc();
3634
3635 // make sure a review can be left for this booking
3636 if (!VikBooking::canBookingBeReviewed($order)) {
3637 throw new Exception('Cannot leave a review at this time', 403);
3638 }
3639
3640 $orderrooms = array();
3641 $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;";
3642 $dbo->setQuery($q);
3643 $dbo->execute();
3644 if (!$dbo->getNumRows()) {
3645 throw new Exception('No rooms found', 404);
3646 }
3647 $orderrooms = $dbo->loadAssocList();
3648
3649 // get customer information
3650 $customer = VikBooking::getCPinIstance()->getCustomerFromBooking($order['id']);
3651
3652 // booking details page
3653 $goto = JRoute::rewrite('index.php?option=com_vikbooking&view=booking&sid=' . $order['sid'] . '&ts=' . $order['ts'] . (!empty($pitemid) ? '&Itemid=' . $pitemid : ''), false);
3654 if (empty($order['sid']) && !empty($order['idorderota'])) {
3655 // booking details page for OTA bookings
3656 $goto = JRoute::rewrite('index.php?option=com_vikbooking&view=booking&sid=' . $order['idorderota'] . '&ts=' . $order['ts'] . (!empty($pitemid) ? '&Itemid=' . $pitemid : ''), false);
3657 }
3658
3659 // reviews settings
3660 $gr_approval = VikBooking::guestReviewsApproval();
3661 $gr_type = VikBooking::guestReviewsType();
3662 $gr_services = VikBooking::guestReviewsServices();
3663 $rawservices = $gr_services;
3664 $vbo_tn->translateContents($gr_services, '#__vikbooking_greview_service');
3665
3666 // make sure all ratings are not empty
3667 if ($gr_type == 'global') {
3668 if (empty($rating[0]) || intval($rating[0]) < 1 || intval($rating[0]) > 5) {
3669 // no or invalid single-rating received
3670 VikError::raiseWarning('', 'Please rate your experience to leave a review.');
3671 $app->redirect($goto);
3672 exit;
3673 }
3674 } else {
3675 if (count($gr_services) != count($rating) || !count($rating)) {
3676 // something is missing
3677 VikError::raiseWarning('', 'Please rate your experience for all services');
3678 $app->redirect($goto);
3679 exit;
3680 }
3681 // make sure all ratings are valid
3682 foreach ($rating as $k => $score) {
3683 if (empty($score) || intval($score) < 1 || intval($score) > 5) {
3684 // no or invalid rating received for this service
3685 VikError::raiseWarning('', 'Please rate your experience to leave a review (missing ' . (isset($gr_services[$k]) ? $gr_services[$k]['service_name'] : '-----') . ').');
3686 $app->redirect($goto);
3687 exit;
3688 }
3689 }
3690 }
3691
3692 // average review score (in base 10) and services map
3693 $avg_score = 0;
3694 $serv_scores = array();
3695
3696 // gather the information
3697 foreach ($rating as $k => $score) {
3698 // rating in base 10
3699 $score = ((int)$score * 2);
3700 //
3701 $avg_score += $score;
3702 if ($gr_type == 'service' && isset($gr_services[$k]) && !empty($gr_services[$k]['service_name'])) {
3703 $skey = $gr_services[$k]['service_name'];
3704 $serv_scores[$skey] = $score;
3705 }
3706 }
3707 // this will be the review_score
3708 $avg_score = round(($avg_score / count($rating)), 2);
3709
3710 // build review content object
3711 $review_content = new stdClass;
3712
3713 // creation date
3714 $review_content->created_timestamp = date('Y-m-d H:i:s');
3715
3716 // scoring per service (if any)
3717 $review_content->scoring = new stdClass;
3718 if (count($serv_scores)) {
3719 $counter = 0;
3720 foreach ($serv_scores as $snametranx => $servscore) {
3721 // we build the object with the original names of the services, as they could have been translated
3722 $origskey = $rawservices[$counter]['service_name'];
3723 $review_content->scoring->{$origskey} = $servscore;
3724 $counter++;
3725 }
3726 }
3727 // scoring total value ("review_score" is a protected key) is added no matter of the review type (service/global)
3728 $review_content->scoring->review_score = $avg_score;
3729
3730 // reviewer information
3731 $review_content->reviewer = new stdClass;
3732 $customer_name = '';
3733 if ($customer) {
3734 $review_content->reviewer->name = $customer['first_name'];
3735 $review_content->reviewer->country_code = $customer['country'];
3736 $customer_name = $customer['first_name'] . ' ' . $customer['last_name'];
3737 } else {
3738 $revuname = '';
3739 if (!empty($order['custdata'])) {
3740 $uinfos = explode("\n", $order['custdata']);
3741 $first_info = explode(':', $uinfos[0]);
3742 if (count($first_info) > 1) {
3743 unset($first_info[0]);
3744 $revuname = implode(':', $first_info);
3745 } else {
3746 $revuname = trim($first_info[0]);
3747 }
3748 }
3749 $review_content->reviewer->name = $revuname;
3750 $review_content->reviewer->country_code = $order['country'];
3751 $customer_name = $revuname;
3752 }
3753
3754 // maximum 2000 chars for the message review to avoid spammers
3755 if (!empty($ratingmess) && strlen($ratingmess) > 2000) {
3756 $ratingmess = substr($ratingmess, 0, 2000);
3757 }
3758
3759 // review message
3760 $review_content->content = new stdClass;
3761 $review_content->content->message = !empty($ratingmess) ? $ratingmess : null;
3762
3763 // null reply
3764 $review_content->reply = null;
3765
3766 // check if multiple accounts to find the property name
3767 $property_name = null;
3768 $has_multiaccounts = false;
3769 $multi_map = array();
3770 $q = "SELECT * FROM `#__vikchannelmanager_roomsxref`;";
3771 $dbo->setQuery($q);
3772 $dbo->execute();
3773 if ($dbo->getNumRows()) {
3774 $xref_data = $dbo->loadAssocList();
3775 foreach ($xref_data as $xref) {
3776 if (empty($xref['prop_params'])) {
3777 continue;
3778 }
3779 if (!isset($multi_map[$xref['idchannel']])) {
3780 $multi_map[$xref['idchannel']] = array();
3781 }
3782 if (!isset($multi_map[$xref['idchannel']][$xref['prop_params']])) {
3783 $multi_map[$xref['idchannel']][$xref['prop_params']] = 0;
3784 }
3785 $multi_map[$xref['idchannel']][$xref['prop_params']]++;
3786 }
3787 foreach ($multi_map as $ch_id => $ch_params) {
3788 if (count($ch_params) > 1) {
3789 $has_multiaccounts = true;
3790 break;
3791 }
3792 }
3793 }
3794 if ($has_multiaccounts && (int)$order['roomsnum'] === 1) {
3795 // find the category name of the room booked (if any, and if one room booked)
3796 $q = "SELECT `idcat` FROM `#__vikbooking_rooms` WHERE `id`={$orderrooms[0]['idroom']};";
3797 $dbo->setQuery($q);
3798 $dbo->execute();
3799 if ($dbo->getNumRows()) {
3800 $allcats = $dbo->loadResult();
3801 if (!empty($allcats)) {
3802 $parts = explode(';', $allcats);
3803 if (count($parts) === 2) {
3804 // just one category, get the name of it
3805 $property_name = VikBooking::getCategoryName($parts[0]);
3806 }
3807 }
3808 }
3809 if (empty($property_name)) {
3810 // category not found, get the room name
3811 $property_name = $orderrooms[0]['name'];
3812 }
3813 }
3814
3815 // create record
3816 $review_record = new stdClass;
3817 $review_record->review_id = -1;
3818 $review_record->prop_first_param = null;
3819 $review_record->prop_name = $property_name;
3820 $review_record->channel = null;
3821 $review_record->uniquekey = 0;
3822 $review_record->idorder = $order['id'];
3823 $review_record->dt = JFactory::getDate()->toSql(true);
3824 $review_record->customer_name = $customer_name;
3825 $review_record->lang = JFactory::getLanguage()->getTag();
3826 $review_record->score = $avg_score;
3827 $review_record->country = $order['country'];
3828 $review_record->content = json_encode($review_content);
3829 $review_record->published = ($gr_approval == 'auto' ? 1 : 0);
3830
3831 // insert review
3832 if ($dbo->insertObject('#__vikchannelmanager_otareviews', $review_record, 'id')) {
3833 $app->enqueueMessage(JText::translate('VBOTHANKSREVIEWLEFT'));
3834 // Booking History
3835 VikBooking::getBookingHistoryInstance()->setBid($order['id'])->store('GR');
3836 } else {
3837 VikError::raiseWarning('', JText::translate('VBOREVIEWGENERROR'));
3838 }
3839
3840 // update global score for website and this property (if multiple accounts)
3841 $globscore_id = null;
3842 $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));
3843 $dbo->setQuery($q, 0, 1);
3844 $dbo->execute();
3845 if ($dbo->getNumRows()) {
3846 $globscore_id = $dbo->loadResult();
3847 }
3848 // select score for all reviews for the website and this account
3849 $services_scores = array();
3850 $services_revscount = array();
3851 $revs_count = 0;
3852 $super_tot = 0;
3853 $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)) . ";";
3854 $dbo->setQuery($q);
3855 $dbo->execute();
3856 if ($dbo->getNumRows()) {
3857 $all_scores = $dbo->loadAssocList();
3858 $revs_count += count($all_scores);
3859 foreach ($all_scores as $s) {
3860 $super_tot += $s['score'];
3861 // check if scores were given per service
3862 $s['content'] = json_decode($s['content'], true);
3863 if (isset($s['content']['scoring']) && count($s['content']['scoring']) > 1) {
3864 // review was left for services
3865 foreach ($s['content']['scoring'] as $sname => $sval) {
3866 if (!isset($services_scores[$sname])) {
3867 $services_scores[$sname] = 0;
3868 $services_revscount[$sname] = 0;
3869 }
3870 $services_scores[$sname] += $sval;
3871 $services_revscount[$sname]++;
3872 }
3873 }
3874 }
3875 }
3876 // global average score
3877 $revs_count = $revs_count > 0 ? $revs_count : 1;
3878 $glob_avg_score = ($super_tot / $revs_count);
3879 // services average score
3880 $glob_servs_avg_score = array();
3881 foreach ($services_scores as $sname => $val) {
3882 $services_revscount[$sname] = isset($services_revscount[$sname]) && $services_revscount[$sname] > 0 ? $services_revscount[$sname] : 1;
3883 $glob_servs_avg_score[$sname] = ($val / $services_revscount[$sname]);
3884 }
3885
3886 // build global score content
3887 $glob_score_content = new stdClass;
3888 $glob_score_content->review_score = new stdClass;
3889 $glob_score_content->review_score->score = $glob_avg_score;
3890 $glob_score_content->review_score->review_count = $revs_count;
3891 foreach ($glob_servs_avg_score as $sname => $val) {
3892 $glob_score_content->{$sname} = new stdClass;
3893 $glob_score_content->{$sname}->score = $val;
3894 $glob_score_content->{$sname}->review_count = $services_revscount[$sname];
3895 }
3896
3897 // build global score object
3898 $glob_score_obj = new stdClass;
3899 if (!empty($globscore_id)) {
3900 $glob_score_obj->id = $globscore_id;
3901 }
3902 $glob_score_obj->prop_first_param = null;
3903 $glob_score_obj->prop_name = $property_name;
3904 $glob_score_obj->channel = null;
3905 $glob_score_obj->uniquekey = 0;
3906 $glob_score_obj->last_updated = JFactory::getDate()->toSql(true);
3907 $glob_score_obj->score = round($glob_avg_score, 2);
3908 $glob_score_obj->content = json_encode($glob_score_content);
3909
3910 // update or create global score
3911 if (!empty($globscore_id)) {
3912 $dbo->updateObject('#__vikchannelmanager_otascores', $glob_score_obj, 'id');
3913 } else {
3914 $dbo->insertObject('#__vikchannelmanager_otascores', $glob_score_obj, 'id');
3915 }
3916
3917 // redirect to main view
3918 $app->redirect($goto);
3919 }
3920
3921 /**
3922 * AJAX task to get one monthly availability calendar of the room details page.
3923 *
3924 * @since 1.13.5
3925 */
3926 public function get_avcalendars_data()
3927 {
3928 $dbo = JFactory::getDbo();
3929 $rid = VikRequest::getInt('rid', 0, 'request');
3930 $direction = VikRequest::getString('direction', 'next', 'request');
3931 $fromdt = VikRequest::getString('fromdt', '', 'request');
3932 $nextdt = VikRequest::getString('nextdt', '', 'request');
3933 $prevdt = VikRequest::getString('prevdt', '', 'request');
3934
3935 // make sure vars are not empty
3936 if (empty($rid) || empty($direction) || empty($fromdt) || empty($nextdt) || empty($prevdt)) {
3937 /**
3938 * Search engines may follow this endpoint, so we have to exit with HTTP status code 200
3939 *
3940 * @since 1.16.0 (J) - 1.6.0 (WP)
3941 */
3942 VBOHttpDocument::getInstance()->json(['Invalid request variables']);
3943 }
3944
3945 // date format
3946 $vbo_df = VikBooking::getDateFormat();
3947 if ($vbo_df == "%d/%m/%Y") {
3948 $vbo_df = 'd/m/Y';
3949 } elseif ($vbo_df == "%m/%d/%Y") {
3950 $vbo_df = 'm/d/Y';
3951 } else {
3952 $vbo_df = 'Y/m/d';
3953 }
3954
3955 // configuration settings
3956 $numcalendars = VikBooking::numCalendars();
3957 $showpartlyres = VikBooking::showPartlyReserved();
3958 $showcheckinoutonly = VikBooking::showStatusCheckinoutOnly();
3959 $usepricecal = false;
3960 $inonout_allowed = true;
3961 $timeopst = VikBooking::getTimeOpenStore();
3962 if (is_array($timeopst)) {
3963 if ($timeopst[0] < $timeopst[1]) {
3964 // check-in not allowed on a day where there is already a check out (no arrivals/depatures on the same day)
3965 $inonout_allowed = false;
3966 }
3967 }
3968
3969 // week-days ordering
3970 $firstwday = (int)VikBooking::getFirstWeekDay();
3971 $days_labels = array(
3972 JText::translate('VBSUN'),
3973 JText::translate('VBMON'),
3974 JText::translate('VBTUE'),
3975 JText::translate('VBWED'),
3976 JText::translate('VBTHU'),
3977 JText::translate('VBFRI'),
3978 JText::translate('VBSAT')
3979 );
3980 $days_indexes = array();
3981 for ($i = 0; $i < 7; $i++) {
3982 $days_indexes[$i] = (6 - ($firstwday - $i) + 1) % 7;
3983 }
3984
3985 // first day timestamp of month to read in case of forward navigation
3986 $start_ts = strtotime($fromdt);
3987 $start_info = getdate($start_ts);
3988 if (!$start_ts || !$start_info) {
3989 VBOHttpDocument::getInstance()->close(500, 'Invalid date provided');
3990 }
3991 // backward navigation
3992 if ($direction == 'prev') {
3993 // we need to get the previous month
3994 $start_ts = mktime(0, 0, 0, ($start_info['mon'] - 1), 1, $start_info['year']);
3995 $start_info = getdate($start_ts);
3996 if (!$start_ts || !$start_info) {
3997 VBOHttpDocument::getInstance()->close(500, 'Invalid date calculated');
3998 }
3999 }
4000
4001 // make sure minimum date is respected
4002 $min_lim_ts = mktime(0, 0, 0, date('n'), 1, date('Y'));
4003 if ($start_ts < $min_lim_ts) {
4004 VBOHttpDocument::getInstance()->close(500, 'Dates in the past not allowed');
4005 }
4006
4007 // check the current next and prev dates to help the next AJAX navigations
4008 $nextnav_ts = strtotime($nextdt);
4009 $nextnav_info = getdate($nextnav_ts);
4010 if (!$nextnav_ts || !$nextnav_info) {
4011 VBOHttpDocument::getInstance()->close(500, 'Invalid next navigation date provided');
4012 }
4013 $prevnav_ts = strtotime($prevdt);
4014 $prevnav_info = getdate($prevnav_ts);
4015 if (!$prevnav_ts || !$prevnav_info) {
4016 VBOHttpDocument::getInstance()->close(500, 'Invalid prev navigation date provided');
4017 }
4018
4019 // make sure maximum date is respected
4020 $max_months_future = 12;
4021 $max_date_future = VikBooking::getMaxDateFuture($rid);
4022 if (!empty($max_date_future)) {
4023 $numlim = (int)substr($max_date_future, 1, (strlen($max_date_future) - 2));
4024 $numlim = $numlim < 1 ? 1 : $numlim;
4025 $quantlim = substr($max_date_future, -1, 1);
4026 if ($quantlim == 'm' || $quantlim == 'y') {
4027 $max_months_future = $numlim * ($quantlim == 'm' ? 1 : 12);
4028 $max_ts_future = strtotime("+{$max_months_future} months");
4029 $max_info = getdate($max_ts_future);
4030 $max_endts_future = mktime(23, 59, 59, $max_info['mon'], date('t', $max_info[0]), $max_info['year']);
4031 if ($start_ts > $max_endts_future) {
4032 VBOHttpDocument::getInstance()->close(500, 'Maximum date in the future exceeded');
4033 }
4034 }
4035 }
4036
4037 // get global property closing dates
4038 $cal_closing_dates = VikBooking::parseJsClosingDates();
4039 if (count($cal_closing_dates)) {
4040 foreach ($cal_closing_dates as $ccdk => $ccdv) {
4041 if (!(count($ccdv) == 2)) {
4042 continue;
4043 }
4044 $cal_closing_dates[$ccdk][0] = strtotime($ccdv[0]);
4045 $cal_closing_dates[$ccdk][1] = strtotime($ccdv[1]);
4046 }
4047 }
4048
4049 // load room details
4050 $q = "SELECT * FROM `#__vikbooking_rooms` WHERE `id`={$rid}";
4051 $dbo->setQuery($q, 0, 1);
4052 $dbo->execute();
4053 if (!$dbo->getNumRows()) {
4054 VBOHttpDocument::getInstance()->close(404, 'Room not found');
4055 }
4056 $room_details = $dbo->loadAssoc();
4057
4058 // get the future busy records
4059 $today_ts = mktime(0, 0, 0, date('n'), date('j'), date('Y'));
4060 $previousdayclass = '';
4061
4062 $q = "SELECT * FROM `#__vikbooking_busy` WHERE `idroom`={$room_details['id']} AND `checkout`>={$start_ts};";
4063 $dbo->setQuery($q);
4064 $busy = $dbo->loadAssocList();
4065
4066 // empty day element
4067 $empty_elem = new stdClass;
4068 $empty_elem->type = 'placeholder';
4069 $empty_elem->cont = '&nbsp;';
4070
4071 // build response container
4072 $calendars = array();
4073
4074 // prepare calendar object
4075 $calendar = new stdClass;
4076 $calendar->ts = $start_info[0];
4077 $calendar->mon = $start_info['mon'];
4078 $calendar->mday = $start_info['mday'];
4079 $calendar->year = $start_info['year'];
4080 $calendar->month = VikBooking::sayMonth($start_info['mon']);
4081 $calendar->wdays = array();
4082 for ($i = 0; $i < 7; $i++) {
4083 $d_ind = ($i + $firstwday) < 7 ? ($i + $firstwday) : ($i + $firstwday - 7);
4084 array_push($calendar->wdays, $days_labels[$d_ind]);
4085 }
4086
4087 // build the calendar rows by looping over the days of this month
4088 $calendar->rows = array();
4089
4090 // the row will contain all the placeholders and real days (7 elements at most)
4091 $row = array();
4092 $d_count = 0;
4093
4094 // first, we push empty dates placeholders for printing the first table cells (cells before the 1st of the month)
4095 for ($i = 0, $n = $days_indexes[$start_info['wday']]; $i < $n; $i++, $d_count++) {
4096 // push fake-day element (placeholder)
4097 array_push($row, $empty_elem);
4098 }
4099
4100 // start looping
4101 $loop_end_month = $start_info['mon'];
4102 while ($start_info['mon'] == $loop_end_month) {
4103 if ($d_count > 6) {
4104 // push the current row
4105 array_push($calendar->rows, $row);
4106
4107 // start a new row and reset cells counter
4108 $row = array();
4109 $d_count = 0;
4110 }
4111
4112 // build real-day element
4113 $elem = new stdClass;
4114 $elem->type = 'day';
4115 $elem->cont = $start_info['mday'] < 10 ? "0{$start_info['mday']}" : $start_info['mday'];
4116 $elem->dt = date($vbo_df, $start_info[0]);
4117 $elem->ymd = date('Y-m-d', $start_info[0]);
4118 $elem->ts = $start_info[0];
4119 $elem->class = 'vbtdfree';
4120 $elem->past_class = $start_info[0] < $today_ts ? ' vbtdpast' : '';
4121
4122 // check whether this day has got bookings
4123 $totfound = 0;
4124 $ischeckinday = false;
4125 $ischeckoutday = false;
4126 foreach ($busy as $b) {
4127 $info_in = getdate($b['checkin']);
4128 $checkin_ts = mktime(0, 0, 0, $info_in['mon'], $info_in['mday'], $info_in['year']);
4129 $info_out = getdate($b['checkout']);
4130 $checkout_ts = mktime(0, 0, 0, $info_out['mon'], $info_out['mday'], $info_out['year']);
4131 if ($start_info[0] >= $checkin_ts && $start_info[0] == $checkout_ts) {
4132 $ischeckoutday = true;
4133 }
4134 if ($start_info[0] >= $checkin_ts && $start_info[0] < $checkout_ts) {
4135 $totfound++;
4136 if ($start_info[0] == $checkin_ts) {
4137 $ischeckinday = true;
4138 }
4139 }
4140 }
4141 if ($totfound >= $room_details['units']) {
4142 $elem->class = "vbtdbusy";
4143 if ($ischeckinday && $showcheckinoutonly && !$usepricecal && $inonout_allowed && $previousdayclass != "vbtdbusy" && $previousdayclass != "vbtdbusy vbtdbusyforcheckin") {
4144 $elem->class = "vbtdbusy vbtdbusyforcheckin";
4145 } elseif ($ischeckinday && !$usepricecal && !$inonout_allowed && $previousdayclass != "vbtdbusy" && $previousdayclass != "vbtdbusy vbtdbusyforcheckin") {
4146 // check-out not allowed on a day where someone is already checking-in
4147 $elem->class = "vbtdbusy";
4148 }
4149 } elseif ($totfound > 0) {
4150 if ($showpartlyres) {
4151 $elem->class = "vbtdwarning";
4152 }
4153 } else {
4154 if ($ischeckoutday && !$usepricecal && $showcheckinoutonly && $inonout_allowed && !($room_details['units'] > 1)) {
4155 $elem->class = "vbtdbusy vbtdbusyforcheckout";
4156 } elseif ($ischeckoutday && !$usepricecal && !$inonout_allowed && !($room_details['units'] > 1)) {
4157 $elem->class = "vbtdbusy";
4158 }
4159 }
4160
4161 // check global closing dates
4162 if (count($cal_closing_dates)) {
4163 foreach ($cal_closing_dates as $closed_interval) {
4164 if ($start_info[0] >= $closed_interval[0] && $start_info[0] <= $closed_interval[1]) {
4165 $elem->class = "vbtdbusy";
4166 break;
4167 }
4168 }
4169 }
4170
4171 // push cell element and increase counter
4172 array_push($row, $elem);
4173 $d_count++;
4174
4175 // update previous day class
4176 $previousdayclass = $elem->class;
4177
4178 // go to next day
4179 $start_info = getdate(mktime(0, 0, 0, $start_info['mon'], ($start_info['mday'] + 1), $start_info['year']));
4180 }
4181
4182 if (count($row)) {
4183 // the last row still need to be pushed in the rows
4184 for ($i = $d_count; $i <= 6; $i++) {
4185 // fill last empty days
4186 array_push($row, $empty_elem);
4187 }
4188
4189 // push ending row
4190 array_push($calendar->rows, $row);
4191 }
4192
4193 // push this month's calendar object
4194 array_push($calendars, $calendar);
4195
4196 // check whether next request can navigate forward
4197 $can_nav_next = false;
4198 if ($direction == 'prev') {
4199 // we went prev, so we got to be able to go next
4200 $can_nav_next = true;
4201 } elseif ($direction == 'next' && strtotime("+{$max_months_future} months") > $start_info[0]) {
4202 // went next and max future date is still greater than next hypothetical month
4203 $can_nav_next = true;
4204 }
4205
4206 // check whether next request can navigate backward
4207 $can_nav_prev = false;
4208 if ($direction == 'next') {
4209 // we went next, so we got to be able to go prev
4210 $can_nav_prev = true;
4211 } elseif ($direction == 'prev' && $min_lim_ts < mktime(0, 0, 0, ($start_info['mon'] - 1), 1, $start_info['year'])) {
4212 // went prev and first day of today's month is less than (not equal to) the last month just rendered
4213 $can_nav_prev = true;
4214 }
4215
4216 // calculate next and prev dates for the next navigations to help the AJAX request
4217 $next_ymd = null;
4218 if ($can_nav_next) {
4219 if ($direction == 'next') {
4220 // next forward navigation will start from the ne month
4221 $next_ymd = date('Y-m-d', $start_info[0]);
4222 } else {
4223 // we add the total number of calendars to the month after the one lastly displayed
4224 $next_ymd = date('Y-m-d', strtotime("+" . ($numcalendars - 1) . " months", $start_info[0]));
4225 }
4226 }
4227 $prev_ymd = null;
4228 if ($can_nav_prev) {
4229 if ($direction == 'prev') {
4230 // next backward navigation will start from the last month we just displayed
4231 $prev_ymd = date('Y-m-d', mktime(0, 0, 0, ($start_info['mon'] - 1), 1, $start_info['year']));
4232 } else {
4233 // we add the total number of calendars to the month after the one lastly displayed
4234 $prev_ymd = date('Y-m-d', strtotime("-{$numcalendars} months", $start_info[0]));
4235 }
4236 }
4237
4238 // build response object
4239 $response = new stdClass;
4240 $response->calendars = $calendars;
4241 $response->can_nav_next = $can_nav_next;
4242 $response->can_nav_prev = $can_nav_prev;
4243 $response->next_ymd = $next_ymd;
4244 $response->prev_ymd = $prev_ymd;
4245
4246 echo json_encode($response);
4247 exit;
4248 }
4249
4250 /**
4251 * AJAX request for adding a new room-day note from the front-end tableaux.
4252 *
4253 * @return void
4254 *
4255 * @since 1.13.5
4256 */
4257 public function add_roomdaynote()
4258 {
4259 if (!JSession::checkToken()) {
4260 // missing CSRF-proof token
4261 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
4262 }
4263
4264 $dt = VikRequest::getString('dt', '', 'request');
4265 $idroom = VikRequest::getInt('idroom', 0, 'request');
4266 $subunit = VikRequest::getInt('subunit', 0, 'request');
4267 $type = VikRequest::getString('type', '', 'request');
4268 $type = empty($type) ? 'custom' : $type;
4269 $name = VikRequest::getString('name', '', 'request');
4270 $descr = VikRequest::getString('descr', '', 'request');
4271 $cdays = VikRequest::getInt('cdays', 0, 'request');
4272 $cdays = $cdays < 0 ? 0 : $cdays;
4273 $cdays = $cdays > 365 ? 365 : $cdays;
4274 if (empty($idroom) || empty($dt) || !strtotime($dt)) {
4275 echo 'e4j.error.1';
4276 exit;
4277 }
4278
4279 // we put the operator name in the description (if available)
4280 $operator = VikBooking::getOperatorInstance()->getOperatorAccount();
4281 if ($operator !== false) {
4282 $oper_signature = "({$operator['first_name']} {$operator['last_name']})";
4283 $descr = empty($descr) ? $oper_signature : $descr . " \n" . $oper_signature;
4284 }
4285
4286 // reload end date
4287 $end_date = $dt;
4288
4289 // build critical date object
4290 $new_note = array(
4291 'name' => $name,
4292 'type' => $type,
4293 'descr' => $descr,
4294 );
4295
4296 // get object
4297 $notes = VikBooking::getCriticalDatesInstance();
4298
4299 // store the notes for all consecutive dates
4300 for ($i = 0; $i <= $cdays; $i++) {
4301 $store_dt = $dt;
4302 if ($i > 0) {
4303 $dt_info = getdate(strtotime($store_dt));
4304 $store_dt = date('Y-m-d', mktime(0, 0, 0, $dt_info['mon'], ($dt_info['mday'] + $i), $dt_info['year']));
4305 $end_date = $store_dt;
4306 }
4307 $result = $notes->storeDayNote($new_note, $store_dt, $idroom, $subunit);
4308 if (!$result) {
4309 echo 'e4j.error.2';
4310 exit;
4311 }
4312 }
4313
4314 // reload all room day notes for this day for the AJAX response
4315 $all_notes = $notes->loadRoomDayNotes($dt, $end_date, $idroom, $subunit);
4316
4317 if (!$all_notes || !count($all_notes)) {
4318 // no notes found even after storing it
4319 echo 'e4j.error.3';
4320 exit;
4321 }
4322
4323 echo json_encode($all_notes);
4324 exit;
4325 }
4326
4327 /**
4328 * AJAX endpoint to upload customer documents during the pre-checkin.
4329 *
4330 * @since 1.14 (J) - 1.4.0 (WP)
4331 * @since 1.18.6 (J) - 1.8.6 (WP) added support for MRZ detection through Channel Manager.
4332 */
4333 public function precheckin_upload_docs()
4334 {
4335 $app = JFactory::getApplication();
4336 $dbo = JFactory::getDbo();
4337 $input = $app->input;
4338
4339 if (!JSession::checkToken()) {
4340 // missing CSRF-proof token
4341 VBOHttpDocument::getInstance($app)->close(403, JText::translate('JINVALID_TOKEN'));
4342 }
4343
4344 // gather request values
4345 $order_sid = $input->getString('sid', '');
4346 $order_ts = $input->getString('ts', '');
4347 $use_mrz = $input->getBool('mrz', false);
4348 /**
4349 * This is a simple file uploading process, but if
4350 * we wanted to automatically update the pax_data,
4351 * the request vars below would be necessary.
4352 */
4353 $room_index = $input->getInt('room_index', 0);
4354 $guest_index = $input->getInt('guest_index', 0);
4355 $pax_index = $input->getString('pax_index', '');
4356
4357 if (empty($order_sid) || empty($order_ts)) {
4358 VBOHttpDocument::getInstance($app)->close(404, 'Missing booking details');
4359 }
4360
4361 $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';";
4362 $dbo->setQuery($q);
4363 $order = $dbo->loadAssoc();
4364 if (!$order) {
4365 VBOHttpDocument::getInstance($app)->close(404, 'Booking not found');
4366 }
4367
4368 $customer = array();
4369 $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'].";";
4370 $dbo->setQuery($q);
4371 $customer = $dbo->loadObject();
4372 if (!$customer) {
4373 // one customer must be assigned to this booking for the pre-checkin
4374 VBOHttpDocument::getInstance($app)->close(404, 'No customers associated to this booking');
4375 }
4376
4377 // make sure pre-checkin is allowed
4378 $precheckin = VikBooking::precheckinEnabled();
4379 if ($precheckin) {
4380 // make sure the limit of days in advance is reflected
4381 $precheckin_mind = VikBooking::precheckinMinOffset();
4382 if ($precheckin_mind < 0) {
4383 // validation made prior to check-out date and time
4384 $precheckin = time() <= strtotime("{$precheckin_mind} days 23:59:59", $order['checkout']);
4385 } else {
4386 // classic validation prior to check-in date and time
4387 $precheckin_lim_ts = strtotime("+{$precheckin_mind} days 00:00:00");
4388 $precheckin = ($precheckin_lim_ts <= $order['checkin'] || ($precheckin_mind === 1 && time() <= $order['checkin']));
4389 }
4390 }
4391 if (!$precheckin) {
4392 VBOHttpDocument::getInstance($app)->close(403, 'Pre-checkin not allowed at this time');
4393 }
4394
4395 // get uploaded files array (use "raw" to avoid filtering the file to upload)
4396 $files = $input->files->get('docs', array(), 'raw');
4397 if (!$files) {
4398 VBOHttpDocument::getInstance($app)->close(500, 'No files to be uploaded');
4399 }
4400
4401 if (isset($files['name'])) {
4402 // we have a single associative array, we need to push it within a list,
4403 // because the upload iterates the $files array
4404 $files = [$files];
4405 }
4406
4407 // fetch documents folder path
4408 $dirpath = VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR;
4409
4410 // check if we have a valid directory
4411 if (empty($customer->docsfolder) || !is_dir($dirpath . $customer->docsfolder)) {
4412 // randomize string
4413 $customer->seed = uniqid();
4414
4415 // create blocks for hashed folder
4416 $parts = [
4417 $customer->first_name,
4418 $customer->last_name,
4419 md5(serialize($customer)),
4420 ];
4421
4422 // join fetched parts
4423 $customer->docsfolder = strtolower(implode('-', array_filter($parts)));
4424
4425 if (strlen($customer->docsfolder) < 16) {
4426 VBOHttpDocument::getInstance($app)->close(400, 'Possible security breach. Please specify as many details as possible.');
4427 }
4428
4429 // create a folder for this customer
4430 $created = JFolder::create($dirpath . $customer->docsfolder);
4431
4432 if (!$created) {
4433 VBOHttpDocument::getInstance($app)->close(403, sprintf('Unable to create the folder [%s]', $dirpath . $customer->docsfolder));
4434 }
4435
4436 unset($customer->seed);
4437
4438 // update customer docs folder
4439 $record = new stdClass;
4440 $record->id = $customer->id;
4441 $record->docsfolder = $customer->docsfolder;
4442 $dbo->updateObject('#__vikbooking_customers', $record, 'id');
4443 }
4444
4445 // prepare the response with the uploaded-file objects
4446 $response = [
4447 'uploads' => [],
4448 ];
4449 $upload_err = null;
4450
4451 // compose prefix for all files uploaded (must end with an underscrore for View's compatibility)
4452 $file_prefix = str_replace(' ', '-', JText::translate('VBOPRECHECKIN')) . '_';
4453
4454 try {
4455
4456 foreach ($files as $file) {
4457 // sanitize file name
4458 if (!empty($file['name'])) {
4459 // replace quotes and pipe, which is the separator in pax_data
4460 $file['name'] = str_replace(array("'", '"', '|'), '', $file['name']);
4461 }
4462 if (empty($file['name'])) {
4463 continue;
4464 }
4465 // always prepend "pre check-in" to the original file name
4466 $file['name'] = strtolower($file_prefix . $file['name']);
4467 // try to upload the file
4468 $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');
4469 // set a valid URL for the uploaded file
4470 $result->url = str_replace(DIRECTORY_SEPARATOR, '/', str_replace(VBO_CUSTOMERS_PATH . DIRECTORY_SEPARATOR, VBO_CUSTOMERS_URI, $result->path));
4471 // push uploaded file
4472 array_push($response['uploads'], $result);
4473 }
4474
4475 } catch (Exception $e) {
4476 // do nothing, but catch the error
4477 $upload_err = $e;
4478 }
4479
4480 if (!$response['uploads']) {
4481 // raise an error
4482 if ($upload_err instanceof Exception) {
4483 VBOHttpDocument::getInstance($app)->close($upload_err->getCode() ?: 500, $upload_err->getMessage());
4484 }
4485 VBOHttpDocument::getInstance($app)->close(500, 'No files could actually be uploaded');
4486 }
4487
4488 if ($use_mrz) {
4489 // file upload completed, detect MRZ codes from uploaded documents
4490 $mrzImageUrls = array_values(array_filter(array_map(function($uploaded) {
4491 return $uploaded->url ?? '';
4492 }, $response['uploads'])));
4493
4494 // set response properties for MRZ detection result
4495 $response['mrz'] = [
4496 'data' => null,
4497 'raw' => null,
4498 'verified' => null,
4499 'error' => null,
4500 ];
4501
4502 try {
4503 // let the AI model service analyse the uploaded documents
4504 $mrzResult = (new VCMAiModelService)->mrzDetection($mrzImageUrls);
4505
4506 // let the current pax-fields data collector parse the extracted MRZ data
4507 $mrzMappedFields = VBOCheckinPax::getInstance()
4508 ->getMRZMapper()
4509 ->setVerified((bool) ($mrzResult->valid ?? false))
4510 ->mapDetectedProperties((array) ($mrzResult->data ?? null))
4511 ->getMappedFields();
4512
4513 // set MRZ validation result within the response
4514 $response['mrz']['data'] = $mrzMappedFields;
4515 $response['mrz']['raw'] = ($mrzResult->data ?? null);
4516 $response['mrz']['verified'] = (bool) $mrzResult->valid ?? false;
4517 } catch (Exception $e) {
4518 // catch the error message
4519 $response['mrz']['error'] = $e->getMessage();
4520 } catch (Throwable $e) {
4521 // catch the PHP error
4522 $response['mrz']['error'] = sprintf('Fatal error caught: %s', $e->getMessage());
4523 }
4524 }
4525
4526 // send response to output
4527 VBOHttpDocument::getInstance($app)->json($response);
4528 }
4529
4530 /**
4531 * AJAX endpoint to submit an inquiry/information request.
4532 *
4533 * @since 1.15.0 (J) - 1.5.0 (WP)
4534 */
4535 public function submit_inquiry()
4536 {
4537 if (!JSession::checkToken()) {
4538 // missing CSRF-proof token
4539 VBOHttpDocument::getInstance()->close(403, JText::translate('JINVALID_TOKEN'));
4540 }
4541
4542 $dbo = JFactory::getDbo();
4543 $app = JFactory::getApplication();
4544 $input = $app->input;
4545
4546 // get request values
4547 $checkin_dt = $input->getString('checkindate', '');
4548 $checkin_h = $input->getInt('checkinh', 0);
4549 $checkin_m = $input->getInt('checkinm', 0);
4550 $checkout_dt = $input->getString('checkoutdate', '');
4551 $checkout_h = $input->getInt('checkouth', 0);
4552 $checkout_m = $input->getInt('checkoutm', 0);
4553 $categories = $input->getString('categories', '');
4554 $roomsnum = $input->getInt('roomsnum', 1);
4555 $adults = $input->get('adults', array(), 'int');
4556 $children = $input->get('children', array(), 'int');
4557 $inquiry = $input->get('inquiry', array(), 'raw');
4558 $ulang = $input->getString('ulang', '');
4559
4560 $timeopst = VikBooking::getTimeOpenStore();
4561 if (empty($checkin_h) && empty($checkout_h) && is_array($timeopst)) {
4562 $opent = VikBooking::getHoursMinutes($timeopst[0]);
4563 $closet = VikBooking::getHoursMinutes($timeopst[1]);
4564 $checkin_h = $opent[0];
4565 $checkin_m = $opent[1];
4566 $checkout_h = $closet[0];
4567 $checkout_m = $closet[1];
4568 }
4569
4570 // compose the stay dates
4571 $checkin_ts = VikBooking::getDateTimestamp($checkin_dt, $checkin_h, $checkin_m);
4572 $checkout_ts = VikBooking::getDateTimestamp($checkout_dt, $checkout_h, $checkout_m);
4573
4574 if (empty($checkin_dt) || empty($checkout_dt) || empty($checkin_ts) || empty($checkout_ts)) {
4575 // invalid dates
4576 VBOHttpDocument::getInstance()->close(400, JText::translate('VBINVALIDDATES'));
4577 }
4578
4579 // validate guests
4580 if (!is_array($adults) || !count($adults)) {
4581 // invalid adults
4582 VBOHttpDocument::getInstance()->close(400, JText::translate('VBINCONGRDATA'));
4583 }
4584
4585 // prepare customer information
4586 $res_custdata = array();
4587 $t_first_name = '';
4588 $t_last_name = '';
4589 $guest_email = '';
4590 $guest_phone = '';
4591 $guest_country = '';
4592 $guest_extras = array();
4593 $guest_custom = array();
4594 foreach ($inquiry as $info_type => $info_vals) {
4595 if (empty($info_type) || $info_type == 'checkbox') {
4596 // we ignore any checkbox information
4597 continue;
4598 }
4599 foreach ($info_vals as $info_val) {
4600 if (!is_scalar($info_val) || !strlen($info_val)) {
4601 // empty or invalid field
4602 continue;
4603 }
4604 if ($info_type == 'nominative') {
4605 if (empty($t_first_name)) {
4606 $t_first_name = $info_val;
4607 $res_custdata[JText::translate('VBNAME')] = $info_val;
4608 } else {
4609 $t_last_name = $info_val;
4610 $res_custdata[JText::translate('VBLNAME')] = $info_val;
4611 }
4612 } elseif ($info_type == 'email') {
4613 $guest_email = $info_val;
4614 $res_custdata[JText::translate('ORDER_EMAIL')] = $info_val;
4615 } elseif ($info_type == 'phone') {
4616 $guest_phone = $info_val;
4617 $res_custdata[JText::translate('ORDER_PHONE')] = $info_val;
4618 } elseif ($info_type == 'country') {
4619 $guest_country = $info_val;
4620 $res_custdata[JText::translate('ORDER_STATE')] = $info_val;
4621 } elseif ($info_type == 'city') {
4622 $guest_extras['city'] = $info_val;
4623 $res_custdata[JText::translate('ORDER_CITY')] = $info_val;
4624 } else {
4625 // we treat this as a custom data field
4626 $guest_custom[$info_type] = $info_val;
4627 // inject reservation custdata string for this value
4628 if ($info_type == 'special_requests') {
4629 $res_custdata[JText::translate('ORDER_SPREQUESTS')] = $info_val;
4630 } else {
4631 $readable_ftype = ucwords(str_replace(array('_', '-'), ' ', $info_type));
4632 $res_custdata[$readable_ftype] = $info_val;
4633 }
4634 }
4635 }
4636 }
4637
4638 // validate fields
4639 if (!count($res_custdata)) {
4640 // we received no filled information
4641 VBOHttpDocument::getInstance()->close(400, JText::translate('VBINCONGRDATA'));
4642 }
4643
4644 // build the reservation raw-text for the customer data
4645 $res_custdata_str = VikBooking::buildCustData($res_custdata, "\n");
4646
4647 // store the customer record as first thing
4648 $cpin = VikBooking::getCPinIstance();
4649 $cpin->setCustomerExtraInfo($guest_extras);
4650 $cpin->saveCustomerDetails($t_first_name, $t_last_name, $guest_email, $guest_phone, $guest_country, array());
4651
4652 // build customer object for the inquiry reservation
4653 $customer = new stdClass;
4654 $customer->name = $t_first_name;
4655 $customer->lname = $t_last_name;
4656 $customer->email = $guest_email;
4657 $customer->phone = $guest_phone;
4658 $customer->country = $guest_country;
4659 $customer->lang = $ulang;
4660 $customer->custdata = $res_custdata_str;
4661 $customer->adminnotes = isset($guest_custom['special_requests']) ? $guest_custom['special_requests'] : '';
4662 /**
4663 * Always append the originally selected stay dates and guest party to the
4664 * administrator notes string so that they can be accessed all the times.
4665 */
4666 $customer->adminnotes .= !empty($customer->adminnotes) ? "\n" : '';
4667 $customer->adminnotes .= JText::translate('VBPICKUP') . ': ' . $checkin_dt . "\n";
4668 $customer->adminnotes .= JText::translate('VBRETURN') . ': ' . $checkout_dt . "\n";
4669 $customer->adminnotes .= JText::translate('VBFORMADULTS') . ': ' . array_sum($adults) . "\n";
4670 $customer->adminnotes .= JText::translate('VBFORMCHILDREN') . ': ' . array_sum($children) . "\n";
4671
4672 // prepare response object
4673 $response = new stdClass;
4674 $response->status = 0;
4675 $response->error = '';
4676
4677 // invoke availability helper class
4678 $av_helper = VikBooking::getAvailabilityInstance();
4679
4680 // turn flag on to ignore restrictions, as this is an inquiry and we must allocate the booking
4681 $av_helper->ignoreRestrictions(true);
4682
4683 // increase the default number of back and forth days for alternative date suggestions
4684 $av_helper->setBackForthDays(90);
4685
4686 // set stay dates
4687 $av_helper->setStayDates($checkin_dt, $checkout_dt);
4688
4689 // set room parties, but we expect to always have one room for the inquiry
4690 foreach ($adults as $k => $num_adults) {
4691 $num_children = isset($children[$k]) ? $children[$k] : 0;
4692 $av_helper->setRoomParty($num_adults, $num_children);
4693 }
4694
4695 // load available room rates
4696 $room_rates = $av_helper->getRates();
4697
4698 // check if availability errors occurred
4699 $has_av_error = strlen($av_helper->getError());
4700 $av_error_code = $av_helper->getErrorCode();
4701
4702 // count total fitting records
4703 $tot_records = is_array($room_rates) && !$has_av_error ? count($room_rates) : 0;
4704
4705 // build history extra data base object
4706 $ymd_stay_dates = $av_helper->getStayDates();
4707 $hist_extra_data = new stdClass;
4708 $hist_extra_data->checkin_date = $ymd_stay_dates[0];
4709 $hist_extra_data->checkout_date = $ymd_stay_dates[1];
4710 $hist_extra_data->adults = array_sum($adults);
4711 $hist_extra_data->children = array_sum($children);
4712
4713 if ($tot_records > 0) {
4714 // we can create an inquiry pending reservation for a room rate
4715 $inquiry_res_id = 0;
4716 foreach ($room_rates as $rid => $rates) {
4717 foreach ($rates as $room_rplan) {
4718 if (!is_array($room_rplan) || !isset($room_rplan['idroom'])) {
4719 continue;
4720 }
4721 // we grab the cheapest room and cheapest rate plan (the first room-rate plan)
4722 $inquiry_res_id = $av_helper->createInquiryReservation($room_rplan, $customer);
4723 break;
4724 }
4725 // make sure to create just one inquiry reservation for the cheapest room available
4726 if ($inquiry_res_id) {
4727 break;
4728 }
4729 }
4730
4731 if ($inquiry_res_id) {
4732 // assign booking to customer
4733 $cpin->saveCustomerBooking($inquiry_res_id);
4734
4735 // send email notification to admin
4736 VikBooking::sendBookingEmail($inquiry_res_id, array('admin'));
4737
4738 // trigger SMS sending
4739 VikBooking::sendBookingSMS($inquiry_res_id);
4740
4741 // booking history (set inquiry availability type to 1)
4742 $hist_extra_data->av_type = 1;
4743 VikBooking::getBookingHistoryInstance()->setBid($inquiry_res_id)->setExtraData($hist_extra_data)->store('IR', $customer->adminnotes);
4744 }
4745
4746 // update the response status no matter what
4747 $response->status = 1;
4748
4749 // output response and terminate the request
4750 VBOHttpDocument::getInstance()->json($response);
4751 }
4752
4753 // rely on suggestions in case of no rooms available
4754 if (!is_array($room_rates) || $has_av_error) {
4755 // try to get the suggestions when no availability
4756 list($alternative_dates, $alternative_parties) = $av_helper->findSuggestions();
4757
4758 $inquiry_res_id = 0;
4759 $alt_suggestion = '';
4760
4761 if (count($alternative_dates)) {
4762 $inquiry_res_id = $av_helper->allocateAltDatesInquiry($alternative_dates, $customer);
4763 $alt_suggestion = JText::translate('VBO_ALT_DATES_INQ');
4764 // set inquiry availability type to 2 for alternative dates
4765 $hist_extra_data->av_type = 2;
4766 } elseif (count($alternative_parties)) {
4767 $inquiry_res_id = $av_helper->allocateAltPartyInquiry($alternative_parties, $customer);
4768 $alt_suggestion = JText::translate('VBO_ALT_PARTY_INQ');
4769 // set inquiry availability type to 3 for alternative party
4770 $hist_extra_data->av_type = 3;
4771 }
4772
4773 if ($inquiry_res_id) {
4774 // assign booking to customer
4775 $cpin->saveCustomerBooking($inquiry_res_id);
4776
4777 // send email notification to admin
4778 VikBooking::sendBookingEmail($inquiry_res_id, array('admin'));
4779
4780 // trigger SMS sending
4781 VikBooking::sendBookingSMS($inquiry_res_id);
4782
4783 // booking history
4784 $history_obj = VikBooking::getBookingHistoryInstance()->setBid($inquiry_res_id);
4785 // store first history record
4786 $history_obj->store('IR', $customer->adminnotes);
4787 // store history record mentioning the suggestion used
4788 $history_obj->setExtraData($hist_extra_data)->store('IR', $alt_suggestion);
4789 }
4790
4791 // update the response status no matter what
4792 $response->status = 1;
4793
4794 // output response and terminate the request
4795 VBOHttpDocument::getInstance()->json($response);
4796 }
4797
4798 /**
4799 * If we reach this point it means that no rooms were available and no rooms/dates could be
4800 * suggested as an alternative party. Therefore, we allocate the reservation on a "dummy room".
4801 */
4802 $all_rooms = $av_helper->loadRooms();
4803 if (count($all_rooms)) {
4804 // grab the first "dummy room"
4805 $dummy_room_id = key($all_rooms);
4806 $room_rplan = array(
4807 'idroom' => $dummy_room_id,
4808 );
4809
4810 // create inquiry reservation for a dummy room
4811 $inquiry_res_id = $av_helper->createInquiryReservation($room_rplan, $customer);
4812 $alt_suggestion = JText::translate('VBO_ALT_DUMMY_INQ');
4813 // set inquiry availability type to 4 for "dummy room"
4814 $hist_extra_data->av_type = 4;
4815
4816 if ($inquiry_res_id) {
4817 // assign booking to customer
4818 $cpin->saveCustomerBooking($inquiry_res_id);
4819
4820 // send email notification to admin
4821 VikBooking::sendBookingEmail($inquiry_res_id, array('admin'));
4822
4823 // trigger SMS sending
4824 VikBooking::sendBookingSMS($inquiry_res_id);
4825
4826 // booking history
4827 $history_obj = VikBooking::getBookingHistoryInstance()->setBid($inquiry_res_id);
4828 // store first history record
4829 $history_obj->store('IR', $customer->adminnotes);
4830 // store history record mentioning the suggestion used
4831 $history_obj->setExtraData($hist_extra_data)->store('IR', $alt_suggestion);
4832
4833 // update the response status
4834 $response->status = 1;
4835
4836 // output response and terminate the request
4837 VBOHttpDocument::getInstance()->json($response);
4838 }
4839 }
4840
4841 // if not even a dummy room could be allocated, it means Vik Booking isn't set up
4842 $response->error = $av_helper->explainErrorCode();
4843 if (empty($response->error)) {
4844 $response->error = 'No rooms have been configured on this site yet';
4845 }
4846
4847 // output response and terminate the request
4848 VBOHttpDocument::getInstance()->json($response);
4849 }
4850
4851 /**
4852 * AJAX endpoint to load the states of a given country.
4853 *
4854 * @return void
4855 *
4856 * @since 1.16.0 (J) - 1.6.0 (WP)
4857 */
4858 public function states_load_from_country()
4859 {
4860 $dbo = JFactory::getDbo();
4861
4862 $id_country = VikRequest::getInt('id_country', 0, 'request');
4863 $country_3_code = VikRequest::getString('country_3_code', '', 'request');
4864 $country_2_code = VikRequest::getString('country_2_code', '', 'request');
4865 $country_name = VikRequest::getString('country_name', '', 'request');
4866
4867 if (empty($id_country) && empty($country_3_code) && empty($country_2_code) && empty($country_name)) {
4868 VBOHttpDocument::getInstance()->close(500, 'Missing country identifier');
4869 }
4870
4871 if (!empty($id_country)) {
4872 $q = "SELECT * FROM `#__vikbooking_states` WHERE `id_country`=" . $id_country;
4873 $dbo->setQuery($q);
4874 $dbo->execute();
4875 if (!$dbo->getNumRows()) {
4876 // no records found for this country
4877 VBOHttpDocument::getInstance()->json([]);
4878 }
4879 // output the JSON encoded list of states found
4880 VBOHttpDocument::getInstance()->json($dbo->loadAssocList());
4881 }
4882
4883 // find country ID by name or code
4884 $field_name = $dbo->qn('country_name');
4885 $field_value = $country_name;
4886 if (!empty($country_3_code)) {
4887 $field_name = $dbo->qn('country_3_code');
4888 $field_value = $country_3_code;
4889 }
4890 if (!empty($country_2_code)) {
4891 $field_name = $dbo->qn('country_2_code');
4892 $field_value = $country_2_code;
4893 }
4894
4895 $q = "SELECT `id` FROM `#__vikbooking_countries` WHERE {$field_name}=" . $dbo->quote($field_value);
4896 $dbo->setQuery($q, 0, 1);
4897 $dbo->execute();
4898 if (!$dbo->getNumRows()) {
4899 // country not found
4900 VBOHttpDocument::getInstance()->close(404, sprintf('Country [%s] not found', $field_value));
4901 }
4902
4903 $id_country = $dbo->loadResult();
4904
4905 $q = "SELECT * FROM `#__vikbooking_states` WHERE `id_country`=" . $id_country;
4906 $dbo->setQuery($q);
4907 $dbo->execute();
4908 if (!$dbo->getNumRows()) {
4909 // no records found for this country
4910 VBOHttpDocument::getInstance()->json([]);
4911 }
4912 // output the JSON encoded list of states found
4913 VBOHttpDocument::getInstance()->json($dbo->loadAssocList());
4914 }
4915
4916 /**
4917 * AJAX endpoint to perform the room upgrade operation.
4918 *
4919 * @return void
4920 *
4921 * @since 1.16.0 (J) - 1.6.0 (WP)
4922 */
4923 public function upgrade_room()
4924 {
4925 $dbo = JFactory::getDbo();
4926
4927 $bid = VikRequest::getInt('bid', 0, 'request');
4928 $sid = VikRequest::getString('sid', '', 'request');
4929 $ts = VikRequest::getString('ts', '', 'request');
4930 $room_index = VikRequest::getInt('room_index', 0, 'request');
4931 $room_id = VikRequest::getInt('room_id', 0, 'request');
4932
4933 if (empty($room_id)) {
4934 VBOHttpDocument::getInstance()->close(500, 'Invalid data provided');
4935 }
4936
4937 $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'";
4938 $dbo->setQuery($q, 0, 1);
4939 $dbo->execute();
4940 if (!$dbo->getNumRows()) {
4941 VBOHttpDocument::getInstance()->close(404, JText::translate('VBORDERNOTFOUND'));
4942 }
4943
4944 $booking = $dbo->loadAssoc();
4945
4946 $booking_rooms = VikBooking::loadOrdersRoomsData($booking['id']);
4947 if (!$booking_rooms || !isset($booking_rooms[$room_index])) {
4948 VBOHttpDocument::getInstance()->close(404, JText::translate('VBORDERNOTFOUND'));
4949 }
4950
4951 // access the room helper object
4952 $room_helper = VBORoomHelper::getInstance([
4953 'booking' => $booking,
4954 'rooms' => $booking_rooms,
4955 ]);
4956
4957 // load the upgrade options for this booking
4958 $upgrade_options = $room_helper->getUpgradeOptions();
4959
4960 if (!$upgrade_options || !isset($upgrade_options['upgrade'][$room_index]) || !isset($upgrade_options['upgrade'][$room_index]['r_costs'][$room_id])) {
4961 // the room for the upgrade is not available
4962 VBOHttpDocument::getInstance()->close(500, 'Could not upgrade to the selected room. Please reload the page and try again');
4963 }
4964
4965 $upgrade_room_rate = $upgrade_options['upgrade'][$room_index]['r_costs'][$room_id];
4966
4967 // calculate the cost difference, if any
4968 $current_room_cost = $booking_rooms[$room_index]['cust_cost'] > 0 ? (float)$booking_rooms[$room_index]['cust_cost'] : (float)$booking_rooms[$room_index]['room_cost'];
4969 $upgrade_room_cost = $current_room_cost;
4970 $upgrade_difference = 0;
4971 $upgtax_difference = 0;
4972
4973 if ($current_room_cost < $upgrade_room_rate['upgrade_cost']) {
4974 // we update the total booking amount only if the upgrade room is more expensive
4975 $upgrade_room_cost = $upgrade_room_rate['upgrade_cost'];
4976 $upgrade_difference = $upgrade_room_rate['upgrade_cost'] - $current_room_cost;
4977 // handle taxes
4978 $current_tariff_data = VBORoomHelper::getInstance()->getTariffData($booking_rooms[$room_index]['idtar']);
4979 if ($current_tariff_data) {
4980 // current room tax
4981 $current_cost_plus_tax = VikBooking::sayCostPlusIva($current_room_cost, $current_tariff_data['idprice']);
4982 $current_cost_minus_tax = VikBooking::sayCostMinusIva($current_room_cost, $current_tariff_data['idprice']);
4983 $current_room_tax = $current_cost_plus_tax - $current_cost_minus_tax;
4984 // upgrade room tax
4985 $upgrade_cost_plus_tax = VikBooking::sayCostPlusIva($upgrade_room_rate['upgrade_cost'], $upgrade_room_rate['idprice']);
4986 $upgrade_cost_minus_tax = VikBooking::sayCostMinusIva($upgrade_room_rate['upgrade_cost'], $upgrade_room_rate['idprice']);
4987 $upgrade_room_tax = $upgrade_cost_plus_tax - $upgrade_cost_minus_tax;
4988 // calculate tax difference
4989 $upgtax_difference = $upgrade_room_tax - $current_room_tax;
4990 }
4991
4992 }
4993
4994 // update booking total amounts as first
4995 if ($upgrade_difference > 0) {
4996 $upd_booking = new stdClass;
4997 $upd_booking->id = $booking['id'];
4998 $upd_booking->total = (float)($booking['total'] + $upgrade_difference);
4999 $upd_booking->tot_taxes = (float)($booking['tot_taxes'] + $upgtax_difference);
5000
5001 $dbo->updateObject('#__vikbooking_orders', $upd_booking, 'id');
5002 }
5003
5004 // perform the room upgrade (switch)
5005 $broom_record = new stdClass;
5006 $broom_record->id = $booking_rooms[$room_index]['id'];
5007 $broom_record->idorder = $booking_rooms[$room_index]['idorder'];
5008 $broom_record->idroom = $room_id;
5009 $broom_record->idtar = $upgrade_room_rate['id'];
5010 $broom_record->room_cost = $upgrade_room_cost;
5011
5012 $dbo->updateObject('#__vikbooking_ordersrooms', $broom_record, ['id', 'idorder']);
5013
5014 // Booking History
5015 $hist_descr = !empty($booking_rooms[$room_index]['room_name']) ? $booking_rooms[$room_index]['room_name'] . ' ' : '';
5016 $hist_descr .= '-&gt; ' . $upgrade_options['rooms'][$room_id]['name'];
5017 VikBooking::getBookingHistoryInstance()->setBid($booking['id'])->store('UR', $hist_descr);
5018
5019 // output the JSON encoded successful response
5020 VBOHttpDocument::getInstance()->json(['success' => 1]);
5021 }
5022
5023 /**
5024 * End-point used to ping the execution of the scheduled crontab runners.
5025 *
5026 * @return void
5027 *
5028 * @since 1.17 (J) - 1.7 (WP)
5029 */
5030 public function crontab()
5031 {
5032 VBOFactory::getCrontabSimulator()->run();
5033 exit;
5034 }
5035
5036 /**
5037 * End-point used to route at runtime the link to the requested room details page.
5038 * Originally introduced to improve landing page URL accuracy with Google VR.
5039 *
5040 * @return void
5041 *
5042 * @since 1.18.3 (J) - 1.8.3 (WP)
5043 */
5044 public function route_listing_details()
5045 {
5046 $app = JFactory::getApplication();
5047
5048 $listing_id = $app->input->getUint('listing_id', 0);
5049
5050 if (!$listing_id) {
5051 VBOHttpDocument::getInstance($app)->close(400, 'Missing listing ID.');
5052 }
5053
5054 // make sure the requested listing exists
5055 $listing_data = VikBooking::getRoomInfo($listing_id);
5056
5057 if (!$listing_data) {
5058 VBOHttpDocument::getInstance($app)->close(404, 'Listing not found.');
5059 }
5060
5061 if (empty($listing_data['avail'])) {
5062 VBOHttpDocument::getInstance($app)->close(404, 'Listing currently unavailable.');
5063 }
5064
5065 // get user country and locale (preferred language), if available, to find the best language
5066 $user_country = $app->input->getString('country', '');
5067 $user_lang = $app->input->getString('ulang', '');
5068 $user_country = empty($user_country) && !empty($user_lang) ? $user_lang : $user_country;
5069 $best_lang = (!empty($user_country) || !empty($user_lang)) && class_exists('VikChannelManager') ? VikChannelManager::guessBookingLangFromCountry($user_country, $user_lang) : '';
5070
5071 /**
5072 * Adjust best language in case the default website lang is different than English and
5073 * the visitor speaks a foreign language. This way we make English the best language.
5074 * I.E. Website default lang is IT, visitor's lang is DE, we make it land on EN because DE isn't available.
5075 */
5076 $current_lang = JFactory::getLanguage()->getTag();
5077 if (empty($best_lang) && !empty($user_lang) && substr(strtolower($user_lang), 0, 2) != 'en' && substr(strtolower($current_lang), 0, 2) != 'en') {
5078 // check if English is available
5079 foreach (VikBooking::getVboApplication()->getKnownLanguages() as $ltag => $ldet) {
5080 if (substr(strtolower($ltag), 0, 2) == 'en') {
5081 // grab this English-esque language
5082 $best_lang = $ltag;
5083 break;
5084 }
5085 }
5086 }
5087
5088 // build query arguments for routing
5089 $route_query_args = [
5090 'option' => 'com_vikbooking',
5091 'view' => 'roomdetails',
5092 'roomid' => $listing_id,
5093 ];
5094
5095 // route the best URI (if possible)
5096 try {
5097 // find the best page id for the URL according to the CMS
5098 $itemid = null;
5099
5100 if (VBOPlatformDetection::isWordPress()) {
5101 /**
5102 * @wponly route the best Shortcode for the booking process ("room details" or "search form").
5103 * the best booking language is passed over the model to find the best shortcode.
5104 */
5105 $model = JModel::getInstance('vikbooking', 'shortcodes', 'admin');
5106
5107 // grab all Shortcodes and parse them to find the best one for this room, if any
5108 $shortcodes = $model->all();
5109 foreach ($shortcodes as $shortcode) {
5110 if ($shortcode->type != 'roomdetails' || empty($shortcode->post_id)) {
5111 continue;
5112 }
5113 $page_params = json_decode($shortcode->json);
5114 if (!is_object($page_params) || !isset($page_params->roomid) || (int) $page_params->roomid != $listing_id) {
5115 continue;
5116 }
5117 if (!empty($best_lang) && $shortcode->lang == $best_lang) {
5118 // always give higher priority to the exact lang
5119 $itemid = $shortcode->post_id;
5120 }
5121 if (empty($itemid)) {
5122 // in case no perfect lang found, use this post id for this room-type
5123 $itemid = $shortcode->post_id;
5124 }
5125 }
5126
5127 if (empty($itemid)) {
5128 // default to the best shortcode for this lang of type "search form"
5129 $itemid = $model->best(['vikbooking'], $best_lang);
5130 if (!$itemid) {
5131 // if we really get to this point, try to fetch the first non-empty shortcode, if any
5132 $shortcodes = $model->all($columns = 'post_id', $full = true);
5133 if ($shortcodes) {
5134 // grab the very first active shortcode, no matter what's the type of it
5135 $itemid = $shortcodes[0]->post_id;
5136 }
5137 }
5138 }
5139 } else {
5140 /**
5141 * @joomlaonly inject the language to the query arguments list before routing
5142 */
5143 if (!empty($best_lang)) {
5144 $route_query_args['lang'] = $best_lang;
5145 }
5146
5147 if (class_exists('VCMFactory')) {
5148 $best_menuitem_id = VCMFactory::getPlatform()->getPagerouter()->findProperPageId(['roomdetails'], ['roomid' => $room_type_id, 'lang' => $best_lang]);
5149
5150 if ($best_menuitem_id) {
5151 $itemid = $best_menuitem_id;
5152 } else {
5153 $itemid = VCMFactory::getPlatform()->getPagerouter()->findProperPageId(['vikbooking'], $best_lang);
5154 }
5155 }
5156 }
5157
5158 // route final URL with all arguments
5159 $routed_url = VikBooking::externalroute($route_query_args, false, $itemid);
5160 } catch (Throwable $e) {
5161 // raise an error
5162 VBOHttpDocument::getInstance($app)->close(500, 'Could not route to the requested page.');
5163 }
5164
5165 // redirect the request to the proper page
5166 $app->redirect($routed_url);
5167 $app->close();
5168 }
5169 }
5170