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 / history.php

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

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