PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.6.9
VikBooking Hotel Booking Engine & PMS v1.6.9
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 / helpers / cpin.php

cpin.php in VikBooking Hotel Booking Engine & PMS 1.6.9, at site/helpers/cpin.php

1,076 lines 32.1 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 /**
14 * Handle customers within the reservation scope.
15 */
16 class VikBookingCustomersPin
17 {
18 public $all_pins;
19 public $is_admin;
20 public $fieldflags;
21 public $error;
22 private $dbo;
23 private $new_pin;
24 private $new_customer_id;
25
26 public function __construct()
27 {
28 $this->all_pins = false;
29 $this->is_admin = false;
30 $this->fieldflags = [];
31 $this->error = '';
32 $this->dbo = JFactory::getDbo();
33 $this->new_pin = '';
34 $this->new_customer_id = 0;
35 }
36
37 /**
38 * Generates a unique PIN number for the customer.
39 *
40 * @param boolean $notpush
41 *
42 * @return int 8-digit pin
43 *
44 * @since 1.15.0 (J) - 1.5.0 (WP) pin length changed from 5 to 8 digits.
45 */
46 public function generateUniquePin($notpush = false)
47 {
48 // minimum 5 digits, maximum 8 digits
49 $rand_pin = rand(10000, 99999999);
50 if ($this->pinExists($rand_pin)) {
51 while ($this->pinExists($rand_pin)) {
52 $rand_pin += 1;
53 }
54 }
55
56 if (!$notpush) {
57 $this->all_pins[] = $rand_pin;
58 }
59
60 return $rand_pin;
61 }
62
63 /**
64 * Checks if the pin already exists.
65 *
66 * @param string $pin
67 * @param string $ignorepin
68 *
69 * @return boolean
70 */
71 public function pinExists($pin, $ignorepin = '')
72 {
73 $current_pins = $this->all_pins === false ? $this->getAllPins($ignorepin) : $this->all_pins;
74 return in_array($pin, $current_pins);
75 }
76
77 /**
78 * Fetches and sets all the pins currently stored in the database.
79 *
80 * @param string $ignorepin
81 */
82 public function getAllPins($ignorepin = '')
83 {
84 $current_pins = [];
85
86 $q = "SELECT `pin` FROM `#__vikbooking_customers`".(!empty($ignorepin) ? " WHERE `pin`!=".$this->dbo->quote($ignorepin) : "").";";
87 $this->dbo->setQuery($q);
88 $pins = $this->dbo->loadAssocList();
89 if ($pins) {
90 foreach ($pins as $v) {
91 $current_pins[] = $v['pin'];
92 }
93 }
94
95 $this->all_pins = $current_pins;
96
97 return $this->all_pins;
98 }
99
100 /**
101 * Attempts to fetch the customer details record by Joomla/WordPress User ID.
102 *
103 * @param array $customer_details
104 *
105 * @return array can also be used with no return value as reference.
106 */
107 private function getDetailsByUjid(&$customer_details)
108 {
109 $user = JFactory::getUser();
110
111 if (!$user->guest && (int)$user->id > 0) {
112 $q = "SELECT * FROM `#__vikbooking_customers` WHERE `ujid`=" . (int)$user->id . " ORDER BY `#__vikbooking_customers`.`id` DESC";
113 $this->dbo->setQuery($q, 0, 1);
114 $customer = $this->dbo->loadAssoc();
115 if ($customer) {
116 $customer['cfields'] = empty($customer['cfields']) ? array() : json_decode($customer['cfields'], true);
117 $customer_details = $customer;
118 }
119 }
120
121 return $customer_details;
122 }
123
124 /**
125 * Attempts to fetch the customer details record by PIN Cookie.
126 *
127 * @param array $customer_details
128 *
129 * @return array can also be used with no return value as reference.
130 */
131 private function getDetailsByPinCookie(&$customer_details)
132 {
133 $pin_cookie = $this->getPinCookie();
134 $pin_cookie = empty($pin_cookie) ? (int)$this->getNewPin() : $pin_cookie;
135 if ($pin_cookie) {
136 $q = "SELECT * FROM `#__vikbooking_customers` WHERE `pin`=" . $this->dbo->quote($pin_cookie) . " ORDER BY `#__vikbooking_customers`.`id` DESC";
137 $this->dbo->setQuery($q, 0, 1);
138 $customer = $this->dbo->loadAssoc();
139 if ($customer) {
140 $customer['cfields'] = empty($customer['cfields']) ? array() : json_decode($customer['cfields'], true);
141 $customer_details = $customer;
142 }
143 }
144
145 return $customer_details;
146 }
147
148 /**
149 * Gets "decoded" PIN from Cookie.
150 *
151 * @return int the pin cookie.
152 */
153 private function getPinCookie()
154 {
155 $pin_cookie = 0;
156 $cookie = JFactory::getApplication()->input->cookie;
157 $cookie_val = $cookie->get('vboPinData', '', 'string');
158 if (!empty($cookie_val) && intval($cookie_val) > 0) {
159 $cookie_val = intval(strrev( (string)$cookie_val )) / 1987;
160 $pin_cookie = (int)$cookie_val > 0 ? $cookie_val : $pin_cookie;
161 }
162 return $pin_cookie;
163 }
164
165 /**
166 * Sets "encoded" PIN to Cookie with a lifetime of 365 days.
167 *
168 * @param string $pin
169 */
170 private function setPinCookie($pin)
171 {
172 $pin_cookie = 0;
173 if (!empty($pin)) {
174 $pin_cookie = (int)$pin * 1987;
175 $pin_cookie = strrev( (string)$pin_cookie );
176 VikRequest::setCookie('vboPinData', $pin_cookie, (time() + (86400 * 365)), '/', '', false, true);
177 }
178
179 return $pin_cookie;
180 }
181
182 /**
183 * Unsets PIN Cookie
184 */
185 private function unsetPinCookie()
186 {
187 $cookie = JFactory::getApplication()->input->cookie;
188 VikRequest::setCookie('vboPinData', $pin_cookie, (time() - (86400 * 365)), '/', '', false, true);
189 $cookie_val = $cookie->get('vboPinData', '', 'string');
190
191 return $pin_cookie;
192 }
193
194 /**
195 * Loads the customer details by Joomla/WordPress User ID or by PIN cookie.
196 * Returns an associative array with the record fetched from the DB.
197 *
198 * @return array empty array or customer record associative array.
199 */
200 public function loadCustomerDetails()
201 {
202 $customer_details = [];
203
204 // first attempt is through Joomla User ID
205 $this->getDetailsByUjid($customer_details);
206
207 if (!count($customer_details)) {
208 // second attempt is through PIN Cookie
209 $this->getDetailsByPinCookie($customer_details);
210 }
211
212 return $customer_details;
213 }
214
215 /**
216 * Checks whether the given customer has got automatic discounts reserved for the current booking.
217 *
218 * @param array $customer customer record associative array.
219 * @param array $booking the booking information array to validate the coupon.
220 *
221 * @return array empty array or proper customer coupon record.
222 *
223 * @since 1.16.0 (J) - 1.6.0 (WP)
224 */
225 public function getCustomerCoupon(array $customer = [], array $booking = [])
226 {
227 if (empty($customer['id'])) {
228 return [];
229 }
230
231 $q = "SELECT `ccp`.`idcoupon`, `cp`.* FROM `#__vikbooking_customers_coupons` AS `ccp`
232 LEFT JOIN `#__vikbooking_coupons` AS `cp` ON `ccp`.`idcoupon`=`cp`.`id`
233 WHERE `ccp`.`idcustomer`=" . (int)$customer['id'] . " AND `ccp`.`automatic`=1 ORDER BY `ccp`.`idcoupon` DESC";
234 $this->dbo->setQuery($q);
235 $customer_coupons = $this->dbo->loadAssocList();
236 if (!$customer_coupons) {
237 return [];
238 }
239
240 if (empty($booking)) {
241 // do not perform any validation on the coupon restrictions
242 return $customer_coupons[0];
243 }
244
245 // process all coupon codes for this customer to find the best one for this booking details
246 foreach ($customer_coupons as $coupon) {
247 if (!empty($coupon['datevalid'])) {
248 $dateparts = explode("-", $coupon['datevalid']);
249 $pickinfo = getdate($booking['checkin']);
250 $dropinfo = getdate($booking['checkout']);
251 $checkpick = mktime(0, 0, 0, $pickinfo['mon'], $pickinfo['mday'], $pickinfo['year']);
252 $checkdrop = mktime(0, 0, 0, $dropinfo['mon'], $dropinfo['mday'], $dropinfo['year']);
253 if (!($checkpick >= $dateparts[0] && $checkpick <= $dateparts[1] && $checkdrop >= $dateparts[0] && $checkdrop <= $dateparts[1])) {
254 // invalid dates
255 continue;
256 }
257 }
258 if (!empty($coupon['minlos']) && $coupon['minlos'] > $booking['days']) {
259 // invalid min LOS
260 continue;
261 }
262 if (!$coupon['allvehicles'] && !empty($coupon['idrooms'])) {
263 // validate rooms booked
264 foreach ((array)$booking['rooms'] as $room) {
265 if (!preg_match("/;" . $room['id'] . ";/i", $coupon['idrooms'])) {
266 // room not allowed
267 continue 2;
268 }
269 }
270 }
271 // return the first eligible coupon discount
272 return $coupon;
273 }
274
275 // no eligible discounts found
276 return [];
277 }
278
279 /**
280 * Attempts to fetch the customer details record by PIN code.
281 */
282 public function getCustomerByPin($pin)
283 {
284 $customer = [];
285 $this->setNewPin($pin);
286
287 if (!empty($pin)) {
288 $q = "SELECT * FROM `#__vikbooking_customers` WHERE `pin`=".$this->dbo->quote($pin)." ORDER BY `#__vikbooking_customers`.`id` DESC";
289 $this->dbo->setQuery($q, 0, 1);
290 $customer = $this->dbo->loadAssoc();
291 if ($customer) {
292 $customer['cfields'] = empty($customer['cfields']) ? array() : json_decode($customer['cfields'], true);
293 $this->setPinCookie($pin);
294 }
295 }
296
297 return $customer;
298 }
299
300 /**
301 * Get customer by ID.
302 *
303 * @param int $cust_id the ID of the customer.
304 *
305 * @return array the customer array or an empty array.
306 */
307 public function getCustomerByID($cust_id)
308 {
309 $customer = [];
310 if (!empty($cust_id)) {
311 $q = "SELECT `c`.*,`nat`.`country_name`,`nat`.`country_2_code` FROM `#__vikbooking_customers` AS `c` LEFT JOIN `#__vikbooking_countries` AS `nat` ON `c`.`country`=`nat`.`country_3_code` WHERE `c`.`id`=".$this->dbo->quote($cust_id);
312 $this->dbo->setQuery($q, 0, 1);
313 $customer = $this->dbo->loadAssoc();
314 $customer = !$customer ? [] : $customer;
315 }
316
317 if ($customer) {
318 $customer['cfields'] = empty($customer['cfields']) ? array() : json_decode($customer['cfields'], true);
319 $customer['chdata'] = !empty($customer['chdata']) ? json_decode($customer['chdata'], true) : array();
320 $customer['chdata'] = is_array($customer['chdata']) ? $customer['chdata'] : array();
321 }
322
323 return $customer;
324 }
325
326 /**
327 * Get customer array by booking ID.
328 * Also returns the pax_data information.
329 *
330 * @param int $orderid the VBO booking ID.
331 *
332 * @return array the customer array or an empty array.
333 *
334 * @uses getCustomerByID()
335 */
336 public function getCustomerFromBooking($orderid)
337 {
338 if (empty($orderid)) {
339 return [];
340 }
341 $q = "SELECT `idcustomer`, `pax_data` FROM `#__vikbooking_customers_orders` WHERE `idorder`=".(int)$orderid.";";
342 $this->dbo->setQuery($q);
343 $data = $this->dbo->loadAssoc();
344 if (!$data) {
345 return [];
346 }
347
348 $customer = $this->getCustomerByID($data['idcustomer']);
349 if ($customer) {
350 /**
351 * Merge pax_data into the customer array to know whether
352 * the pre check-in or the registration was performed.
353 *
354 * @since 1.12
355 */
356 $customer['pax_data'] = $data['pax_data'];
357 }
358
359 return $customer;
360 }
361
362 /**
363 * Checks whether a customer with the same email address already exists
364 * Returns false or the record of the existing customer
365 *
366 * @param string $email the email address.
367 * @param string $first_name optional first name to compare existing emails.
368 * @param string $last_name optional last name to compare existing emails.
369 *
370 * @return mixed false if customer does not exist, array if customer found.
371 *
372 * @since 1.13 equal email addresses can be shared across multiple customers
373 * so long as the name or the last name are different.
374 */
375 public function customerExists($email, $first_name = '', $last_name = '')
376 {
377 if (empty($email)) {
378 return false;
379 }
380 $q = "SELECT * FROM `#__vikbooking_customers` WHERE `email`=".$this->dbo->quote(trim($email))." ORDER BY `#__vikbooking_customers`.`id` DESC;";
381 $this->dbo->setQuery($q);
382 $customers = $this->dbo->loadAssocList();
383 if (!$customers) {
384 return false;
385 }
386
387 if (empty($first_name) || empty($last_name)) {
388 // no info to compare an existing customer, so say it exists with this email address
389 return $customers[0];
390 }
391
392 // check if name and last name match with the current customers having this email address
393 foreach ($customers as $c) {
394 if (stripos($c['first_name'], $first_name) !== false && stripos($c['last_name'], $last_name) !== false) {
395 // customer with same email, first name and last name found
396 return $c;
397 }
398 }
399
400 /**
401 * We now check if a record with same email, first name and last name is found first, if not we return false.
402 *
403 * @since 1.14 (Joomla) - 1.4.0 (WordPress)
404 */
405 return false;
406 }
407
408 /**
409 * Sets some customer extra information like address, city, zip, company name, vat
410 */
411 public function setCustomerExtraInfo($fieldflags)
412 {
413 if (is_array($fieldflags) && count($fieldflags) > 0) {
414 $this->fieldflags = $fieldflags;
415 }
416 }
417
418 /**
419 * Converts the 2-char country code or country name into the ISO Alpha3 Char Code.
420 *
421 * @param string $country the 2-char country code or country name to convert.
422 *
423 * @return string either the 3-char version or the passed value.
424 *
425 * @since 1.13.5
426 */
427 public function get3CharCountry($country)
428 {
429 if (empty($country) || strlen((string)$country) < 2) {
430 return $country;
431 }
432
433 // trim white spaces
434 $country = trim($country);
435
436 // check what field to look for
437 $clause = [];
438 if (strlen($country) == 2) {
439 array_push($clause, "`country_2_code`=" . $this->dbo->quote($country));
440 } else {
441 array_push($clause, "`country_name` LIKE " . $this->dbo->quote("%{$country}%"));
442 }
443
444 // query the db
445 $q = "SELECT `country_3_code` FROM `#__vikbooking_countries` WHERE " . implode(' AND ', $clause);
446 $this->dbo->setQuery($q, 0, 1);
447 $three_country = $this->dbo->loadResult();
448 if ($three_country) {
449 // 3-char code found
450 return $three_country;
451 }
452
453 // nothing found, return the passed value
454 return $country;
455 }
456
457 /**
458 * Saves the customer in DB if it doesn't exist, generates the PIN and sets the cookie
459 */
460 public function saveCustomerDetails($first_name, $last_name, $email, $phone_number, $country, $cfields)
461 {
462 if (empty($first_name) || empty($last_name) || empty($email)) {
463 $this->setError('Missing fields for saving new customer');
464 return false;
465 }
466
467 /**
468 * We convert any 2-char country code into a 3-char country code
469 *
470 * @since 1.13.5
471 */
472 if (!empty($country) && strlen($country) == 2) {
473 $country = $this->get3CharCountry($country);
474 }
475
476 /**
477 * In case the phone number is missing the international prefix at the beginning of the string,
478 * mostly happens in case of OTA bookings, we pre-pend the country prefix.
479 *
480 * @since 1.12 (patch October 2019)
481 */
482 $phone_number = trim($phone_number);
483 if (!empty($phone_number) && !empty($country) && substr($phone_number, 0, 1) != '+' && substr($phone_number, 0, 2) != '00') {
484 // try to find the country phone prefix
485 $q = "SELECT `phone_prefix` FROM `#__vikbooking_countries` WHERE `country_" . (strlen($country) == 2 ? '2' : '3') . "_code`=" . $this->dbo->quote($country) . ";";
486 $this->dbo->setQuery($q);
487 $phone_prefix = $this->dbo->loadResult();
488 if ($phone_prefix) {
489 $country_prefix = str_replace(' ', '', $phone_prefix);
490 $num_prefix = str_replace('+', '', $country_prefix);
491 if (substr($phone_number, 0, strlen($num_prefix)) != $num_prefix) {
492 // country prefix is completely missing
493 $phone_number = $country_prefix . $phone_number;
494 } else {
495 // try to prepend the plus symbol because the phone number starts with the country prefix
496 $phone_number = '+' . $phone_number;
497 }
498 }
499 }
500
501 // check if the customer exists
502 $customer = $this->customerExists($email, $first_name, $last_name);
503
504 // access the current user
505 $user = JFactory::getUser();
506
507 if ($customer === false) {
508 $new_pin = $this->generateUniquePin();
509
510 // build customer record
511 $customer_obj = new stdClass;
512 $customer_obj->first_name = $first_name;
513 $customer_obj->last_name = $last_name;
514 $customer_obj->email = $email;
515 $customer_obj->phone = $phone_number;
516 $customer_obj->country = $country;
517 $customer_obj->cfields = is_array($cfields) && $cfields ? json_encode($cfields) : null;
518 $customer_obj->pin = $new_pin;
519 $customer_obj->ujid = $this->is_admin ? 0 : (int)$user->id;
520 // add the extra field flags
521 foreach ($this->fieldflags as $flagk => $flagv) {
522 if (is_array($flagv) || is_object($flagv)) {
523 $flagv = json_encode($flagv);
524 }
525 $customer_obj->{$flagk} = $flagv;
526 }
527
528 // trigger the customer before-insert event
529 $this->pluginCustomerSync(0, 'insert', (array)$customer_obj, $before = true);
530
531 // insert the new customer record
532 $this->dbo->insertObject('#__vikbooking_customers', $customer_obj, 'id');
533 $new_customer_id = isset($customer_obj->id) ? $customer_obj->id : null;
534
535 if (!empty($new_customer_id)) {
536 $this->setNewPin($new_pin);
537 $this->setNewCustomerId($new_customer_id);
538
539 // trigger the customer after-save event
540 $this->pluginCustomerSync($new_customer_id, 'insert', (array)$customer_obj, $before = false);
541 }
542 } elseif (is_array($customer) && $customer) {
543 $this->setNewPin($customer['pin']);
544 $this->setNewCustomerId($customer['id']);
545
546 // build customer record
547 $customer_obj = new stdClass;
548 $customer_obj->id = $customer['id'];
549 $customer_obj->first_name = $first_name;
550 $customer_obj->last_name = $last_name;
551 $customer_obj->email = $email;
552 $customer_obj->phone = $phone_number;
553 $customer_obj->country = $country;
554 if (!$this->is_admin) {
555 $customer_obj->cfields = is_array($cfields) && $cfields ? json_encode($cfields) : null;
556 }
557 $customer_obj->pin = $customer['pin'];
558 $customer_obj->ujid = $this->is_admin ? 0 : (int)$user->id;
559 // add the extra field flags
560 foreach ($this->fieldflags as $flagk => $flagv) {
561 if (is_array($flagv) || is_object($flagv)) {
562 $flagv = json_encode($flagv);
563 }
564 $customer_obj->{$flagk} = $flagv;
565 }
566
567 // trigger the customer before-update event
568 $this->pluginCustomerSync($customer['id'], 'update', (array)$customer_obj, $before = true);
569
570 // update customer record
571 $this->dbo->updateObject('#__vikbooking_customers', $customer_obj, 'id');
572
573 // trigger the customer after-save event
574 $this->pluginCustomerSync($customer['id'], 'update', (array)$customer_obj, $before = false);
575 }
576
577 //unset extra info
578 $this->fieldflags = [];
579
580 return !$this->is_admin ? $this->storeCustomerCookie() : true;
581 }
582
583 public function storeCustomerCookie()
584 {
585 $pin = $this->getNewPin();
586 $customer_id = $this->getNewCustomerId();
587 if (empty($pin) || empty($customer_id)) {
588 return false;
589 }
590 $this->setPinCookie($pin);
591 return true;
592 }
593
594 /**
595 * Stores a relation between the Customer ID and the Booking ID
596 * This method should be called after the saveCustomerDetails() because
597 * it requires the methods setNewPin and setNewCustomerId to be called before.
598 * Since VBO 1.9 this method also calculates and sets the commissions
599 * amount if the customer is a sales channel.
600 * Requires the records in _ordersrooms to be stored before being called.
601 *
602 * @param int orderid the ID of the VBO order
603 *
604 * @return boolean
605 */
606 public function saveCustomerBooking($orderid)
607 {
608 $pin = $this->getNewPin();
609 $customer_id = $this->getNewCustomerId();
610 if (empty($orderid) || empty($pin) || empty($customer_id)) {
611 return false;
612 }
613
614 $q = "SELECT * FROM `#__vikbooking_ordersrooms` WHERE `idorder`=".(int)$orderid.";";
615 $this->dbo->setQuery($q);
616 $orders_rooms = $this->dbo->loadAssocList();
617 if (!$orders_rooms) {
618 return false;
619 }
620
621 $q = "DELETE FROM `#__vikbooking_customers_orders` WHERE `idorder`=".$this->dbo->quote($orderid).";";
622 $this->dbo->setQuery($q);
623 $this->dbo->execute();
624
625 $q = "INSERT INTO `#__vikbooking_customers_orders` (`idcustomer`,`idorder`) VALUES(".$this->dbo->quote($customer_id).", ".$this->dbo->quote($orderid).");";
626 $this->dbo->setQuery($q);
627 $this->dbo->execute();
628
629 // when assigning a booking to a customer, check that the traveler first and last name is not empty for the page Dashboard that reads it
630 if (empty($orders_rooms[0]['t_first_name']) && empty($orders_rooms[0]['t_last_name'])) {
631 $customer_info = $this->getCustomerByID($customer_id);
632 $q = "UPDATE `#__vikbooking_ordersrooms` SET `t_first_name`=".$this->dbo->quote($customer_info['first_name']).", `t_last_name`=".$this->dbo->quote($customer_info['last_name'])." WHERE `idorder`=".(int)$orderid." LIMIT 1;";
633 $this->dbo->setQuery($q);
634 $this->dbo->execute();
635
636 // update the country as well
637 if (!empty($customer_info['country'])) {
638 $q = "UPDATE `#__vikbooking_orders` SET `country`=".$this->dbo->quote($customer_info['country'])." WHERE `id`=".(int)$orderid.";";
639 $this->dbo->setQuery($q);
640 $this->dbo->execute();
641 }
642 }
643
644 // commissions for customers that are sales channels
645 $this->updateBookingCommissions($orderid, $customer_id);
646
647 return true;
648 }
649
650 /**
651 * Changes the customer assigned to the booking by
652 * re-calculating the amount of commissions (if any).
653 *
654 * @param int $orderid the booking id.
655 * @param int $customer_id the id of the new customer.
656 *
657 * @return boolean
658 *
659 * @since 1.11
660 */
661 public function updateCustomerBooking($orderid, $customer_id)
662 {
663 if (empty($orderid) || empty($customer_id)) {
664 return false;
665 }
666 $new_customer = $this->getCustomerByID($customer_id);
667 if (!count($new_customer)) {
668 // invalid customer ID given
669 return false;
670 }
671 $old_customer = $this->getCustomerFromBooking($orderid);
672 if (count($old_customer) && (int)$old_customer['ischannel'] > 0) {
673 // unset first the old commissions and channel name
674 $q = "UPDATE `#__vikbooking_orders` SET `channel`=NULL, `cmms`=NULL WHERE `id`=".(int)$orderid.";";
675 $this->dbo->setQuery($q);
676 $this->dbo->execute();
677 }
678 if (count($old_customer)) {
679 // update reference
680 $q = "UPDATE `#__vikbooking_customers_orders` SET `idcustomer`=".(int)$new_customer['id']." WHERE `idorder`=".(int)$orderid.";";
681 $this->dbo->setQuery($q);
682 $this->dbo->execute();
683 if (!empty($new_customer['country']) && strlen($new_customer['country']) == 3) {
684 // update booking country
685 $q = "UPDATE `#__vikbooking_orders` SET `country`=" . $this->dbo->quote($new_customer['country']) . " WHERE `id`=" . (int)$orderid . ";";
686 $this->dbo->setQuery($q);
687 $this->dbo->execute();
688 }
689 } else {
690 // insert relation
691 $q = "INSERT INTO `#__vikbooking_customers_orders` (`idcustomer`,`idorder`) VALUES(".(int)$new_customer['id'].", ".(int)$orderid.");";
692 $this->dbo->setQuery($q);
693 $this->dbo->execute();
694 if (!empty($new_customer['country']) && strlen($new_customer['country']) == 3) {
695 // update booking country
696 $q = "UPDATE `#__vikbooking_orders` SET `country`=" . $this->dbo->quote($new_customer['country']) . " WHERE `id`=" . (int)$orderid . ";";
697 $this->dbo->setQuery($q);
698 $this->dbo->execute();
699 }
700 }
701
702 // update commissions and sale channel information
703 return $this->updateBookingCommissions($orderid, $new_customer['id']);
704 }
705
706 /**
707 * Calculates and sets the commissions amount if the customer
708 * is a "sales channel" with a percentage greater than 0.
709 *
710 * @param int $orderid
711 * @param int $customer_id
712 *
713 * @since 1.9
714 */
715 public function updateBookingCommissions($orderid, $customer_id = 0)
716 {
717 if (empty($customer_id)) {
718 $customer_id = $this->getNewCustomerId();
719 }
720
721 if (empty($orderid) || empty($customer_id)) {
722 return false;
723 }
724
725 $customer = $this->getCustomerByID($customer_id);
726 if ((int)$customer['ischannel'] > 0 && !empty($customer['chdata']) && isset($customer['chdata']['commission']) && $customer['chdata']['commission'] > 0.00) {
727 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=".$this->dbo->quote($orderid).";";
728 $this->dbo->setQuery($q);
729 $order_info = $this->dbo->loadAssoc();
730 if ($order_info) {
731 if ((float)$order_info['total'] > 0.00) {
732 $cmms_calc_base = $this->calcCommissionsBaseAmount($order_info, $customer);
733 $cmms_amount = $cmms_calc_base * $customer['chdata']['commission'] / 100;
734 $source_name = 'customer'.$customer['id'].(array_key_exists('chname', $customer['chdata']) ? '_'.$customer['chdata']['chname'] : '');
735
736 $q = "UPDATE `#__vikbooking_orders` SET `channel`=".$this->dbo->quote($source_name).", `cmms`=".$this->dbo->quote($cmms_amount)." WHERE `id`=".$this->dbo->quote($order_info['id']).";";
737 $this->dbo->setQuery($q);
738 $this->dbo->execute();
739
740 return true;
741 }
742 }
743 }
744
745 return false;
746 }
747
748 /**
749 * Calculates the base amount on which the commissions should be applied.
750 * Considers the parameters for this customer sales channel for taxes and rooms rates.
751 *
752 * @param array $order_info
753 * @param array $customer
754 */
755 private function calcCommissionsBaseAmount($order_info, $customer)
756 {
757 $cmms_calc_base = $order_info['total'];
758
759 if (!empty($customer['chdata']) && array_key_exists('calccmmon', $customer['chdata']) && (int)$customer['chdata']['calccmmon'] > 0) {
760 // commissions based on room rates only
761 $q = "SELECT `or`.* FROM `#__vikbooking_ordersrooms` AS `or` WHERE `or`.`idorder`='" . (int)$order_info['id'] . "';";
762 $this->dbo->setQuery($q);
763 $order_rooms = $this->dbo->loadAssocList();
764 if ($order_rooms) {
765 $map_cost_tax = [];
766 foreach ($order_rooms as $or) {
767 if (!((float)$or['room_cost'] > 0.00) && !((float)$or['cust_cost'] > 0.00)) {
768 // missing information about the room cost - cannot proceed
769 continue;
770 }
771 $map_cost_tax[] = array(
772 'amount' => ((float)$or['room_cost'] > 0.00 ? $or['room_cost'] : $or['cust_cost']),
773 'taxid' => (!empty($or['cust_cost']) ? intval($or['cust_idiva']) : $this->getTaxIdFromTar($or['idtar']))
774 );
775 }
776 if (count($map_cost_tax) == count($order_rooms)) {
777 // all rooms have a custom cost or a room cost set. We can proceed
778 if (array_key_exists('applycmmon', $customer['chdata']) && (int)$customer['chdata']['applycmmon'] > 0) {
779 // commissions based on amounts tax excluded
780 if ($this->pricesTaxIncluded()) {
781 // prices are tax included so update the amounts in $map_cost_tax
782 foreach ($map_cost_tax as $ctk => $ctv) {
783 if (!((float)$ctv['amount'] > 0.00) || empty($ctv['taxid'])) {
784 continue;
785 }
786 list($aliq, $taxcap) = $this->getAliqFromTaxId($ctv['taxid']);
787 if ((float)$aliq > 0.00) {
788 $op_div = (100 + $aliq) / 100;
789 $tmp_op = $ctv['amount'] / $op_div;
790 /**
791 * Tax Cap implementation
792 *
793 * @since 1.12
794 */
795 if ($taxcap > 0 && ($ctv['amount'] - $tmp_op) > $taxcap) {
796 $tmp_op = $ctv['amount'] - $taxcap;
797 }
798 $map_cost_tax[$ctk]['amount'] = $tmp_op;
799 }
800 }
801 }
802 }
803 // sum all the amounts to get the base amount where commissions will be applied
804 $sum = 0;
805 foreach ($map_cost_tax as $k => $map) {
806 $sum += $map['amount'];
807 }
808 $cmms_calc_base = $sum;
809 }
810 }
811 }
812
813 return $cmms_calc_base;
814 }
815
816 private function pricesTaxIncluded()
817 {
818 if (!class_exists('VikBooking')) {
819 require_once(VBO_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikbooking.php');
820 }
821 return VikBooking::ivaInclusa();
822 }
823
824 /**
825 * Retrieves the aliquot and tax cap of the tax ID passed.
826 * Returns 0 if nothing is found, the Tax Aliq otherwise.
827 *
828 * @param int $taxid the ID of the tax rate
829 *
830 * @return array the aliquote as 0th array value, the tax cap as 1st value.
831 *
832 * @since 1.12 this private method used to return just the aliquote.
833 */
834 private function getAliqFromTaxId($taxid)
835 {
836 $aliq = 0;
837 $taxcap = 0;
838 if (intval($taxid) > 0) {
839 $q = "SELECT `i`.`aliq`,`i`.`taxcap` FROM `#__vikbooking_iva` AS `i` WHERE `i`.`id`=" . (int)$taxid;
840 $this->dbo->setQuery($q, 0, 1);
841 $tax_info = $this->dbo->loadAssoc();
842 if ($tax_info) {
843 $aliq = $tax_info['aliq'];
844 $taxcap = $tax_info['taxcap'];
845 }
846 }
847
848 return [$aliq, $taxcap];
849 }
850
851 /**
852 * Retrieves the ID of the tax used for the tariff passed.
853 * Returns 0 if nothing is found, the Tax ID otherwise.
854 *
855 * @param int $idtar
856 */
857 private function getTaxIdFromTar($idtar)
858 {
859 $taxid = 0;
860 if (intval($idtar) > 0) {
861 $q = "SELECT `d`.`id`,`d`.`idprice`,`p`.`idiva`,`i`.`aliq` FROM `#__vikbooking_dispcost` AS `d` LEFT JOIN `#__vikbooking_prices` `p` ON `p`.`id`=`d`.`idprice` LEFT JOIN `#__vikbooking_iva` `i` ON `i`.`id`=`p`.`idiva` WHERE `d`.`id`=" . (int)$idtar;
862 $this->dbo->setQuery($q, 0, 1);
863 $tax_info = $this->dbo->loadAssoc();
864 if ($tax_info) {
865 $taxid = (int)$tax_info['idiva'];
866 }
867 }
868
869 return $taxid;
870 }
871
872 /**
873 * Takes the Customer PIN from the Order ID
874 *
875 * @param int $orderid
876 */
877 public function getPinCodeByOrderId($orderid)
878 {
879 $pin = '';
880 if (!empty($orderid)) {
881 $q = "SELECT `o`.`id`,`oc`.`idcustomer`,`c`.`pin` FROM `#__vikbooking_orders` AS `o` LEFT JOIN `#__vikbooking_customers_orders` `oc` ON `oc`.`idorder`=`o`.`id` LEFT JOIN `#__vikbooking_customers` `c` ON `c`.`id`=`oc`.`idcustomer` WHERE `o`.`id`=".intval($orderid)." AND `oc`.`idcustomer` IS NOT NULL;";
882 $this->dbo->setQuery($q);
883 $custdata = $this->dbo->loadAssocList();
884 if ($custdata) {
885 if (!empty($custdata[0]['pin'])) {
886 $pin = $custdata[0]['pin'];
887 }
888 }
889 }
890
891 return $pin;
892 }
893
894 /**
895 * Triggers the events to sync customers accross multiple plugins.
896 *
897 * @param int $customer_id
898 * @param string $mode
899 * @param array $data optional customer data associative array.
900 * @param bool $before whether it's before or after saving the customer.
901 *
902 * @since 1.16.7 (J) - 1.6.7 (WP) introduced args $data and $before.
903 */
904 public function pluginCustomerSync($customer_id, $mode, $data = [], $before = true)
905 {
906 if ($data) {
907 $customer = $data;
908 } else {
909 $q = "SELECT * FROM `#__vikbooking_customers` WHERE `id`=" . (int)$customer_id;
910 $this->dbo->setQuery($q, 0, 1);
911 $customer = $this->dbo->loadAssoc();
912 if (!$customer) {
913 return false;
914 }
915 }
916
917 // make sure to import this type of plugins
918 JPluginHelper::importPlugin('e4j');
919
920 // get the event name
921 if ($mode == 'insert') {
922 // trigger plugin -> customer creation
923 $ev_name = 'onCustomerInsert';
924 } elseif ($mode == 'update') {
925 // trigger plugin -> customer update
926 $ev_name = 'onCustomerUpdate';
927 } elseif ($mode == 'delete') {
928 // trigger plugin -> customer delete
929 $ev_name = 'onCustomerDelete';
930 } else {
931 return false;
932 }
933
934 // event options
935 $options = [
936 'alias' => 'com_vikbooking',
937 'version' => (defined('VIKBOOKING_SOFTWARE_VERSION') ? VIKBOOKING_SOFTWARE_VERSION : E4J_SOFTWARE_VERSION),
938 'admin' => VikBooking::isAdmin(),
939 'call' => __FUNCTION__,
940 ];
941
942 try {
943 if ($before) {
944 /**
945 * Trigger event for the imminent creation, update or deletion of the customer.
946 */
947 VBOFactory::getPlatform()->getDispatcher()->trigger($ev_name, [$customer, $options]);
948 } elseif (!$before && ($mode === 'insert' || $mode === 'update')) {
949 /**
950 * Trigger event after the creation or the update of the customer.
951 */
952 VBOFactory::getPlatform()->getDispatcher()->trigger('onAfterSaveCustomer', [$customer, ($mode === 'insert')]);
953 }
954 } catch (Throwable $e) {
955 // do nothing
956 }
957
958 return true;
959 }
960
961 /**
962 * Sets the current customer PIN
963 *
964 * @param string $pin
965 */
966 public function setNewPin($pin = '')
967 {
968 $this->new_pin = $pin;
969 }
970
971 /**
972 * Get the current customer PIN
973 */
974 public function getNewPin()
975 {
976 return $this->new_pin;
977 }
978
979 /**
980 * Sets the current customer ID
981 *
982 * @param int $cid
983 */
984 public function setNewCustomerId($cid = 0)
985 {
986 $this->new_customer_id = (int)$cid;
987 }
988
989 /**
990 * Get the current customer ID
991 */
992 public function getNewCustomerId()
993 {
994 return $this->new_customer_id;
995 }
996
997 /**
998 * Explanation of the XML error
999 *
1000 * @param object $error
1001 */
1002 public function libxml_display_error($error)
1003 {
1004 $return = "\n";
1005 switch ($error->level) {
1006 case LIBXML_ERR_WARNING :
1007 $return .= "Warning ".$error->code.": ";
1008 break;
1009 case LIBXML_ERR_ERROR :
1010 $return .= "Error ".$error->code.": ";
1011 break;
1012 case LIBXML_ERR_FATAL :
1013 $return .= "Fatal Error ".$error->code.": ";
1014 break;
1015 }
1016 $return .= trim($error->message);
1017 if ($error->file) {
1018 $return .= " in ".$error->file;
1019 }
1020 $return .= " on line ".$error->line."\n";
1021 return $return;
1022 }
1023
1024 /**
1025 * Get the XML errors occurred
1026 */
1027 public function libxml_display_errors()
1028 {
1029 $errorstr = "";
1030 $errors = libxml_get_errors();
1031 foreach ($errors as $error) {
1032 $errorstr .= $this->libxml_display_error($error);
1033 }
1034 libxml_clear_errors();
1035 return $errorstr;
1036 }
1037
1038 private function setError($str)
1039 {
1040 $this->error .= $str."\n";
1041 }
1042
1043 public function getError()
1044 {
1045 return nl2br(rtrim($this->error, "\n"));
1046 }
1047
1048 /**
1049 * Tells whether Vik Booking is up to date to support the customer picture.
1050 * May be used by VCM to detect if this feature is available in Vik Booking.
1051 *
1052 * @return bool
1053 *
1054 * @since 1.15.3 (J) - 1.5.5 (WP)
1055 * @requires VCM >= 1.8.6
1056 */
1057 public function supportsProfileAvatar()
1058 {
1059 return true;
1060 }
1061
1062 /**
1063 * Tells whether Vik Booking is up to date to support the state/province.
1064 * May be used by VCM to detect if this feature is available in Vik Booking.
1065 *
1066 * @return bool
1067 *
1068 * @since 1.16.1 (J) - 1.6.1 (WP)
1069 * @requires VCM >= 1.8.12
1070 */
1071 public function supportsStateProvince()
1072 {
1073 return true;
1074 }
1075 }
1076