PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.7.2
VikBooking Hotel Booking Engine & PMS v1.7.2
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.7.2, at site/helpers/cpin.php

1,072 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->q($country));
440 } else {
441 array_push($clause, "`country_name` LIKE " . $this->dbo->q("%{$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 // convert any 2-char country code or country name into a 3-char country code
468 if (!empty($country) && (strlen($country) == 2 || strlen($country) > 3)) {
469 $country = $this->get3CharCountry($country);
470 }
471
472 /**
473 * In case the phone number is missing the international prefix at the beginning of the string,
474 * mostly happens in case of OTA bookings, we pre-pend the country prefix.
475 *
476 * @since 1.12 (patch October 2019)
477 */
478 $phone_number = trim($phone_number);
479 if (!empty($phone_number) && !empty($country) && substr($phone_number, 0, 1) != '+' && substr($phone_number, 0, 2) != '00') {
480 // try to find the country phone prefix
481 $q = "SELECT `phone_prefix` FROM `#__vikbooking_countries` WHERE `country_" . (strlen($country) == 2 ? '2' : '3') . "_code`=" . $this->dbo->quote($country) . ";";
482 $this->dbo->setQuery($q);
483 $phone_prefix = $this->dbo->loadResult();
484 if ($phone_prefix) {
485 $country_prefix = str_replace(' ', '', $phone_prefix);
486 $num_prefix = str_replace('+', '', $country_prefix);
487 if (substr($phone_number, 0, strlen($num_prefix)) != $num_prefix) {
488 // country prefix is completely missing
489 $phone_number = $country_prefix . $phone_number;
490 } else {
491 // try to prepend the plus symbol because the phone number starts with the country prefix
492 $phone_number = '+' . $phone_number;
493 }
494 }
495 }
496
497 // check if the customer exists
498 $customer = $this->customerExists($email, $first_name, $last_name);
499
500 // access the current user
501 $user = JFactory::getUser();
502
503 if ($customer === false) {
504 $new_pin = $this->generateUniquePin();
505
506 // build customer record
507 $customer_obj = new stdClass;
508 $customer_obj->first_name = $first_name;
509 $customer_obj->last_name = $last_name;
510 $customer_obj->email = $email;
511 $customer_obj->phone = $phone_number;
512 $customer_obj->country = $country;
513 $customer_obj->cfields = is_array($cfields) && $cfields ? json_encode($cfields) : null;
514 $customer_obj->pin = $new_pin;
515 $customer_obj->ujid = $this->is_admin ? 0 : (int)$user->id;
516 // add the extra field flags
517 foreach ($this->fieldflags as $flagk => $flagv) {
518 if (is_array($flagv) || is_object($flagv)) {
519 $flagv = json_encode($flagv);
520 }
521 $customer_obj->{$flagk} = $flagv;
522 }
523
524 // trigger the customer before-insert event
525 $this->pluginCustomerSync(0, 'insert', (array)$customer_obj, $before = true);
526
527 // insert the new customer record
528 $this->dbo->insertObject('#__vikbooking_customers', $customer_obj, 'id');
529 $new_customer_id = isset($customer_obj->id) ? $customer_obj->id : null;
530
531 if (!empty($new_customer_id)) {
532 $this->setNewPin($new_pin);
533 $this->setNewCustomerId($new_customer_id);
534
535 // trigger the customer after-save event
536 $this->pluginCustomerSync($new_customer_id, 'insert', (array)$customer_obj, $before = false);
537 }
538 } elseif (is_array($customer) && $customer) {
539 $this->setNewPin($customer['pin']);
540 $this->setNewCustomerId($customer['id']);
541
542 // build customer record
543 $customer_obj = new stdClass;
544 $customer_obj->id = $customer['id'];
545 $customer_obj->first_name = $first_name;
546 $customer_obj->last_name = $last_name;
547 $customer_obj->email = $email;
548 $customer_obj->phone = $phone_number;
549 $customer_obj->country = $country;
550 if (!$this->is_admin) {
551 $customer_obj->cfields = is_array($cfields) && $cfields ? json_encode($cfields) : null;
552 }
553 $customer_obj->pin = $customer['pin'];
554 $customer_obj->ujid = $this->is_admin ? 0 : (int)$user->id;
555 // add the extra field flags
556 foreach ($this->fieldflags as $flagk => $flagv) {
557 if (is_array($flagv) || is_object($flagv)) {
558 $flagv = json_encode($flagv);
559 }
560 $customer_obj->{$flagk} = $flagv;
561 }
562
563 // trigger the customer before-update event
564 $this->pluginCustomerSync($customer['id'], 'update', (array)$customer_obj, $before = true);
565
566 // update customer record
567 $this->dbo->updateObject('#__vikbooking_customers', $customer_obj, 'id');
568
569 // trigger the customer after-save event
570 $this->pluginCustomerSync($customer['id'], 'update', (array)$customer_obj, $before = false);
571 }
572
573 //unset extra info
574 $this->fieldflags = [];
575
576 return !$this->is_admin ? $this->storeCustomerCookie() : true;
577 }
578
579 public function storeCustomerCookie()
580 {
581 $pin = $this->getNewPin();
582 $customer_id = $this->getNewCustomerId();
583 if (empty($pin) || empty($customer_id)) {
584 return false;
585 }
586 $this->setPinCookie($pin);
587 return true;
588 }
589
590 /**
591 * Stores a relation between the Customer ID and the Booking ID
592 * This method should be called after the saveCustomerDetails() because
593 * it requires the methods setNewPin and setNewCustomerId to be called before.
594 * Since VBO 1.9 this method also calculates and sets the commissions
595 * amount if the customer is a sales channel.
596 * Requires the records in _ordersrooms to be stored before being called.
597 *
598 * @param int orderid the ID of the VBO order
599 *
600 * @return boolean
601 */
602 public function saveCustomerBooking($orderid)
603 {
604 $pin = $this->getNewPin();
605 $customer_id = $this->getNewCustomerId();
606 if (empty($orderid) || empty($pin) || empty($customer_id)) {
607 return false;
608 }
609
610 $q = "SELECT * FROM `#__vikbooking_ordersrooms` WHERE `idorder`=".(int)$orderid.";";
611 $this->dbo->setQuery($q);
612 $orders_rooms = $this->dbo->loadAssocList();
613 if (!$orders_rooms) {
614 return false;
615 }
616
617 $q = "DELETE FROM `#__vikbooking_customers_orders` WHERE `idorder`=".$this->dbo->quote($orderid).";";
618 $this->dbo->setQuery($q);
619 $this->dbo->execute();
620
621 $q = "INSERT INTO `#__vikbooking_customers_orders` (`idcustomer`,`idorder`) VALUES(".$this->dbo->quote($customer_id).", ".$this->dbo->quote($orderid).");";
622 $this->dbo->setQuery($q);
623 $this->dbo->execute();
624
625 // 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
626 if (empty($orders_rooms[0]['t_first_name']) && empty($orders_rooms[0]['t_last_name'])) {
627 $customer_info = $this->getCustomerByID($customer_id);
628 $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;";
629 $this->dbo->setQuery($q);
630 $this->dbo->execute();
631
632 // update the country as well
633 if (!empty($customer_info['country'])) {
634 $q = "UPDATE `#__vikbooking_orders` SET `country`=".$this->dbo->quote($customer_info['country'])." WHERE `id`=".(int)$orderid.";";
635 $this->dbo->setQuery($q);
636 $this->dbo->execute();
637 }
638 }
639
640 // commissions for customers that are sales channels
641 $this->updateBookingCommissions($orderid, $customer_id);
642
643 return true;
644 }
645
646 /**
647 * Changes the customer assigned to the booking by
648 * re-calculating the amount of commissions (if any).
649 *
650 * @param int $orderid the booking id.
651 * @param int $customer_id the id of the new customer.
652 *
653 * @return boolean
654 *
655 * @since 1.11
656 */
657 public function updateCustomerBooking($orderid, $customer_id)
658 {
659 if (empty($orderid) || empty($customer_id)) {
660 return false;
661 }
662 $new_customer = $this->getCustomerByID($customer_id);
663 if (!count($new_customer)) {
664 // invalid customer ID given
665 return false;
666 }
667 $old_customer = $this->getCustomerFromBooking($orderid);
668 if (count($old_customer) && (int)$old_customer['ischannel'] > 0) {
669 // unset first the old commissions and channel name
670 $q = "UPDATE `#__vikbooking_orders` SET `channel`=NULL, `cmms`=NULL WHERE `id`=".(int)$orderid.";";
671 $this->dbo->setQuery($q);
672 $this->dbo->execute();
673 }
674 if (count($old_customer)) {
675 // update reference
676 $q = "UPDATE `#__vikbooking_customers_orders` SET `idcustomer`=".(int)$new_customer['id']." WHERE `idorder`=".(int)$orderid.";";
677 $this->dbo->setQuery($q);
678 $this->dbo->execute();
679 if (!empty($new_customer['country']) && strlen($new_customer['country']) == 3) {
680 // update booking country
681 $q = "UPDATE `#__vikbooking_orders` SET `country`=" . $this->dbo->quote($new_customer['country']) . " WHERE `id`=" . (int)$orderid . ";";
682 $this->dbo->setQuery($q);
683 $this->dbo->execute();
684 }
685 } else {
686 // insert relation
687 $q = "INSERT INTO `#__vikbooking_customers_orders` (`idcustomer`,`idorder`) VALUES(".(int)$new_customer['id'].", ".(int)$orderid.");";
688 $this->dbo->setQuery($q);
689 $this->dbo->execute();
690 if (!empty($new_customer['country']) && strlen($new_customer['country']) == 3) {
691 // update booking country
692 $q = "UPDATE `#__vikbooking_orders` SET `country`=" . $this->dbo->quote($new_customer['country']) . " WHERE `id`=" . (int)$orderid . ";";
693 $this->dbo->setQuery($q);
694 $this->dbo->execute();
695 }
696 }
697
698 // update commissions and sale channel information
699 return $this->updateBookingCommissions($orderid, $new_customer['id']);
700 }
701
702 /**
703 * Calculates and sets the commissions amount if the customer
704 * is a "sales channel" with a percentage greater than 0.
705 *
706 * @param int $orderid
707 * @param int $customer_id
708 *
709 * @since 1.9
710 */
711 public function updateBookingCommissions($orderid, $customer_id = 0)
712 {
713 if (empty($customer_id)) {
714 $customer_id = $this->getNewCustomerId();
715 }
716
717 if (empty($orderid) || empty($customer_id)) {
718 return false;
719 }
720
721 $customer = $this->getCustomerByID($customer_id);
722 if ((int)$customer['ischannel'] > 0 && !empty($customer['chdata']) && isset($customer['chdata']['commission']) && $customer['chdata']['commission'] > 0.00) {
723 $q = "SELECT * FROM `#__vikbooking_orders` WHERE `id`=".$this->dbo->quote($orderid).";";
724 $this->dbo->setQuery($q);
725 $order_info = $this->dbo->loadAssoc();
726 if ($order_info) {
727 if ((float)$order_info['total'] > 0.00) {
728 $cmms_calc_base = $this->calcCommissionsBaseAmount($order_info, $customer);
729 $cmms_amount = $cmms_calc_base * $customer['chdata']['commission'] / 100;
730 $source_name = 'customer'.$customer['id'].(array_key_exists('chname', $customer['chdata']) ? '_'.$customer['chdata']['chname'] : '');
731
732 $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']).";";
733 $this->dbo->setQuery($q);
734 $this->dbo->execute();
735
736 return true;
737 }
738 }
739 }
740
741 return false;
742 }
743
744 /**
745 * Calculates the base amount on which the commissions should be applied.
746 * Considers the parameters for this customer sales channel for taxes and rooms rates.
747 *
748 * @param array $order_info
749 * @param array $customer
750 */
751 private function calcCommissionsBaseAmount($order_info, $customer)
752 {
753 $cmms_calc_base = $order_info['total'];
754
755 if (!empty($customer['chdata']) && array_key_exists('calccmmon', $customer['chdata']) && (int)$customer['chdata']['calccmmon'] > 0) {
756 // commissions based on room rates only
757 $q = "SELECT `or`.* FROM `#__vikbooking_ordersrooms` AS `or` WHERE `or`.`idorder`='" . (int)$order_info['id'] . "';";
758 $this->dbo->setQuery($q);
759 $order_rooms = $this->dbo->loadAssocList();
760 if ($order_rooms) {
761 $map_cost_tax = [];
762 foreach ($order_rooms as $or) {
763 if (!((float)$or['room_cost'] > 0.00) && !((float)$or['cust_cost'] > 0.00)) {
764 // missing information about the room cost - cannot proceed
765 continue;
766 }
767 $map_cost_tax[] = array(
768 'amount' => ((float)$or['room_cost'] > 0.00 ? $or['room_cost'] : $or['cust_cost']),
769 'taxid' => (!empty($or['cust_cost']) ? intval($or['cust_idiva']) : $this->getTaxIdFromTar($or['idtar']))
770 );
771 }
772 if (count($map_cost_tax) == count($order_rooms)) {
773 // all rooms have a custom cost or a room cost set. We can proceed
774 if (array_key_exists('applycmmon', $customer['chdata']) && (int)$customer['chdata']['applycmmon'] > 0) {
775 // commissions based on amounts tax excluded
776 if ($this->pricesTaxIncluded()) {
777 // prices are tax included so update the amounts in $map_cost_tax
778 foreach ($map_cost_tax as $ctk => $ctv) {
779 if (!((float)$ctv['amount'] > 0.00) || empty($ctv['taxid'])) {
780 continue;
781 }
782 list($aliq, $taxcap) = $this->getAliqFromTaxId($ctv['taxid']);
783 if ((float)$aliq > 0.00) {
784 $op_div = (100 + $aliq) / 100;
785 $tmp_op = $ctv['amount'] / $op_div;
786 /**
787 * Tax Cap implementation
788 *
789 * @since 1.12
790 */
791 if ($taxcap > 0 && ($ctv['amount'] - $tmp_op) > $taxcap) {
792 $tmp_op = $ctv['amount'] - $taxcap;
793 }
794 $map_cost_tax[$ctk]['amount'] = $tmp_op;
795 }
796 }
797 }
798 }
799 // sum all the amounts to get the base amount where commissions will be applied
800 $sum = 0;
801 foreach ($map_cost_tax as $k => $map) {
802 $sum += $map['amount'];
803 }
804 $cmms_calc_base = $sum;
805 }
806 }
807 }
808
809 return $cmms_calc_base;
810 }
811
812 private function pricesTaxIncluded()
813 {
814 if (!class_exists('VikBooking')) {
815 require_once(VBO_SITE_PATH.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.'lib.vikbooking.php');
816 }
817 return VikBooking::ivaInclusa();
818 }
819
820 /**
821 * Retrieves the aliquot and tax cap of the tax ID passed.
822 * Returns 0 if nothing is found, the Tax Aliq otherwise.
823 *
824 * @param int $taxid the ID of the tax rate
825 *
826 * @return array the aliquote as 0th array value, the tax cap as 1st value.
827 *
828 * @since 1.12 this private method used to return just the aliquote.
829 */
830 private function getAliqFromTaxId($taxid)
831 {
832 $aliq = 0;
833 $taxcap = 0;
834 if (intval($taxid) > 0) {
835 $q = "SELECT `i`.`aliq`,`i`.`taxcap` FROM `#__vikbooking_iva` AS `i` WHERE `i`.`id`=" . (int)$taxid;
836 $this->dbo->setQuery($q, 0, 1);
837 $tax_info = $this->dbo->loadAssoc();
838 if ($tax_info) {
839 $aliq = $tax_info['aliq'];
840 $taxcap = $tax_info['taxcap'];
841 }
842 }
843
844 return [$aliq, $taxcap];
845 }
846
847 /**
848 * Retrieves the ID of the tax used for the tariff passed.
849 * Returns 0 if nothing is found, the Tax ID otherwise.
850 *
851 * @param int $idtar
852 */
853 private function getTaxIdFromTar($idtar)
854 {
855 $taxid = 0;
856 if (intval($idtar) > 0) {
857 $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;
858 $this->dbo->setQuery($q, 0, 1);
859 $tax_info = $this->dbo->loadAssoc();
860 if ($tax_info) {
861 $taxid = (int)$tax_info['idiva'];
862 }
863 }
864
865 return $taxid;
866 }
867
868 /**
869 * Takes the Customer PIN from the Order ID
870 *
871 * @param int $orderid
872 */
873 public function getPinCodeByOrderId($orderid)
874 {
875 $pin = '';
876 if (!empty($orderid)) {
877 $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;";
878 $this->dbo->setQuery($q);
879 $custdata = $this->dbo->loadAssocList();
880 if ($custdata) {
881 if (!empty($custdata[0]['pin'])) {
882 $pin = $custdata[0]['pin'];
883 }
884 }
885 }
886
887 return $pin;
888 }
889
890 /**
891 * Triggers the events to sync customers accross multiple plugins.
892 *
893 * @param int $customer_id
894 * @param string $mode
895 * @param array $data optional customer data associative array.
896 * @param bool $before whether it's before or after saving the customer.
897 *
898 * @since 1.16.7 (J) - 1.6.7 (WP) introduced args $data and $before.
899 */
900 public function pluginCustomerSync($customer_id, $mode, $data = [], $before = true)
901 {
902 if ($data) {
903 $customer = $data;
904 } else {
905 $q = "SELECT * FROM `#__vikbooking_customers` WHERE `id`=" . (int)$customer_id;
906 $this->dbo->setQuery($q, 0, 1);
907 $customer = $this->dbo->loadAssoc();
908 if (!$customer) {
909 return false;
910 }
911 }
912
913 // make sure to import this type of plugins
914 JPluginHelper::importPlugin('e4j');
915
916 // get the event name
917 if ($mode == 'insert') {
918 // trigger plugin -> customer creation
919 $ev_name = 'onCustomerInsert';
920 } elseif ($mode == 'update') {
921 // trigger plugin -> customer update
922 $ev_name = 'onCustomerUpdate';
923 } elseif ($mode == 'delete') {
924 // trigger plugin -> customer delete
925 $ev_name = 'onCustomerDelete';
926 } else {
927 return false;
928 }
929
930 // event options
931 $options = [
932 'alias' => 'com_vikbooking',
933 'version' => (defined('VIKBOOKING_SOFTWARE_VERSION') ? VIKBOOKING_SOFTWARE_VERSION : E4J_SOFTWARE_VERSION),
934 'admin' => VikBooking::isAdmin(),
935 'call' => __FUNCTION__,
936 ];
937
938 try {
939 if ($before) {
940 /**
941 * Trigger event for the imminent creation, update or deletion of the customer.
942 */
943 VBOFactory::getPlatform()->getDispatcher()->trigger($ev_name, [$customer, $options]);
944 } elseif (!$before && ($mode === 'insert' || $mode === 'update')) {
945 /**
946 * Trigger event after the creation or the update of the customer.
947 */
948 VBOFactory::getPlatform()->getDispatcher()->trigger('onAfterSaveCustomer', [$customer, ($mode === 'insert')]);
949 }
950 } catch (Throwable $e) {
951 // do nothing
952 }
953
954 return true;
955 }
956
957 /**
958 * Sets the current customer PIN
959 *
960 * @param string $pin
961 */
962 public function setNewPin($pin = '')
963 {
964 $this->new_pin = $pin;
965 }
966
967 /**
968 * Get the current customer PIN
969 */
970 public function getNewPin()
971 {
972 return $this->new_pin;
973 }
974
975 /**
976 * Sets the current customer ID
977 *
978 * @param int $cid
979 */
980 public function setNewCustomerId($cid = 0)
981 {
982 $this->new_customer_id = (int)$cid;
983 }
984
985 /**
986 * Get the current customer ID
987 */
988 public function getNewCustomerId()
989 {
990 return $this->new_customer_id;
991 }
992
993 /**
994 * Explanation of the XML error
995 *
996 * @param object $error
997 */
998 public function libxml_display_error($error)
999 {
1000 $return = "\n";
1001 switch ($error->level) {
1002 case LIBXML_ERR_WARNING :
1003 $return .= "Warning ".$error->code.": ";
1004 break;
1005 case LIBXML_ERR_ERROR :
1006 $return .= "Error ".$error->code.": ";
1007 break;
1008 case LIBXML_ERR_FATAL :
1009 $return .= "Fatal Error ".$error->code.": ";
1010 break;
1011 }
1012 $return .= trim($error->message);
1013 if ($error->file) {
1014 $return .= " in ".$error->file;
1015 }
1016 $return .= " on line ".$error->line."\n";
1017 return $return;
1018 }
1019
1020 /**
1021 * Get the XML errors occurred
1022 */
1023 public function libxml_display_errors()
1024 {
1025 $errorstr = "";
1026 $errors = libxml_get_errors();
1027 foreach ($errors as $error) {
1028 $errorstr .= $this->libxml_display_error($error);
1029 }
1030 libxml_clear_errors();
1031 return $errorstr;
1032 }
1033
1034 private function setError($str)
1035 {
1036 $this->error .= $str."\n";
1037 }
1038
1039 public function getError()
1040 {
1041 return nl2br(rtrim($this->error, "\n"));
1042 }
1043
1044 /**
1045 * Tells whether Vik Booking is up to date to support the customer picture.
1046 * May be used by VCM to detect if this feature is available in Vik Booking.
1047 *
1048 * @return bool
1049 *
1050 * @since 1.15.3 (J) - 1.5.5 (WP)
1051 * @requires VCM >= 1.8.6
1052 */
1053 public function supportsProfileAvatar()
1054 {
1055 return true;
1056 }
1057
1058 /**
1059 * Tells whether Vik Booking is up to date to support the state/province.
1060 * May be used by VCM to detect if this feature is available in Vik Booking.
1061 *
1062 * @return bool
1063 *
1064 * @since 1.16.1 (J) - 1.6.1 (WP)
1065 * @requires VCM >= 1.8.12
1066 */
1067 public function supportsStateProvince()
1068 {
1069 return true;
1070 }
1071 }
1072