PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.10
VikBooking Hotel Booking Engine & PMS v1.8.10
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.10, at site/controller.php

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