PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.8
VikBooking Hotel Booking Engine & PMS v1.8.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.8.8, at site/helpers/history.php

1,339 lines 36.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 array
35 */
36 protected $typesMap = [];
37
38 /**
39 * @var array
40 *
41 * @since 1.18.0 (J) - 1.8.0 (WP)
42 */
43 protected $bookingInfo = [];
44
45 /**
46 * @var array
47 *
48 * @since 1.18.0 (J) - 1.8.0 (WP)
49 */
50 protected $bookingRooms = [];
51
52 /**
53 * @var array Static cache to identify duplicate operations.
54 *
55 * @since 1.18.0 (J) - 1.8.0 (WP)
56 */
57 protected static $signaturesList = [];
58
59 /**
60 * List of event types worthy of a notification.
61 *
62 * @var array
63 *
64 * @since 1.15.0 (J) - 1.5.0 (WP)
65 */
66 protected $worthy_events = [
67 'NC',
68 'MW',
69 'NP',
70 'P0',
71 'PN',
72 'CR',
73 'CW',
74 'MC',
75 'CC',
76 'NO',
77 'PO',
78 'IR',
79 'PC',
80 'UR',
81 ];
82
83 /**
84 * List of event types supported by the Notifications Center.
85 *
86 * @var array
87 *
88 * @since 1.16.8 (J) - 1.6.8 (WP)
89 */
90 protected $notificationsCenterTypes = [
91 // New booking with status Confirmed
92 'NC',
93 // Booking paid for the first time
94 'P0',
95 // Booking paid for a second time
96 'PN',
97 // Booking modified from website
98 'MW',
99 // Cancellation request message
100 'CR',
101 // Booking cancelled via front-end website
102 'CW',
103 // Booking modified from channel
104 'MC',
105 // Booking cancelled from channel
106 'CC',
107 // New Booking from OTA
108 'NO',
109 // Overbooking
110 'OB',
111 // Channel Manager payout notification
112 'PO',
113 // Pre-checkin updated via front-end
114 'PC',
115 // Guest review received
116 'GR',
117 ];
118
119 /**
120 * @var bool
121 *
122 * @since 1.18.3 (J) - 1.8.3 (WP)
123 */
124 protected $silentNotificationCenter = false;
125
126 /**
127 * List of booking events for a new record.
128 *
129 * @var array
130 *
131 * @since 1.18.0 (J) - 1.8.0 (WP)
132 */
133 protected $bookingEventsRecordNew = [
134 // New booking with status Confirmed
135 'NC',
136 // New booking from back-end
137 'NB',
138 // Booking paid for the first time
139 'P0',
140 // Booking set to Confirmed by admin
141 'TC',
142 // Booking set to Confirmed via App
143 'AC',
144 // New booking via App
145 'AN',
146 // New Booking from OTA
147 'NO',
148 ];
149
150 /**
151 * List of booking events for a modified record.
152 *
153 * @var array
154 *
155 * @since 1.18.0 (J) - 1.8.0 (WP)
156 */
157 protected $bookingEventsRecordModified = [
158 // Booking modified from website
159 'MW',
160 // Booking modified from back-end
161 'MB',
162 // Booking modified from channel
163 'MC',
164 // Booking modified via App
165 'AM',
166 ];
167
168 /**
169 * List of booking events for a cancelled record.
170 *
171 * @var array
172 *
173 * @since 1.18.0 (J) - 1.8.0 (WP)
174 */
175 protected $bookingEventsRecordCancelled = [
176 // Booking cancelled via front-end website
177 'CW',
178 // Booking cancelled via back-end by admin
179 'CB',
180 // Booking cancelled from channel
181 'CC',
182 // Booking removed via App
183 'AR',
184 ];
185
186 /**
187 * List of booking pre-checkin events.
188 *
189 * @var array
190 *
191 * @since 1.18.6 (J) - 1.8.6 (WP)
192 */
193 protected $bookingPrecheckinEvents = [
194 // Pre-checkin updated via front-end
195 'PC',
196 ];
197
198 /**
199 * Class constructor.
200 *
201 * @param int $bid optional booking ID to set.
202 *
203 * @since 1.16.5 (J) - 1.6.5 (WP) added argument $bid.
204 */
205 public function __construct($bid = 0)
206 {
207 $this->typesMap = $this->getTypesMap();
208
209 if ($bid) {
210 $this->bid = (int) $bid;
211 }
212 }
213
214 /**
215 * Returns an array of types mapped to
216 * the corresponding language definition.
217 * All the history types should be listed here.
218 *
219 * @return array
220 */
221 public function getTypesMap()
222 {
223 return [
224 // New booking with status Confirmed
225 'NC' => JText::translate('VBOBOOKHISTORYTNC'),
226 // Booking modified from website
227 'MW' => JText::translate('VBOBOOKHISTORYTMW'),
228 // Booking modified from back-end
229 'MB' => JText::translate('VBOBOOKHISTORYTMB'),
230 // New booking from back-end
231 'NB' => JText::translate('VBOBOOKHISTORYTNB'),
232 // New booking with status Pending
233 'NP' => JText::translate('VBOBOOKHISTORYTNP'),
234 // Booking paid for the first time
235 'P0' => JText::translate('VBOBOOKHISTORYTP0'),
236 // Booking paid for a second time
237 'PN' => JText::translate('VBOBOOKHISTORYTPN'),
238 // Cancellation request message
239 'CR' => JText::translate('VBOBOOKHISTORYTCR'),
240 // Booking cancelled via front-end website
241 'CW' => JText::translate('VBOBOOKHISTORYTCW'),
242 // Booking auto cancelled via front-end
243 'CA' => JText::translate('VBOBOOKHISTORYTCA'),
244 // Booking cancelled via back-end by admin
245 'CB' => JText::translate('VBOBOOKHISTORYTCB'),
246 // Booking Receipt generated via back-end
247 'BR' => JText::translate('VBOBOOKHISTORYTBR'),
248 // Booking Invoice generated
249 'BI' => JText::translate('VBOBOOKHISTORYTBI'),
250 // Booking registration unset status by admin
251 'RA' => JText::translate('VBOBOOKHISTORYTRA'),
252 // Booking checked-in status set by admin
253 'RB' => JText::translate('VBOBOOKHISTORYTRB'),
254 // Booking checked-out status set by admin
255 'RC' => JText::translate('VBOBOOKHISTORYTRC'),
256 // Booking no-show status set by admin
257 'RZ' => JText::translate('VBOBOOKHISTORYTRZ'),
258 // Booking set to Confirmed by admin
259 'TC' => JText::translate('VBOBOOKHISTORYTTC'),
260 // Booking set to Confirmed via App
261 'AC' => JText::translate('VBOBOOKHISTORYTAC'),
262 // Booking modified from channel
263 'MC' => JText::translate('VBOBOOKHISTORYTMC'),
264 // Booking cancelled from channel
265 'CC' => JText::translate('VBOBOOKHISTORYTCC'),
266 // Booking removed via App
267 'AR' => JText::translate('VBOBOOKHISTORYTAR'),
268 // Booking modified via App
269 'AM' => JText::translate('VBOBOOKHISTORYTAM'),
270 // New booking via App
271 'AN' => JText::translate('VBOBOOKHISTORYTAN'),
272 // New Booking from OTA
273 'NO' => JText::translate('VBOBOOKHISTORYTNO'),
274 // Report affecting the booking
275 'RP' => JText::translate('VBOBOOKHISTORYTRP'),
276 // Custom email sent to the customer by admin
277 'CE' => JText::translate('VBOBOOKHISTORYTCE'),
278 // Custom SMS sent to the customer by admin
279 'CS' => JText::translate('VBOBOOKHISTORYTCS'),
280 // Confirmation email re-sent by admin to guest
281 'ER' => JText::translate('VBRESENDORDEMAIL'),
282 // Payment Update (a new amount paid has been set)
283 'PU' => JText::translate('VBOBOOKHISTORYTPU'),
284 // Upsell Extras via front-end
285 'UE' => JText::translate('VBOBOOKHISTORYTUE'),
286 // Report guest misconduct to OTA
287 'GM' => JText::translate('VBOBOOKHISTORYTGM'),
288 // Send Email Cancellation by admin
289 'EC' => JText::translate('VBOBOOKHISTORYTEC'),
290 // Amount refunded
291 'RF' => JText::translate('VBOBOOKHISTORYTRF'),
292 // Refund Updated
293 'RU' => JText::translate('VBOBOOKHISTORYTRU'),
294 // Payable Amount Updated
295 'PB' => JText::translate('VBOBOOKHISTORYTPB'),
296 // Channel Manager custom event
297 'CM' => JText::translate('VBOBOOKHISTORYTCM'),
298 // Channel Manager payout notification
299 'PO' => JText::translate('VBOBOOKHISTORYTPO'),
300 // Inquiry reservation (website)
301 'IR' => JText::translate('VBOBOOKHISTORYTIR'),
302 // Pre-checkin updated via front-end
303 'PC' => JText::translate('VBOBOOKHISTORYTPC'),
304 // Guest review received
305 'GR' => JText::translate('VBOBOOKHISTORYTGR'),
306 // Upgrade room
307 'UR' => JText::translate('VBOBOOKHISTORYTUR'),
308 // Overbooking
309 'OB' => JText::translate('VBOBOOKHISTORYTOB'),
310 // Task Manager - General event (i.e. task status updated)
311 'TM' => JText::translate('VBOBOOKHISTORYTTM'),
312 // Task Manager - New tasks
313 'NT' => JText::translate('VBOBOOKHISTORYTNT'),
314 // Task Manager - Modified tasks
315 'MT' => JText::translate('VBOBOOKHISTORYTMT'),
316 // Task Manager - Cancelled tasks
317 'CT' => JText::translate('VBOBOOKHISTORYTCT'),
318 // Door Access Control - New booking device passcode
319 'ND' => JText::translate('VBOBOOKHISTORYTND'),
320 // Door Access Control - Modified booking device passcode
321 'MD' => JText::translate('VBOBOOKHISTORYTMD'),
322 // Door Access Control - Cancelled booking device passcode
323 'CD' => JText::translate('VBOBOOKHISTORYTCD'),
324 // Door Access Control - First access through device passcode
325 'FA' => JText::translate('VBOBOOKHISTORYTFA'),
326 ];
327 }
328
329 /**
330 * Accesses the groups of history event types, useful to group multiple events.
331 * Groups are categories of events containing worthy event types of the same context.
332 *
333 * @param string $group get the list of event types of this group.
334 *
335 * @return array associative array of types group or empty array.
336 *
337 * @since 1.16.0 (J) - 1.6.0 (WP)
338 */
339 public function getTypeGroups($group = null)
340 {
341 // we group various types of events per category
342 $type_groups = [
343 // group "Bookings"
344 'GBK' => [
345 'name' => JText::translate('VBMENUTHREE'),
346 'types' => [
347 'NC',
348 'MW',
349 'NP',
350 'CR',
351 'CW',
352 'MC',
353 'CC',
354 'NO',
355 'IR',
356 'UR',
357 'OB',
358 ],
359 ],
360 // group "Channel Manager"
361 'GCM' => [
362 'name' => JText::translate('VBMENUCHANNELMANAGER'),
363 'types' => [
364 'MC',
365 'CC',
366 'NO',
367 'PO',
368 'CM',
369 'OB',
370 ],
371 ],
372 // group "Website"
373 'GWB' => [
374 'name' => JText::translate('VBORDFROMSITE'),
375 'types' => [
376 'NC',
377 'MW',
378 'NP',
379 'CR',
380 'CW',
381 'IR',
382 'PC',
383 'UE',
384 'GR',
385 'UR',
386 ],
387 ],
388 // group "Payments"
389 'GPM' => [
390 'name' => JText::translate('VBO_HISTORY_GPM'),
391 'types' => [
392 'P0',
393 'PN',
394 'PO',
395 'RF',
396 ],
397 ],
398 ];
399
400 if ($group) {
401 // attempt to fetch the requested group
402 return $type_groups[$group] ?? [];
403 }
404
405 // return the whole associative list
406 return $type_groups;
407 }
408
409 /**
410 * Checks whether the type for the history record is valid.
411 *
412 * @param string $type
413 * @param bool $returnit True for returning the translated value. Bool is returned otherwise.
414 *
415 * @return bool
416 */
417 public function validType($type, $returnit = false)
418 {
419 if ($returnit) {
420 return isset($this->typesMap[strtoupper($type)]) ? $this->typesMap[strtoupper($type)] : $type;
421 }
422
423 return isset($this->typesMap[strtoupper($type)]);
424 }
425
426 /**
427 * Sets the current booking ID.
428 *
429 * @param int $bid
430 *
431 * @return VboBookingHistory
432 **/
433 public function setBid($bid)
434 {
435 $this->bid = (int) $bid;
436
437 return $this;
438 }
439
440 /**
441 * Sets the previous booking array.
442 * To calculate what has changed in the booking after the
443 * modification, VBO uses the method getLogBookingModification().
444 * VCM instead should use this method to tell the class that
445 * what has changed should be calculated to obtain the 'descr'
446 * text of the history record that will be stored.
447 *
448 * @param array $booking
449 *
450 * @return VboBookingHistory
451 **/
452 public function setPrevBooking($booking)
453 {
454 if (is_array($booking)) {
455 $this->prevBooking = $booking;
456 }
457
458 return $this;
459 }
460
461 /**
462 * Sets extra data for the current history log.
463 *
464 * @param mixed $data array, object or string of extra data.
465 * Useful to store the amount paid to be invoiced,
466 * or the transaction details of the payments.
467 *
468 * @return VboBookingHistory
469 **/
470 public function setExtraData($data)
471 {
472 $this->data = $data;
473
474 return $this;
475 }
476
477 /**
478 * Sets the current booking room records.
479 *
480 * @param array $booking_rooms The current booking room records.
481 *
482 * @return VboBookingHistory
483 *
484 * @since 1.18.0 (J) - 1.8.0 (WP)
485 */
486 public function setBookingRooms(array $booking_rooms)
487 {
488 $this->bookingRooms = $booking_rooms;
489 }
490
491 /**
492 * Sets the current booking record.
493 *
494 * @param array $booking The current booking record.
495 *
496 * @return VboBookingHistory
497 *
498 * @since 1.18.0 (J) - 1.8.0 (WP)
499 */
500 public function setBookingInfo(array $booking)
501 {
502 $this->bookingInfo = $booking;
503
504 if (is_array(($booking['rooms_info'] ?? null)) && $booking['rooms_info']) {
505 // booking room records may be available within the booking record itself
506 $this->setBookingRooms($booking['rooms_info']);
507 }
508 }
509
510 /**
511 * Sets the current booking data.
512 *
513 * @param array $booking The current booking record.
514 * @param array $booking_rooms The current booking room records.
515 *
516 * @return VboBookingHistory
517 *
518 * @since 1.18.0 (J) - 1.8.0 (WP)
519 */
520 public function setBookingData(array $booking, array $booking_rooms = [])
521 {
522 $this->setBookingInfo($booking);
523
524 if ($booking_rooms) {
525 $this->setBookingRooms($booking_rooms);
526 }
527
528 return $this;
529 }
530
531 /**
532 * Sets the current flag for silencing the notification center population.
533 *
534 * @param bool $set Whether to silence the notification center once.
535 *
536 * @return VboBookingHistory
537 *
538 * @since 1.18.3 (J) - 1.8.3 (WP)
539 */
540 public function setSilentNotificationCenter(bool $set)
541 {
542 $this->silentNotificationCenter = $set;
543
544 return $this;
545 }
546
547 /**
548 * Returns a list of history event types whenenver a specific booking event occurs.
549 *
550 * @param string $type The booking event type identifier ("new", "modified",
551 * "cancelled", "precheckin").
552 *
553 * @return array
554 *
555 * @since 1.18.6 (J) - 1.8.6 (WP)
556 */
557 public function getBookingEventsType(string $type)
558 {
559 if (!strcasecmp($type, 'new')) {
560 return $this->bookingEventsRecordNew;
561 }
562
563 if (!strcasecmp($type, 'modified')) {
564 return $this->bookingEventsRecordModified;
565 }
566
567 if (!strcasecmp($type, 'cancelled')) {
568 return $this->bookingEventsRecordCancelled;
569 }
570
571 if (!strcasecmp($type, 'precheckin')) {
572 return $this->bookingPrecheckinEvents;
573 }
574
575 return [];
576 }
577
578 /**
579 * Fetches the current booking record.
580 *
581 * @return ?array
582 */
583 protected function getBookingInfo()
584 {
585 $dbo = JFactory::getDbo();
586
587 if ($this->bookingInfo && ($this->bookingInfo['id'] ?? 0) == $this->bid) {
588 // return the cached value
589 return $this->bookingInfo;
590 }
591
592 $q = $dbo->getQuery(true)
593 ->select('*')
594 ->from($dbo->qn('#__vikbooking_orders'))
595 ->where($dbo->qn('id') . ' = ' . (int) $this->bid);
596
597 $dbo->setQuery($q, 0, 1);
598 $record = $dbo->loadAssoc();
599
600 if (!$record) {
601 return null;
602 }
603
604 // cache booking information internally
605 $this->bookingInfo = $record;
606
607 return $record;
608 }
609
610 /**
611 * Stores a new history record for the booking.
612 *
613 * @param string $type Char-type of storing we are making for the history.
614 * @param string $descr Optional description of this booking record.
615 *
616 * @return bool
617 */
618 public function store($type, $descr = '')
619 {
620 $dbo = JFactory::getDbo();
621
622 if (is_null($this->bid) || !$this->validType($type)) {
623 return false;
624 }
625
626 if (!$booking_info = $this->getBookingInfo()) {
627 return false;
628 }
629
630 /**
631 * Define the current storing signature.
632 *
633 * @since 1.18.0 (J) - 1.8.0 (WP)
634 */
635 $storingSignature = implode('-', [$type, $this->bid]);
636
637 if (empty($descr) && $this->prevBooking) {
638 /**
639 * VCM (including the App) could set the previous booking information,
640 * so we need to calculate what has changed with the booking.
641 * Load VBO language.
642 */
643 $lang = JFactory::getLanguage();
644 $lang->load('com_vikbooking', (VBOPlatformDetection::isWordPress() ? VIKBOOKING_ADMIN_LANG : JPATH_ADMINISTRATOR), $lang->getTag(), true);
645 $descr = VikBooking::getLogBookingModification($this->prevBooking);
646 }
647
648 // get the event dispatcher
649 $dispatcher = VBOFactory::getPlatform()->getDispatcher();
650
651 // build record object
652 $history_record = new stdClass;
653 $history_record->idorder = $this->bid;
654 $history_record->dt = JFactory::getDate()->toSql();
655 $history_record->type = $type;
656 $history_record->descr = $descr ?: null;
657 $history_record->totpaid = (float) $booking_info['totpaid'];
658 $history_record->total = (float) $booking_info['total'];
659 $history_record->data = $this->data;
660
661 /**
662 * Trigger event before saving a new history record.
663 *
664 * @since 1.16.0 (J) - 1.6.0 (WP)
665 */
666 $results = $dispatcher->filter('onBeforeSaveBookingHistoryVikBooking', [$history_record, $this->prevBooking]);
667 if (in_array(false, $results, true)) {
668 return false;
669 }
670
671 /**
672 * Store the extra data for this history log. Useful to store
673 * information about the amount paid, its invoicing status or any custom data.
674 *
675 * @since 1.12 (J) - 1.1.7 (WP)
676 */
677 if (!is_null($history_record->data) && !is_scalar($history_record->data)) {
678 $history_record->data = json_encode($history_record->data);
679 }
680
681 // store record
682 if (!$dbo->insertObject('#__vikbooking_orderhistory', $history_record, 'id') || !isset($history_record->id)) {
683 return false;
684 }
685
686 /**
687 * Trigger event for the newly history record added.
688 *
689 * @since 1.16.0 (J) - 1.6.0 (WP)
690 */
691 $dispatcher->trigger('onAfterSaveBookingHistoryVikBooking', [$history_record, $this->prevBooking]);
692
693 /**
694 * Check if the event requires an entry to be stored in the Notifications Center.
695 *
696 * @since 1.16.8 (J) - 1.6.8 (WP)
697 */
698 $results = $dispatcher->filter('onBeforeSaveHistoryNotificationsCenter', [$history_record, $this->notificationsCenterTypes]);
699 if (in_array($type, $this->notificationsCenterTypes) || in_array(true, $results, true)) {
700 if ($this->silentNotificationCenter === false) {
701 // add the event to the NC
702 $this->addToNotificationsCenter($history_record, $booking_info);
703 }
704 // always reset silence flag for NC
705 $this->setSilentNotificationCenter(false);
706 }
707
708 /**
709 * Check if booking-related framework should run for the current event type.
710 *
711 * @since 1.18.0 (J) - 1.8.0 (WP)
712 * @since 1.18.4 (J) - 1.8.4 (WP) added support for Door Access Control framework.
713 * @since 1.18.6 (J) - 1.8.6 (WP) added support for booking pre-checkin events.
714 */
715 if (!in_array($storingSignature, static::$signaturesList)) {
716 // we can safely invoke any booking-related framework for a unique event
717 if (in_array($type, $this->bookingEventsRecordNew)) {
718 // process the new booking confirmation tasks
719 VBOFactory::getTaskManager()->processBookingConfirmation($this->bookingInfo, $this->bookingRooms);
720 // process the new booking confirmation event
721 VBOFactory::getDoorAccessControl()->processBookingConfirmation($this->bookingInfo, $this->bookingRooms);
722 } elseif (in_array($type, $this->bookingEventsRecordCancelled)) {
723 // process the booking cancellation tasks
724 VBOFactory::getTaskManager()->processBookingCancellation($this->bookingInfo, $this->bookingRooms);
725 // process the booking cancellation event
726 VBOFactory::getDoorAccessControl()->processBookingCancellation($this->bookingInfo, $this->bookingRooms);
727 } elseif (in_array($type, $this->bookingEventsRecordModified)) {
728 // process the booking modification tasks
729 VBOFactory::getTaskManager()->processBookingModification($this->bookingInfo, $this->bookingRooms, (array) $this->prevBooking);
730 // process the booking modification event
731 VBOFactory::getDoorAccessControl()->processBookingModification($this->bookingInfo, $this->bookingRooms, (array) $this->prevBooking);
732 } elseif (in_array($type, $this->bookingPrecheckinEvents)) {
733 // process the booking pre-checkin event
734 VBOFactory::getDoorAccessControl()->processPrecheckinCompleted($this->bookingInfo, $this->bookingRooms);
735 }
736 }
737
738 /**
739 * Populate a static cache list to prevent duplicate operations from being executed, unless required.
740 *
741 * @since 1.18.0 (J) - 1.8.0 (WP)
742 */
743 static::$signaturesList[] = $storingSignature;
744
745 // operation completed with success
746 return true;
747 }
748
749 /**
750 * Loads the history records for the booking ID set.
751 *
752 * @param int $offset query offset limit start.
753 * @param int $limit query maximum records to fetch.
754 *
755 * @return array
756 *
757 * @since 1.16.5 (J) - 1.6.5 (WP) added arguments $offset and $limit.
758 */
759 public function loadHistory($offset = 0, $limit = 0)
760 {
761 $dbo = JFactory::getDbo();
762
763 if (empty($this->bid)) {
764 return [];
765 }
766
767 $q = $dbo->getQuery(true)
768 ->select('*')
769 ->from($dbo->qn('#__vikbooking_orderhistory'))
770 ->where($dbo->qn('idorder') . ' = ' . (int) $this->bid)
771 ->order($dbo->qn('dt') . ' DESC')
772 ->order($dbo->qn('id') . ' DESC');
773
774 $dbo->setQuery($q, $offset, $limit);
775
776 return $dbo->loadAssocList();
777 }
778
779 /**
780 * Checks whether this booking has an event of the given type.
781 *
782 * @param string $type the type of the event.
783 *
784 * @return mixed last date on success, false otherwise.
785 *
786 * @since 1.13.5 (J) - 1.3.5 (WP)
787 */
788 public function hasEvent($type)
789 {
790 $dbo = JFactory::getDbo();
791
792 if (empty($type) || empty($this->bid)) {
793 return false;
794 }
795
796 $q = $dbo->getQuery(true)
797 ->select($dbo->qn('dt'))
798 ->from($dbo->qn('#__vikbooking_orderhistory'))
799 ->where($dbo->qn('idorder') . ' = ' . (int) $this->bid)
800 ->where($dbo->qn('type') . ' = ' . $dbo->q($type))
801 ->order($dbo->qn('dt') . ' DESC');
802
803 $dbo->setQuery($q, 0, 1);
804
805 $dt = $dbo->loadResult();
806
807 if (!$dt) {
808 return false;
809 }
810
811 return $dt;
812 }
813
814 /**
815 * Returns a list of records with data defined for the given event type.
816 * Useful to get a list of transactions data for the refund operations.
817 *
818 * @param mixed $type String or array event type(s).
819 * @param ?callable $callvalid Optional callback for the data validation.
820 * @param bool $onlydata Whether to get just the event data.
821 *
822 * @return mixed False or array with data records.
823 *
824 * @since 1.14.0 (J) - 1.4.0 (WP)
825 */
826 public function getEventsWithData($type, $callvalid = null, $onlydata = true)
827 {
828 $dbo = JFactory::getDbo();
829
830 if (empty($type) || empty($this->bid)) {
831 return false;
832 }
833
834 if (!is_array($type)) {
835 $type = [$type];
836 }
837
838 // quote all given types
839 $types = array_map([$dbo, 'q'], $type);
840
841 $q = $dbo->getQuery(true)
842 ->select('*')
843 ->from($dbo->qn('#__vikbooking_orderhistory'))
844 ->where($dbo->qn('idorder') . ' = ' . (int) $this->bid)
845 ->where($dbo->qn('type') . ' IN (' . implode(', ', $types) . ')')
846 // always keep the ordering ascending
847 ->order($dbo->qn('dt') . ' ASC');
848
849 $dbo->setQuery($q);
850 $events = $dbo->loadAssocList();
851
852 if ($events) {
853 $datas = [];
854 foreach ($events as $k => $e) {
855 $data = $e['data'] ? json_decode($e['data']) : null;
856 if (!empty($data)) {
857 $events[$k]['data'] = $data;
858 }
859 $valid_data = true;
860 if (is_callable($callvalid)) {
861 $valid_data = call_user_func($callvalid, $events[$k]['data']);
862 }
863 if ($valid_data) {
864 array_push($datas, $events[$k]['data']);
865 }
866 }
867
868 return $onlydata ? $datas : $events;
869 }
870
871 return false;
872 }
873
874 /**
875 * Gets the latest history event per booking within a list.
876 * Only the latest event per booking will be considered.
877 *
878 * @param int $start the query start offset.
879 * @param int $limit the query limit.
880 * @param int $min_id optional minimum history ID to fetch.
881 * @param array $types optional list of event types (or groups) to fetch.
882 *
883 * @return array the list of recent history record objects.
884 *
885 * @since 1.15.0 (J) - 1.5.0 (WP)
886 * @since 1.16.0 (J) - 1.6.0 (WP) query modified to fetch the customer picture.
887 * @since 1.16.0 (J) - 1.6.0 (WP) fourth argument $types introduced.
888 */
889 public function getLatestBookingEvents($start = 0, $limit = 20, $min_id = 0, array $types = [])
890 {
891 $dbo = JFactory::getDbo();
892
893 $clauses = [];
894
895 if ($min_id > 0) {
896 $clauses[] = "`h`.`id` > " . (int)$min_id;
897 } else {
898 $clauses[] = "`h`.`id` > 0";
899 }
900
901 if ($types) {
902 // scan for groups
903 $groups = [];
904 foreach ($types as $k => $type) {
905 if ($type && strlen($type) === 3 && $group = $this->getTypeGroups($type)) {
906 $groups = array_merge($groups, $group['types']);
907 unset($types[$k]);
908 }
909 }
910
911 if ($groups) {
912 // merge all types
913 $types = array_merge(array_values($types), array_unique($groups));
914 }
915
916 // quote all values
917 $types = array_map(array($dbo, 'q'), array_filter($types));
918
919 if ($types) {
920 // add query clause
921 $clauses[] = "`h`.`type` IN (" . implode(', ', $types) . ")";
922 }
923 }
924
925 /**
926 * On some servers with just 30k records in the history and 5k in bookings,
927 * the INNER JOIN to group the latest history events by booking ID is taking
928 * ~25 seconds to complete, which is not acceptable. This doesn't seem to happen
929 * on every database, but since the admin widget "latest events" requires to get
930 * just the latest history event ID by passing the limit to 1, we should not
931 * use the INNER JOIN all the times to save server resources.
932 * Refactoring performed to abandon also the LEFT JOINS which were timing out in
933 * case of several thousands of records. The widget "latest_events" crashed websites.
934 *
935 * @since 1.15.6 (J) - 1.5.12 (WP)
936 * @since 1.16.0 (J) - 1.6.0 (WP) INNER JOIN dismissed because too slow and not needed.
937 * @since 1.16.1 (J) - 1.6.1 (WP) LEFT JOIN dismissed because too slow, queries split.
938 */
939
940 // query just the history records without joining any other table, or the records would become hundreds of thousands
941 $q = $dbo->getQuery(true)
942 ->select($dbo->qn('h') . '.*')
943 ->from($dbo->qn('#__vikbooking_orderhistory', 'h'))
944 ->where(implode(' AND ', $clauses))
945 ->order($dbo->qn('h.dt') . ' DESC')
946 ->order($dbo->qn('h.id') . ' DESC');
947
948 $dbo->setQuery($q, $start, $limit);
949 $rows = $dbo->loadObjectList();
950 if (!$rows) {
951 return [];
952 }
953
954 // build a list of reservation IDs
955 $history_orders = [];
956 foreach ($rows as $row) {
957 $history_orders[] = (int)$row->idorder;
958 }
959 $history_orders = array_unique($history_orders);
960
961 // fetch the rest of the information
962 $q = "SELECT `o`.`id` AS `idorder`, `o`.`custdata`, `o`.`status`, `o`.`days`, `o`.`checkin`, `o`.`checkout`, `o`.`totpaid`,
963 `o`.`total`, `o`.`idorderota`, `o`.`channel`, `o`.`country`, `o`.`closure`, `o`.`type` AS `booking_type`, `c`.`pic`,
964 (
965 SELECT CONCAT_WS(' ',`or`.`t_first_name`,`or`.`t_last_name`)
966 FROM `#__vikbooking_ordersrooms` AS `or`
967 WHERE `or`.`idorder`=`o`.`id` LIMIT 1
968 ) AS `nominative`
969 FROM `#__vikbooking_orders` AS `o`
970 LEFT JOIN `#__vikbooking_customers_orders` AS `co` ON `co`.`idorder`=`o`.`id`
971 LEFT JOIN `#__vikbooking_customers` AS `c` ON `c`.`id`=`co`.`idcustomer`
972 WHERE `o`.`id` IN (" . implode(', ', $history_orders) . ");";
973
974 $dbo->setQuery($q);
975 $rows_data = $dbo->loadObjectList();
976 if (!$rows_data) {
977 return [];
978 }
979
980 // merge information to object records
981 foreach ($rows as &$row) {
982 foreach ($rows_data as $row_data) {
983 if ($row_data->idorder != $row->idorder) {
984 continue;
985 }
986
987 // merge object properties by using casting
988 $row = (object) array_merge((array) $row, (array) $row_data);
989
990 break;
991 }
992 }
993
994 // unset last reference
995 unset($row);
996
997 // return the final list
998 return $rows;
999 }
1000
1001 /**
1002 * Helper method to set a new list of worthy event types.
1003 *
1004 * @param array $worthy_events list of event type identifiers.
1005 *
1006 * @return VboBookingHistory
1007 *
1008 * @since 1.15.0 (J) - 1.5.0 (WP)
1009 */
1010 public function setWorthyEventTypes($worthy_events = [])
1011 {
1012 if (is_array($worthy_events)) {
1013 $this->worthy_events = $worthy_events;
1014 }
1015
1016 return $this;
1017 }
1018
1019 /**
1020 * Helper method to get the list of worthy event types.
1021 *
1022 * @return array
1023 *
1024 * @since 1.15.4 (J) - 1.5.10 (WP)
1025 */
1026 public function getWorthyEventTypes()
1027 {
1028 return $this->worthy_events;
1029 }
1030
1031 /**
1032 * Gets the latest history events worthy of a notification.
1033 *
1034 * @param int $min_id optional minimum history ID to fetch.
1035 * @param int $limit the query limit.
1036 *
1037 * @return array the list of worthy history record objects.
1038 *
1039 * @since 1.15.0 (J) - 1.5.0 (WP)
1040 * @since 1.16.0 (J) - 1.6.0 (WP) query modified to fetch the customer picture.
1041 * INNER JOIN dismissed.
1042 */
1043 public function getWorthyEvents($min_id = 0, $limit = 0)
1044 {
1045 $dbo = JFactory::getDbo();
1046
1047 $clauses = [
1048 "`o`.`status` IS NOT NULL",
1049 "`o`.`closure` = 0"
1050 ];
1051
1052 if ($min_id > 0) {
1053 $clauses[] = "`h`.`id` > " . (int)$min_id;
1054 }
1055
1056 if ($this->worthy_events) {
1057 // quote all worthy types of event
1058 $worthy_types = array_map(array($dbo, 'q'), $this->worthy_events);
1059 $clauses[] = "`h`.`type` IN (" . implode(', ', $worthy_types) . ")";
1060 }
1061
1062 // join as few tables as possible to reduce the execution timing
1063 $q = $dbo->getQuery(true)
1064 ->select($dbo->qn('h') . '.*')
1065 ->select([
1066 $dbo->qn('o.status'),
1067 $dbo->qn('o.days'),
1068 $dbo->qn('o.checkin'),
1069 $dbo->qn('o.checkout'),
1070 $dbo->qn('o.totpaid'),
1071 $dbo->qn('o.total'),
1072 $dbo->qn('o.idorderota'),
1073 $dbo->qn('o.channel'),
1074 $dbo->qn('o.country'),
1075 ])
1076 ->from($dbo->qn('#__vikbooking_orderhistory', 'h'))
1077 ->leftJoin($dbo->qn('#__vikbooking_orders', 'o') . ' ON ' . $dbo->qn('h.idorder') . ' = ' . $dbo->qn('o.id'))
1078 ->where(implode(' AND ', $clauses))
1079 ->order($dbo->qn('h.dt') . ' DESC')
1080 ->order($dbo->qn('h.id') . ' DESC');
1081
1082 $dbo->setQuery($q, 0, $limit);
1083 $rows = $dbo->loadObjectList();
1084
1085 if (!$rows) {
1086 return [];
1087 }
1088
1089 // set the "pic" property to a default empty value
1090 foreach ($rows as &$row) {
1091 $row->pic = '';
1092 }
1093
1094 // unset last reference
1095 unset($row);
1096
1097 // build a list of reservation IDs
1098 $history_orders = [];
1099 foreach ($rows as $row) {
1100 $history_orders[] = (int)$row->idorder;
1101 }
1102 $history_orders = array_unique($history_orders);
1103
1104 // fetch the rest of the information
1105 $q = $dbo->getQuery(true)
1106 ->select([
1107 $dbo->qn('o.id'),
1108 $dbo->qn('c.pic'),
1109 ])
1110 ->from($dbo->qn('#__vikbooking_orders', 'o'))
1111 ->leftJoin($dbo->qn('#__vikbooking_customers_orders', 'co') . ' ON ' . $dbo->qn('co.idorder') . ' = ' . $dbo->qn('o.id'))
1112 ->leftJoin($dbo->qn('#__vikbooking_customers', 'c') . ' ON ' . $dbo->qn('c.id') . ' = ' . $dbo->qn('co.idcustomer'))
1113 ->where($dbo->qn('o.id') . ' IN (' . implode(', ', $history_orders) . ')');
1114
1115 $dbo->setQuery($q);
1116 $rows_data = $dbo->loadObjectList();
1117
1118 if ($rows_data) {
1119 // merge information
1120 foreach ($rows as &$row) {
1121 foreach ($rows_data as $row_data) {
1122 if ($row_data->id != $row->idorder) {
1123 continue;
1124 }
1125
1126 // set picture property
1127 $row->pic = $row_data->pic;
1128 break;
1129 }
1130 }
1131 }
1132
1133 // return all records found
1134 return $rows;
1135 }
1136
1137 /**
1138 * Builds a summary of what was changed with a booking modification event.
1139 *
1140 * @param array $booking_info the booking information record.
1141 *
1142 * @return string
1143 *
1144 * @since 1.16.8 (J) - 1.6.8 (WP)
1145 */
1146 public function getBookingModificationSummary(array $booking_info)
1147 {
1148 if (!$this->prevBooking) {
1149 return '';
1150 }
1151
1152 $data_changed = [];
1153
1154 // check the stay dates
1155 if (date('Y-m-d', $booking_info['checkin']) != date('Y-m-d', $this->prevBooking['checkin'])) {
1156 $data_changed[] = 'checkin';
1157 }
1158 if (date('Y-m-d', $booking_info['checkout']) != date('Y-m-d', $this->prevBooking['checkout'])) {
1159 $data_changed[] = 'checkout';
1160 }
1161 if ($booking_info['days'] != $this->prevBooking['days']) {
1162 $data_changed[] = 'days';
1163 }
1164
1165 // check rooms and guests
1166 if (!empty($this->prevBooking['rooms_info'])) {
1167 // build previous values
1168 $prev_room_ids = array_column($this->prevBooking['rooms_info'], 'idroom');
1169 $prev_adults = array_column($this->prevBooking['rooms_info'], 'adults');
1170 $prev_children = array_column($this->prevBooking['rooms_info'], 'children');
1171
1172 // build new values
1173 $new_rooms_info = VikBooking::loadOrdersRoomsData($booking_info['id']);
1174 if ($new_rooms_info && $prev_room_ids) {
1175 $now_room_ids = array_column($new_rooms_info, 'idroom');
1176 $now_adults = array_column($new_rooms_info, 'adults');
1177 $now_children = array_column($new_rooms_info, 'children');
1178
1179 // normalize arrays
1180 $prev_room_ids = array_map('intval', $prev_room_ids);
1181 $now_room_ids = array_map('intval', $now_room_ids);
1182 sort($prev_room_ids);
1183 sort($now_room_ids);
1184
1185 // check if anything has changed
1186 if ($prev_room_ids != $now_room_ids) {
1187 $data_changed[] = 'rooms';
1188 }
1189 if (array_sum($prev_adults) != array_sum($now_adults) || array_sum($prev_children) != array_sum($now_children)) {
1190 $data_changed[] = 'guests';
1191 }
1192 }
1193 }
1194
1195 // check totals
1196 if ($booking_info['total'] != $this->prevBooking['total']) {
1197 $data_changed[] = 'total';
1198 }
1199
1200 if (!$data_changed) {
1201 // no changes detected
1202 return '';
1203 }
1204
1205 // obtain the translations for what has changed
1206 $factors_changed = array_map(function($changed) {
1207 switch ($changed) {
1208 case 'checkin':
1209 return JText::translate('VBPICKUPAT');
1210
1211 case 'checkout':
1212 return JText::translate('VBRELEASEAT');
1213
1214 case 'days':
1215 return JText::translate('VBOINVTOTNIGHTS');
1216
1217 case 'rooms':
1218 return JText::translate('VBPVIEWORDERSTHREE');
1219
1220 case 'guests':
1221 return JText::translate('VBPVIEWORDERSPEOPLE');
1222
1223 case 'total':
1224 return JText::translate('VBPVIEWORDERSSEVEN');
1225
1226 default:
1227 return $changed;
1228 }
1229 }, $data_changed);
1230
1231 if (count($factors_changed) > 1) {
1232 // plural version
1233 return JText::sprintf('VBO_BOOK_MOD_SUMMARY_PLR', implode(', ', $factors_changed));
1234 }
1235
1236 // singular version
1237 return JText::sprintf('VBO_BOOK_MOD_SUMMARY_SNG', implode(', ', $factors_changed));
1238 }
1239
1240 /**
1241 * Adds the history event to the Notifications Center.
1242 *
1243 * @param object $history_record the history record created.
1244 * @param array $booking_info the booking information record.
1245 *
1246 * @return bool
1247 *
1248 * @since 1.16.8 (J) - 1.6.8 (WP)
1249 */
1250 protected function addToNotificationsCenter($history_record, array $booking_info)
1251 {
1252 if (VikBooking::isSite()) {
1253 // load language according to platform
1254 $lang = JFactory::getLanguage();
1255 if (VBOPlatformDetection::isJoomla()) {
1256 // make sure to load the back-end language definitions and any override
1257 $lang->load('com_vikbooking', JPATH_ADMINISTRATOR, $lang->getTag(), true);
1258 $lang->load('joomla', JPATH_ADMINISTRATOR, $lang->getTag(), true);
1259 } else {
1260 $lang->load('com_vikbooking', VIKBOOKING_LANG, $lang->getTag(), true);
1261
1262 // make sure to register the language handler
1263 $lib_base_path = defined('VIKBOOKING_LIBRARIES') ? VIKBOOKING_LIBRARIES : '';
1264 if (!$lib_base_path && defined('VIKCHANNELMANAGER_LIBRARIES')) {
1265 $lib_base_path = str_replace('vikchannelmanager' . DIRECTORY_SEPARATOR . 'libraries', 'vikbooking' . DIRECTORY_SEPARATOR . 'libraries', VIKCHANNELMANAGER_LIBRARIES);
1266 }
1267
1268 if ($lib_base_path) {
1269 $lang->attachHandler($lib_base_path . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR . 'admin.php', 'vikbooking');
1270 }
1271 }
1272
1273 // reload history types
1274 $this->typesMap = $this->getTypesMap();
1275 }
1276
1277 // notification payload extra options
1278 $notif_payload_opts = [];
1279
1280 // determine the notification sender
1281 $sender = 'website';
1282 $channel = null;
1283 if (!empty($booking_info['channel']) && !empty($booking_info['idorderota'])) {
1284 // set proper notification group
1285 $sender = 'otas';
1286 $channel = $booking_info['channel'];
1287
1288 /**
1289 * Set proper notification group and payload in case of a new OTA guest review.
1290 *
1291 * @since 1.16.10 (J) - 1.6.10 (WP)
1292 */
1293 if (!strcasecmp($history_record->type, 'GR')) {
1294 $sender = 'guests';
1295 $ev_edata = (array) $this->data;
1296 if (($ev_edata['review_id'] ?? null) || ($ev_edata['ota_review_id'] ?? null)) {
1297 // build CTA payload for the notification
1298 $notif_payload_opts = [
1299 'summary' => JText::sprintf('VBO_NEW_GUEST_REVIEW_SUMM', ($ev_edata['guest_name'] ?? ''), ($ev_edata['score'] ?? '?') . '/10'),
1300 'label' => JText::translate('VBO_NEW_GUEST_REVIEW'),
1301 'widget' => 'guest_reviews',
1302 'widget_options' => [
1303 'review_id' => $ev_edata['review_id'] ?? 0,
1304 'ota_review_id' => $ev_edata['ota_review_id'] ?? '',
1305 ],
1306 ];
1307 }
1308 }
1309 }
1310
1311 // build the notification summary
1312 $summary = $history_record->descr ?? null;
1313 if ($this->prevBooking) {
1314 // in case of booking modification, we compose a summary of just what was changed
1315 $summary = $this->getBookingModificationSummary($booking_info) ?: $summary;
1316 }
1317
1318 // store the notification
1319 try {
1320 $result = VBOFactory::getNotificationCenter()
1321 ->store([
1322 array_merge([
1323 'sender' => $sender,
1324 'type' => $history_record->type,
1325 'title' => $this->validType($history_record->type, true),
1326 'summary' => $summary,
1327 'idorder' => $history_record->idorder,
1328 'idorderota' => $booking_info['idorderota'] ?: null,
1329 'channel' => $channel,
1330 ], $notif_payload_opts),
1331 ]);
1332 } catch (Exception $e) {
1333 return false;
1334 }
1335
1336 return !empty($result['new_notifications']);
1337 }
1338 }
1339