PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.6.8
VikBooking Hotel Booking Engine & PMS v1.6.8
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 / history.php

history.php in VikBooking Hotel Booking Engine & PMS 1.6.8, at site/helpers/history.php

1,031 lines 28.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 * Handles all the events involving a reservation.
15 */
16 class VboBookingHistory
17 {
18 /**
19 * @var int
20 */
21 protected $bid = null;
22
23 /**
24 * @var array
25 */
26 protected $prevBooking = null;
27
28 /**
29 * @var mixed
30 */
31 protected $data = null;
32
33 /**
34 * @var object
35 */
36 protected $dbo = null;
37
38 /**
39 * @var array
40 */
41 protected $typesMap = [];
42
43 /**
44 * List of event types worthy of a notification.
45 *
46 * @var array
47 *
48 * @since 1.15.0 (J) - 1.5.0 (WP)
49 */
50 protected $worthy_events = [
51 'NC',
52 'MW',
53 'NP',
54 'P0',
55 'PN',
56 'CR',
57 'CW',
58 'MC',
59 'CC',
60 'NO',
61 'PO',
62 'IR',
63 'PC',
64 'UR',
65 ];
66
67 /**
68 * List of event types supported by the Notifications Center.
69 *
70 * @var array
71 *
72 * @since 1.16.8 (J) - 1.6.8 (WP)
73 */
74 protected $notificationsCenterTypes = [
75 // New booking with status Confirmed
76 'NC',
77 // Booking paid for the first time
78 'P0',
79 // Booking paid for a second time
80 'PN',
81 // Booking modified from website
82 'MW',
83 // Cancellation request message
84 'CR',
85 // Booking cancelled via front-end website
86 'CW',
87 // Booking modified from channel
88 'MC',
89 // Booking cancelled from channel
90 'CC',
91 // New Booking from OTA
92 'NO',
93 // Overbooking
94 'OB',
95 // Channel Manager payout notification
96 'PO',
97 ];
98
99 /**
100 * Class constructor.
101 *
102 * @param int $bid optional booking ID to set.
103 *
104 * @since 1.16.5 (J) - 1.6.5 (WP) added argument $bid.
105 */
106 public function __construct($bid = 0)
107 {
108 $this->dbo = JFactory::getDbo();
109 $this->typesMap = $this->getTypesMap();
110
111 if ($bid) {
112 $this->bid = (int)$bid;
113 }
114 }
115
116 /**
117 * Returns an array of types mapped to
118 * the corresponding language definition.
119 * All the history types should be listed here.
120 *
121 * @return array
122 */
123 public function getTypesMap()
124 {
125 return [
126 // New booking with status Confirmed
127 'NC' => JText::translate('VBOBOOKHISTORYTNC'),
128 // Booking modified from website
129 'MW' => JText::translate('VBOBOOKHISTORYTMW'),
130 // Booking modified from back-end
131 'MB' => JText::translate('VBOBOOKHISTORYTMB'),
132 // New booking from back-end
133 'NB' => JText::translate('VBOBOOKHISTORYTNB'),
134 // New booking with status Pending
135 'NP' => JText::translate('VBOBOOKHISTORYTNP'),
136 // Booking paid for the first time
137 'P0' => JText::translate('VBOBOOKHISTORYTP0'),
138 // Booking paid for a second time
139 'PN' => JText::translate('VBOBOOKHISTORYTPN'),
140 // Cancellation request message
141 'CR' => JText::translate('VBOBOOKHISTORYTCR'),
142 // Booking cancelled via front-end website
143 'CW' => JText::translate('VBOBOOKHISTORYTCW'),
144 // Booking auto cancelled via front-end
145 'CA' => JText::translate('VBOBOOKHISTORYTCA'),
146 // Booking cancelled via back-end by admin
147 'CB' => JText::translate('VBOBOOKHISTORYTCB'),
148 // Booking Receipt generated via back-end
149 'BR' => JText::translate('VBOBOOKHISTORYTBR'),
150 // Booking Invoice generated
151 'BI' => JText::translate('VBOBOOKHISTORYTBI'),
152 // Booking registration unset status by admin
153 'RA' => JText::translate('VBOBOOKHISTORYTRA'),
154 // Booking checked-in status set by admin
155 'RB' => JText::translate('VBOBOOKHISTORYTRB'),
156 // Booking checked-out status set by admin
157 'RC' => JText::translate('VBOBOOKHISTORYTRC'),
158 // Booking no-show status set by admin
159 'RZ' => JText::translate('VBOBOOKHISTORYTRZ'),
160 // Booking set to Confirmed by admin
161 'TC' => JText::translate('VBOBOOKHISTORYTTC'),
162 // Booking set to Confirmed via App
163 'AC' => JText::translate('VBOBOOKHISTORYTAC'),
164 // Booking modified from channel
165 'MC' => JText::translate('VBOBOOKHISTORYTMC'),
166 // Booking cancelled from channel
167 'CC' => JText::translate('VBOBOOKHISTORYTCC'),
168 // Booking removed via App
169 'AR' => JText::translate('VBOBOOKHISTORYTAR'),
170 // Booking modified via App
171 'AM' => JText::translate('VBOBOOKHISTORYTAM'),
172 // New booking via App
173 'AN' => JText::translate('VBOBOOKHISTORYTAN'),
174 // New Booking from OTA
175 'NO' => JText::translate('VBOBOOKHISTORYTNO'),
176 // Report affecting the booking
177 'RP' => JText::translate('VBOBOOKHISTORYTRP'),
178 // Custom email sent to the customer by admin
179 'CE' => JText::translate('VBOBOOKHISTORYTCE'),
180 // Custom SMS sent to the customer by admin
181 'CS' => JText::translate('VBOBOOKHISTORYTCS'),
182 // Confirmation email re-sent by admin to guest
183 'ER' => JText::translate('VBRESENDORDEMAIL'),
184 // Payment Update (a new amount paid has been set)
185 'PU' => JText::translate('VBOBOOKHISTORYTPU'),
186 // Upsell Extras via front-end
187 'UE' => JText::translate('VBOBOOKHISTORYTUE'),
188 // Report guest misconduct to OTA
189 'GM' => JText::translate('VBOBOOKHISTORYTGM'),
190 // Send Email Cancellation by admin
191 'EC' => JText::translate('VBOBOOKHISTORYTEC'),
192 // Amount refunded
193 'RF' => JText::translate('VBOBOOKHISTORYTRF'),
194 // Refund Updated
195 'RU' => JText::translate('VBOBOOKHISTORYTRU'),
196 // Payable Amount Updated
197 'PB' => JText::translate('VBOBOOKHISTORYTPB'),
198 // Channel Manager custom event
199 'CM' => JText::translate('VBOBOOKHISTORYTCM'),
200 // Channel Manager payout notification
201 'PO' => JText::translate('VBOBOOKHISTORYTPO'),
202 // Inquiry reservation (website)
203 'IR' => JText::translate('VBOBOOKHISTORYTIR'),
204 // Pre-checkin updated via front-end
205 'PC' => JText::translate('VBOBOOKHISTORYTPC'),
206 // Guest review received
207 'GR' => JText::translate('VBOBOOKHISTORYTGR'),
208 // Upgrade room
209 'UR' => JText::translate('VBOBOOKHISTORYTUR'),
210 // Overbooking
211 'OB' => JText::translate('VBOBOOKHISTORYTOB'),
212 ];
213 }
214
215 /**
216 * Accesses the groups of history event types, useful to group multiple events.
217 * Groups are categories of events containing worthy event types of the same context.
218 *
219 * @param string $group get the list of event types of this group.
220 *
221 * @return array associative array of types group or empty array.
222 *
223 * @since 1.16.0 (J) - 1.6.0 (WP)
224 */
225 public function getTypeGroups($group = null)
226 {
227 // we group various types of events per category
228 $type_groups = [
229 // group "Bookings"
230 'GBK' => [
231 'name' => JText::translate('VBMENUTHREE'),
232 'types' => [
233 'NC',
234 'MW',
235 'NP',
236 'CR',
237 'CW',
238 'MC',
239 'CC',
240 'NO',
241 'IR',
242 'UR',
243 'OB',
244 ],
245 ],
246 // group "Channel Manager"
247 'GCM' => [
248 'name' => JText::translate('VBMENUCHANNELMANAGER'),
249 'types' => [
250 'MC',
251 'CC',
252 'NO',
253 'PO',
254 'CM',
255 'OB',
256 ],
257 ],
258 // group "Website"
259 'GWB' => [
260 'name' => JText::translate('VBORDFROMSITE'),
261 'types' => [
262 'NC',
263 'MW',
264 'NP',
265 'CR',
266 'CW',
267 'IR',
268 'PC',
269 'UE',
270 'GR',
271 'UR',
272 ],
273 ],
274 // group "Payments"
275 'GPM' => [
276 'name' => JText::translate('VBO_HISTORY_GPM'),
277 'types' => [
278 'P0',
279 'PN',
280 'PO',
281 'RF',
282 ],
283 ],
284 ];
285
286 if (!empty($group)) {
287 // attempt to fetch the requested group
288 return isset($type_groups[$group]) ? $type_groups[$group] : [];
289 }
290
291 // return the whole associative list
292 return $type_groups;
293 }
294
295 /**
296 * Sets the current booking ID.
297 *
298 * @param int $bid
299 *
300 * @return self
301 **/
302 public function setBid($bid)
303 {
304 $this->bid = (int)$bid;
305
306 return $this;
307 }
308
309 /**
310 * Sets the previous booking array.
311 * To calculate what has changed in the booking after the
312 * modification, VBO uses the method getLogBookingModification().
313 * VCM instead should use this method to tell the class that
314 * what has changed should be calculated to obtain the 'descr'
315 * text of the history record that will be stored.
316 *
317 * @param array $booking
318 *
319 * @return self
320 **/
321 public function setPrevBooking($booking)
322 {
323 if (is_array($booking)) {
324 $this->prevBooking = $booking;
325 }
326
327 return $this;
328 }
329
330 /**
331 * Sets extra data for the current history log.
332 *
333 * @param mixed $data array, object or string of extra data.
334 * Useful to store the amount paid to be invoiced,
335 * or the transaction details of the payments.
336 *
337 * @return self
338 **/
339 public function setExtraData($data)
340 {
341 $this->data = $data;
342
343 return $this;
344 }
345
346 /**
347 * Checks whether the type for the history record is valid.
348 *
349 * @param string $type
350 * @param [bool] $returnit if true, the translated value is returned. Otherwise boolean is returned
351 *
352 * @return boolean
353 */
354 public function validType($type, $returnit = false)
355 {
356 if ($returnit) {
357 return isset($this->typesMap[strtoupper($type)]) ? $this->typesMap[strtoupper($type)] : $type;
358 }
359
360 return isset($this->typesMap[strtoupper($type)]);
361 }
362
363 /**
364 * Reads the current booking record.
365 *
366 * @param mixed
367 *
368 * @return false|array
369 */
370 protected function getBookingInfo()
371 {
372 $q = $this->dbo->getQuery(true)
373 ->select('*')
374 ->from($this->dbo->qn('#__vikbooking_orders'))
375 ->where($this->dbo->qn('id') . ' = ' . (int)$this->bid);
376
377 $this->dbo->setQuery($q, 0, 1);
378 $record = $this->dbo->loadAssoc();
379
380 if (!$record) {
381 return false;
382 }
383
384 return $record;
385 }
386
387 /**
388 * Stores a new history record for the booking.
389 *
390 * @param string $type the char-type of store we are making for the history
391 * @param [string] $descr the description of this booking record (optional)
392 *
393 * @return boolean
394 */
395 public function store($type, $descr = '')
396 {
397 if (is_null($this->bid) || !$this->validType($type)) {
398 return false;
399 }
400
401 if (!$booking_info = $this->getBookingInfo()) {
402 return false;
403 }
404
405 if (empty($descr) && $this->prevBooking) {
406 /**
407 * VCM (including the App) could set the previous booking information,
408 * so we need to calculate what has changed with the booking.
409 * Load VBO language.
410 */
411 $lang = JFactory::getLanguage();
412 $lang->load('com_vikbooking', (VBOPlatformDetection::isWordPress() ? VIKBOOKING_ADMIN_LANG : JPATH_ADMINISTRATOR), $lang->getTag(), true);
413 if (!class_exists('VikBooking')) {
414 require_once(VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'lib.vikbooking.php');
415 }
416 $descr = VikBooking::getLogBookingModification($this->prevBooking);
417 }
418
419 // get the event dispatcher
420 $dispatcher = VBOFactory::getPlatform()->getDispatcher();
421
422 // build record object
423 $history_record = new stdClass;
424 $history_record->idorder = $this->bid;
425 $history_record->dt = JFactory::getDate()->toSql();
426 $history_record->type = $type;
427 $history_record->descr = $descr ?: null;
428 $history_record->totpaid = (float)$booking_info['totpaid'];
429 $history_record->total = (float)$booking_info['total'];
430 $history_record->data = $this->data;
431
432 /**
433 * Trigger event before saving a new history record.
434 *
435 * @since 1.16.0 (J) - 1.6.0 (WP)
436 */
437 $results = $dispatcher->filter('onBeforeSaveBookingHistoryVikBooking', [$history_record, $this->prevBooking]);
438 if (in_array(false, $results, true)) {
439 return false;
440 }
441
442 /**
443 * Store the extra data for this history log. Useful to store
444 * information about the amount paid and its invoicing status.
445 *
446 * @since 1.12 (J) - 1.1.7 (WP)
447 */
448 if (!is_null($history_record->data) && !is_scalar($history_record->data)) {
449 $history_record->data = json_encode($history_record->data);
450 }
451
452 // store record
453 if (!$this->dbo->insertObject('#__vikbooking_orderhistory', $history_record, 'id') || !isset($history_record->id)) {
454 return false;
455 }
456
457 /**
458 * Trigger event for the newly history record added.
459 *
460 * @since 1.16.0 (J) - 1.6.0 (WP)
461 */
462 $dispatcher->trigger('onAfterSaveBookingHistoryVikBooking', [$history_record, $this->prevBooking]);
463
464 /**
465 * Check if the event requires an entry to be stored in the Notifications Center.
466 *
467 * @since 1.16.8 (J) - 1.6.8 (WP)
468 */
469 $results = $dispatcher->filter('onBeforeSaveHistoryNotificationsCenter', [$history_record, $this->notificationsCenterTypes]);
470 if (in_array($type, $this->notificationsCenterTypes) || in_array(true, $results, true)) {
471 $this->addToNotificationsCenter($history_record, $booking_info);
472 }
473
474 return true;
475 }
476
477 /**
478 * Loads the history records for the booking ID set.
479 *
480 * @param int $offset query offset limit start.
481 * @param int $limit query maximum records to fetch.
482 *
483 * @return array
484 *
485 * @since 1.16.5 (J) - 1.6.5 (WP) added arguments $offset and $limit.
486 */
487 public function loadHistory($offset = 0, $limit = 0)
488 {
489 if (empty($this->bid)) {
490 return [];
491 }
492
493 $q = $this->dbo->getQuery(true)
494 ->select('*')
495 ->from($this->dbo->qn('#__vikbooking_orderhistory'))
496 ->where($this->dbo->qn('idorder') . ' = ' . (int)$this->bid)
497 ->order($this->dbo->qn('dt') . ' DESC')
498 ->order($this->dbo->qn('id') . ' DESC');
499
500 $this->dbo->setQuery($q, $offset, $limit);
501
502 return $this->dbo->loadAssocList();
503 }
504
505 /**
506 * Checks whether this booking has an event of the given type.
507 *
508 * @param string $type the type of the event.
509 *
510 * @return mixed last date on success, false otherwise.
511 *
512 * @since 1.13.5 (J) - 1.3.5 (WP)
513 */
514 public function hasEvent($type)
515 {
516 if (empty($type) || empty($this->bid)) {
517 return false;
518 }
519
520 $q = $this->dbo->getQuery(true)
521 ->select($this->dbo->qn('dt'))
522 ->from($this->dbo->qn('#__vikbooking_orderhistory'))
523 ->where($this->dbo->qn('idorder') . ' = ' . (int)$this->bid)
524 ->where($this->dbo->qn('type') . ' = ' . $this->dbo->q($type))
525 ->order($this->dbo->qn('dt') . ' DESC');
526
527 $this->dbo->setQuery($q, 0, 1);
528
529 $dt = $this->dbo->loadResult();
530
531 if (!$dt) {
532 return false;
533 }
534
535 return $dt;
536 }
537
538 /**
539 * Returns a list of records with data defined for the given event type.
540 * Useful to get a list of transactions data for the refund operations.
541 *
542 * @param mixed $type string or array event type(s).
543 * @param callable $callvalid callback for the data validation.
544 * @param bool $onlydata whether to get just the event data.
545 *
546 * @return mixed false or array with data records.
547 *
548 * @since 1.14.0 (J) - 1.4.0 (WP)
549 */
550 public function getEventsWithData($type, $callvalid = null, $onlydata = true)
551 {
552 if (empty($type) || empty($this->bid)) {
553 return false;
554 }
555
556 if (!is_array($type)) {
557 $type = [$type];
558 }
559
560 // quote all given types
561 $types = array_map([$this->dbo, 'q'], $type);
562
563 $q = $this->dbo->getQuery(true)
564 ->select('*')
565 ->from($this->dbo->qn('#__vikbooking_orderhistory'))
566 ->where($this->dbo->qn('idorder') . ' = ' . (int)$this->bid)
567 ->where($this->dbo->qn('type') . ' IN (' . implode(', ', $types) . ')')
568 ->order($this->dbo->qn('dt') . ' ASC');
569
570 $this->dbo->setQuery($q);
571 $events = $this->dbo->loadAssocList();
572
573 if ($events) {
574 $datas = [];
575 foreach ($events as $k => $e) {
576 $data = json_decode($e['data']);
577 if (!empty($data)) {
578 $events[$k]['data'] = $data;
579 }
580 $valid_data = true;
581 if (is_callable($callvalid)) {
582 $valid_data = call_user_func($callvalid, $events[$k]['data']);
583 }
584 if ($valid_data) {
585 array_push($datas, $events[$k]['data']);
586 }
587 }
588
589 return $onlydata ? $datas : $events;
590 }
591
592 return false;
593 }
594
595 /**
596 * Gets the latest history event per booking within a list.
597 * Only the latest event per booking will be considered.
598 *
599 * @param int $start the query start offset.
600 * @param int $limit the query limit.
601 * @param int $min_id optional minimum history ID to fetch.
602 * @param array $types optional list of event types (or groups) to fetch.
603 *
604 * @return array the list of recent history record objects.
605 *
606 * @since 1.15.0 (J) - 1.5.0 (WP)
607 * @since 1.16.0 (J) - 1.6.0 (WP) query modified to fetch the customer picture.
608 * @since 1.16.0 (J) - 1.6.0 (WP) fourth argument $types introduced.
609 */
610 public function getLatestBookingEvents($start = 0, $limit = 20, $min_id = 0, array $types = [])
611 {
612 $clauses = [];
613
614 if ($min_id > 0) {
615 $clauses[] = "`h`.`id` > " . (int)$min_id;
616 } else {
617 $clauses[] = "`h`.`id` > 0";
618 }
619
620 if ($types) {
621 // scan for groups
622 $groups = [];
623 foreach ($types as $k => $type) {
624 if ($type && strlen($type) === 3 && $group = $this->getTypeGroups($type)) {
625 $groups = array_merge($groups, $group['types']);
626 unset($types[$k]);
627 }
628 }
629
630 if ($groups) {
631 // merge all types
632 $types = array_merge(array_values($types), array_unique($groups));
633 }
634
635 // quote all values
636 $types = array_map(array($this->dbo, 'q'), array_filter($types));
637
638 if ($types) {
639 // add query clause
640 $clauses[] = "`h`.`type` IN (" . implode(', ', $types) . ")";
641 }
642 }
643
644 /**
645 * On some servers with just 30k records in the history and 5k in bookings,
646 * the INNER JOIN to group the latest history events by booking ID is taking
647 * ~25 seconds to complete, which is not acceptable. This doesn't seem to happen
648 * on every database, but since the admin widget "latest events" requires to get
649 * just the latest history event ID by passing the limit to 1, we should not
650 * use the INNER JOIN all the times to save server resources.
651 * Refactoring performed to abandon also the LEFT JOINS which were timing out in
652 * case of several thousands of records. The widget "latest_events" crashed websites.
653 *
654 * @since 1.15.6 (J) - 1.5.12 (WP)
655 * @since 1.16.0 (J) - 1.6.0 (WP) INNER JOIN dismissed because too slow and not needed.
656 * @since 1.16.1 (J) - 1.6.1 (WP) LEFT JOIN dismissed because too slow, queries split.
657 */
658
659 // query just the history records without joining any other table, or the records would become hundreds of thousands
660 $q = $this->dbo->getQuery(true)
661 ->select($this->dbo->qn('h') . '.*')
662 ->from($this->dbo->qn('#__vikbooking_orderhistory', 'h'))
663 ->where(implode(' AND ', $clauses))
664 ->order($this->dbo->qn('h.dt') . ' DESC')
665 ->order($this->dbo->qn('h.id') . ' DESC');
666
667 $this->dbo->setQuery($q, $start, $limit);
668 $rows = $this->dbo->loadObjectList();
669 if (!$rows) {
670 return [];
671 }
672
673 // build a list of reservation IDs
674 $history_orders = [];
675 foreach ($rows as $row) {
676 $history_orders[] = (int)$row->idorder;
677 }
678 $history_orders = array_unique($history_orders);
679
680 // fetch the rest of the information
681 $q = "SELECT `o`.`id` AS `idorder`, `o`.`custdata`, `o`.`status`, `o`.`days`, `o`.`checkin`, `o`.`checkout`, `o`.`totpaid`,
682 `o`.`total`, `o`.`idorderota`, `o`.`channel`, `o`.`country`, `o`.`closure`, `o`.`type` AS `booking_type`, `c`.`pic`,
683 (
684 SELECT CONCAT_WS(' ',`or`.`t_first_name`,`or`.`t_last_name`)
685 FROM `#__vikbooking_ordersrooms` AS `or`
686 WHERE `or`.`idorder`=`o`.`id` LIMIT 1
687 ) AS `nominative`
688 FROM `#__vikbooking_orders` AS `o`
689 LEFT JOIN `#__vikbooking_customers_orders` AS `co` ON `co`.`idorder`=`o`.`id`
690 LEFT JOIN `#__vikbooking_customers` AS `c` ON `c`.`id`=`co`.`idcustomer`
691 WHERE `o`.`id` IN (" . implode(', ', $history_orders) . ");";
692
693 $this->dbo->setQuery($q);
694 $rows_data = $this->dbo->loadObjectList();
695 if (!$rows_data) {
696 return [];
697 }
698
699 // merge information to object records
700 foreach ($rows as &$row) {
701 foreach ($rows_data as $row_data) {
702 if ($row_data->idorder != $row->idorder) {
703 continue;
704 }
705
706 // merge object properties by using casting
707 $row = (object) array_merge((array) $row, (array) $row_data);
708
709 break;
710 }
711 }
712
713 // unset last reference
714 unset($row);
715
716 // return the final list
717 return $rows;
718 }
719
720 /**
721 * Helper method to set a new list of worthy event types.
722 *
723 * @param array $worthy_events list of event type identifiers.
724 *
725 * @return self
726 *
727 * @since 1.15.0 (J) - 1.5.0 (WP)
728 */
729 public function setWorthyEventTypes($worthy_events = [])
730 {
731 if (is_array($worthy_events)) {
732 $this->worthy_events = $worthy_events;
733 }
734
735 return $this;
736 }
737
738 /**
739 * Helper method to get the list of worthy event types.
740 *
741 * @return array
742 *
743 * @since 1.15.4 (J) - 1.5.10 (WP)
744 */
745 public function getWorthyEventTypes()
746 {
747 return $this->worthy_events;
748 }
749
750 /**
751 * Gets the latest history events worthy of a notification.
752 *
753 * @param int $min_id optional minimum history ID to fetch.
754 * @param int $limit the query limit.
755 *
756 * @return array the list of worthy history record objects.
757 *
758 * @since 1.15.0 (J) - 1.5.0 (WP)
759 * @since 1.16.0 (J) - 1.6.0 (WP) query modified to fetch the customer picture.
760 * INNER JOIN dismissed.
761 */
762 public function getWorthyEvents($min_id = 0, $limit = 0)
763 {
764 $clauses = [
765 "`o`.`status` IS NOT NULL",
766 "`o`.`closure` = 0"
767 ];
768
769 if ($min_id > 0) {
770 $clauses[] = "`h`.`id` > " . (int)$min_id;
771 }
772
773 if ($this->worthy_events) {
774 // quote all worthy types of event
775 $worthy_types = array_map(array($this->dbo, 'quote'), $this->worthy_events);
776 $clauses[] = "`h`.`type` IN (" . implode(', ', $worthy_types) . ")";
777 }
778
779 // join as few tables as possible to reduce the execution timing
780 $q = $this->dbo->getQuery(true)
781 ->select($this->dbo->qn('h') . '.*')
782 ->select([
783 $this->dbo->qn('o.status'),
784 $this->dbo->qn('o.days'),
785 $this->dbo->qn('o.checkin'),
786 $this->dbo->qn('o.checkout'),
787 $this->dbo->qn('o.totpaid'),
788 $this->dbo->qn('o.total'),
789 $this->dbo->qn('o.idorderota'),
790 $this->dbo->qn('o.channel'),
791 $this->dbo->qn('o.country'),
792 ])
793 ->from($this->dbo->qn('#__vikbooking_orderhistory', 'h'))
794 ->leftJoin($this->dbo->qn('#__vikbooking_orders', 'o') . ' ON ' . $this->dbo->qn('h.idorder') . ' = ' . $this->dbo->qn('o.id'))
795 ->where(implode(' AND ', $clauses))
796 ->order($this->dbo->qn('h.dt') . ' DESC')
797 ->order($this->dbo->qn('h.id') . ' DESC');
798
799 $this->dbo->setQuery($q, 0, $limit);
800 $rows = $this->dbo->loadObjectList();
801
802 if (!$rows) {
803 return [];
804 }
805
806 // set the "pic" property to a default empty value
807 foreach ($rows as &$row) {
808 $row->pic = '';
809 }
810
811 // unset last reference
812 unset($row);
813
814 // build a list of reservation IDs
815 $history_orders = [];
816 foreach ($rows as $row) {
817 $history_orders[] = (int)$row->idorder;
818 }
819 $history_orders = array_unique($history_orders);
820
821 // fetch the rest of the information
822 $q = $this->dbo->getQuery(true)
823 ->select([
824 $this->dbo->qn('o.id'),
825 $this->dbo->qn('c.pic'),
826 ])
827 ->from($this->dbo->qn('#__vikbooking_orders', 'o'))
828 ->leftJoin($this->dbo->qn('#__vikbooking_customers_orders', 'co') . ' ON ' . $this->dbo->qn('co.idorder') . ' = ' . $this->dbo->qn('o.id'))
829 ->leftJoin($this->dbo->qn('#__vikbooking_customers', 'c') . ' ON ' . $this->dbo->qn('c.id') . ' = ' . $this->dbo->qn('co.idcustomer'))
830 ->where($this->dbo->qn('o.id') . ' IN (' . implode(', ', $history_orders) . ')');
831
832 $this->dbo->setQuery($q);
833 $rows_data = $this->dbo->loadObjectList();
834
835 if ($rows_data) {
836 // merge information
837 foreach ($rows as &$row) {
838 foreach ($rows_data as $row_data) {
839 if ($row_data->id != $row->idorder) {
840 continue;
841 }
842
843 // set picture property
844 $row->pic = $row_data->pic;
845 break;
846 }
847 }
848 }
849
850 // return all records found
851 return $rows;
852 }
853
854 /**
855 * Builds a summary of what was changed with a booking modification event.
856 *
857 * @param array $booking_info the booking information record.
858 *
859 * @return string
860 *
861 * @since 1.16.8 (J) - 1.6.8 (WP)
862 */
863 public function getBookingModificationSummary(array $booking_info)
864 {
865 if ($this->prevBooking) {
866 return '';
867 }
868
869 $data_changed = [];
870
871 // check the stay dates
872 if (date('Y-m-d', $booking_info['checkin']) != date('Y-m-d', $this->prevBooking['checkin'])) {
873 $data_changed[] = 'checkin';
874 }
875 if (date('Y-m-d', $booking_info['checkout']) != date('Y-m-d', $this->prevBooking['checkout'])) {
876 $data_changed[] = 'checkout';
877 }
878 if ($booking_info['days'] != $this->prevBooking['days']) {
879 $data_changed[] = 'days';
880 }
881
882 // check rooms and guests
883 if (!empty($this->prevBooking['rooms_info'])) {
884 // build previous values
885 $prev_room_ids = array_column($this->prevBooking['rooms_info'], 'idroom');
886 $prev_adults = array_column($this->prevBooking['rooms_info'], 'adults');
887 $prev_children = array_column($this->prevBooking['rooms_info'], 'children');
888
889 // build new values
890 $new_rooms_info = VikBooking::loadOrdersRoomsData($booking_info['id']);
891 if ($new_rooms_info && $prev_room_ids) {
892 $now_room_ids = array_column($new_rooms_info, 'idroom');
893 $now_adults = array_column($new_rooms_info, 'adults');
894 $now_children = array_column($new_rooms_info, 'children');
895
896 // normalize arrays
897 $prev_room_ids = array_map('intval', $prev_room_ids);
898 $now_room_ids = array_map('intval', $now_room_ids);
899 sort($prev_room_ids);
900 sort($now_room_ids);
901
902 // check if anything has changed
903 if ($prev_room_ids != $now_room_ids) {
904 $data_changed[] = 'rooms';
905 }
906 if (array_sum($prev_adults) != array_sum($now_adults) || array_sum($prev_children) != array_sum($now_children)) {
907 $data_changed[] = 'guests';
908 }
909 }
910 }
911
912 // check totals
913 if ($booking_info['total'] != $this->prevBooking['total']) {
914 $data_changed[] = 'total';
915 }
916
917 if (!$data_changed) {
918 // no changes detected
919 return '';
920 }
921
922 // obtain the translations for what has changed
923 $factors_changed = array_map(function($changed) {
924 switch ($changed) {
925 case 'checkin':
926 return JText::translate('VBPICKUPAT');
927
928 case 'checkout':
929 return JText::translate('VBRELEASEAT');
930
931 case 'days':
932 return JText::translate('VBOINVTOTNIGHTS');
933
934 case 'rooms':
935 return JText::translate('VBPVIEWORDERSTHREE');
936
937 case 'guests':
938 return JText::translate('VBPVIEWORDERSPEOPLE');
939
940 case 'total':
941 return JText::translate('VBPVIEWORDERSSEVEN');
942
943 default:
944 return $changed;
945 }
946 }, $data_changed);
947
948 if (count($factors_changed) > 1) {
949 // plural version
950 return JText::sprintf('VBO_BOOK_MOD_SUMMARY_PLR', implode(', ', $factors_changed));
951 }
952
953 // singular version
954 return JText::sprintf('VBO_BOOK_MOD_SUMMARY_SNG', implode(', ', $factors_changed));
955 }
956
957 /**
958 * Adds the history event to the Notifications Center.
959 *
960 * @param object $history_record the history record created.
961 * @param array $booking_info the booking information record.
962 *
963 * @return bool
964 *
965 * @since 1.16.8 (J) - 1.6.8 (WP)
966 */
967 protected function addToNotificationsCenter($history_record, array $booking_info)
968 {
969 if (VikBooking::isSite()) {
970 // load language according to platform
971 $lang = JFactory::getLanguage();
972 if (VBOPlatformDetection::isJoomla()) {
973 // make sure to load the back-end language definitions and any override
974 $lang->load('com_vikbooking', JPATH_ADMINISTRATOR, $lang->getTag(), true);
975 $lang->load('joomla', JPATH_ADMINISTRATOR, $lang->getTag(), true);
976 } else {
977 $lang->load('com_vikbooking', VIKBOOKING_LANG, $lang->getTag(), true);
978
979 // make sure to register the language handler
980 $lib_base_path = defined('VIKBOOKING_LIBRARIES') ? VIKBOOKING_LIBRARIES : '';
981 if (!$lib_base_path && defined('VIKCHANNELMANAGER_LIBRARIES')) {
982 $lib_base_path = str_replace('vikchannelmanager' . DIRECTORY_SEPARATOR . 'libraries', 'vikbooking' . DIRECTORY_SEPARATOR . 'libraries', VIKCHANNELMANAGER_LIBRARIES);
983 }
984
985 if ($lib_base_path) {
986 $lang->attachHandler($lib_base_path . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR . 'admin.php', 'vikbooking');
987 }
988 }
989
990 // reload history types
991 $this->typesMap = $this->getTypesMap();
992 }
993
994 // determine the notification sender
995 $sender = 'website';
996 $channel = null;
997 if (!empty($booking_info['channel']) && !empty($booking_info['idorderota'])) {
998 $sender = 'otas';
999 $channel = $booking_info['channel'];
1000 }
1001
1002 // build the notification summary
1003 $summary = $history_record->descr ?? null;
1004 if ($this->prevBooking) {
1005 // in case of booking modification, we compose a summary of just what was changed
1006 $summary = $this->getBookingModificationSummary($booking_info);
1007 $summary = $summary ?: null;
1008 }
1009
1010 // store the notification
1011 try {
1012 $result = (new VBONotificationCenter)
1013 ->store([
1014 [
1015 'sender' => $sender,
1016 'type' => $history_record->type,
1017 'title' => $this->validType($history_record->type, true),
1018 'summary' => $history_record->descr ?? null,
1019 'idorder' => $history_record->idorder,
1020 'idorderota' => $booking_info['idorderota'] ?: null,
1021 'channel' => $channel,
1022 ],
1023 ]);
1024 } catch (Exception $e) {
1025 return false;
1026 }
1027
1028 return !empty($result['new_notifications']);
1029 }
1030 }
1031