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

1,356 lines 36.5 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 return $this;
491 }
492
493 /**
494 * Sets the current booking record.
495 *
496 * @param array $booking The current booking record.
497 *
498 * @return VboBookingHistory
499 *
500 * @since 1.18.0 (J) - 1.8.0 (WP)
501 */
502 public function setBookingInfo(array $booking)
503 {
504 $this->bookingInfo = $booking;
505
506 if (is_array(($booking['rooms_info'] ?? null)) && $booking['rooms_info']) {
507 // booking room records may be available within the booking record itself
508 $this->setBookingRooms($booking['rooms_info']);
509 }
510
511 return $this;
512 }
513
514 /**
515 * Sets the current booking data.
516 *
517 * @param array $booking The current booking record.
518 * @param array $booking_rooms The current booking room records.
519 *
520 * @return VboBookingHistory
521 *
522 * @since 1.18.0 (J) - 1.8.0 (WP)
523 */
524 public function setBookingData(array $booking, array $booking_rooms = [])
525 {
526 $this->setBookingInfo($booking);
527
528 if ($booking_rooms) {
529 $this->setBookingRooms($booking_rooms);
530 }
531
532 return $this;
533 }
534
535 /**
536 * Sets the current flag for silencing the notification center population.
537 *
538 * @param bool $set Whether to silence the notification center once.
539 *
540 * @return VboBookingHistory
541 *
542 * @since 1.18.3 (J) - 1.8.3 (WP)
543 */
544 public function setSilentNotificationCenter(bool $set)
545 {
546 $this->silentNotificationCenter = $set;
547
548 return $this;
549 }
550
551 /**
552 * Returns a list of history event types whenenver a specific booking event occurs.
553 *
554 * @param string $type The booking event type identifier ("new", "modified",
555 * "cancelled", "precheckin").
556 *
557 * @return array
558 *
559 * @since 1.18.6 (J) - 1.8.6 (WP)
560 */
561 public function getBookingEventsType(string $type)
562 {
563 if (!strcasecmp($type, 'new')) {
564 return $this->bookingEventsRecordNew;
565 }
566
567 if (!strcasecmp($type, 'modified')) {
568 return $this->bookingEventsRecordModified;
569 }
570
571 if (!strcasecmp($type, 'cancelled')) {
572 return $this->bookingEventsRecordCancelled;
573 }
574
575 if (!strcasecmp($type, 'precheckin')) {
576 return $this->bookingPrecheckinEvents;
577 }
578
579 return [];
580 }
581
582 /**
583 * Fetches the current booking record.
584 *
585 * @return ?array
586 */
587 protected function getBookingInfo()
588 {
589 $dbo = JFactory::getDbo();
590
591 if ($this->bookingInfo && ($this->bookingInfo['id'] ?? 0) == $this->bid) {
592 // return the cached value
593 return $this->bookingInfo;
594 }
595
596 $q = $dbo->getQuery(true)
597 ->select('*')
598 ->from($dbo->qn('#__vikbooking_orders'))
599 ->where($dbo->qn('id') . ' = ' . (int) $this->bid);
600
601 $dbo->setQuery($q, 0, 1);
602 $record = $dbo->loadAssoc();
603
604 if (!$record) {
605 return null;
606 }
607
608 // cache booking information internally
609 $this->bookingInfo = $record;
610
611 return $record;
612 }
613
614 /**
615 * Stores a new history record for the booking.
616 *
617 * @param string $type Char-type of storing we are making for the history.
618 * @param string $descr Optional description of this booking record.
619 *
620 * @return bool
621 */
622 public function store($type, $descr = '')
623 {
624 $dbo = JFactory::getDbo();
625
626 if (is_null($this->bid) || !$this->validType($type)) {
627 return false;
628 }
629
630 if (!$booking_info = $this->getBookingInfo()) {
631 return false;
632 }
633
634 /**
635 * Define the current storing signature.
636 *
637 * @since 1.18.0 (J) - 1.8.0 (WP)
638 */
639 $storingSignature = implode('-', [$type, $this->bid]);
640
641 if (empty($descr) && $this->prevBooking) {
642 /**
643 * VCM (including the App) could set the previous booking information,
644 * so we need to calculate what has changed with the booking.
645 * Load VBO language.
646 */
647 $lang = JFactory::getLanguage();
648 $lang->load('com_vikbooking', (VBOPlatformDetection::isWordPress() ? VIKBOOKING_ADMIN_LANG : JPATH_ADMINISTRATOR), $lang->getTag(), true);
649 $descr = VikBooking::getLogBookingModification($this->prevBooking);
650 }
651
652 // get the event dispatcher
653 $dispatcher = VBOFactory::getPlatform()->getDispatcher();
654
655 // build record object
656 $history_record = new stdClass;
657 $history_record->idorder = $this->bid;
658 $history_record->dt = JFactory::getDate()->toSql();
659 $history_record->type = $type;
660 $history_record->descr = $descr ?: null;
661 $history_record->totpaid = (float) $booking_info['totpaid'];
662 $history_record->total = (float) $booking_info['total'];
663 $history_record->data = $this->data;
664
665 /**
666 * Trigger event before saving a new history record.
667 *
668 * @since 1.16.0 (J) - 1.6.0 (WP)
669 */
670 $results = $dispatcher->filter('onBeforeSaveBookingHistoryVikBooking', [$history_record, $this->prevBooking]);
671 if (in_array(false, $results, true)) {
672 return false;
673 }
674
675 /**
676 * Store the extra data for this history log. Useful to store
677 * information about the amount paid, its invoicing status or any custom data.
678 *
679 * @since 1.12 (J) - 1.1.7 (WP)
680 */
681 if (!is_null($history_record->data) && !is_scalar($history_record->data)) {
682 $history_record->data = json_encode($history_record->data);
683 }
684
685 // store record
686 if (!$dbo->insertObject('#__vikbooking_orderhistory', $history_record, 'id') || !isset($history_record->id)) {
687 return false;
688 }
689
690 /**
691 * Trigger event for the newly history record added.
692 *
693 * @since 1.16.0 (J) - 1.6.0 (WP)
694 */
695 $dispatcher->trigger('onAfterSaveBookingHistoryVikBooking', [$history_record, $this->prevBooking]);
696
697 /**
698 * Check if the event requires an entry to be stored in the Notifications Center.
699 *
700 * @since 1.16.8 (J) - 1.6.8 (WP)
701 */
702 $results = $dispatcher->filter('onBeforeSaveHistoryNotificationsCenter', [$history_record, $this->notificationsCenterTypes]);
703 if (in_array($type, $this->notificationsCenterTypes) || in_array(true, $results, true)) {
704 if ($this->silentNotificationCenter === false) {
705 // add the event to the NC
706 $this->addToNotificationsCenter($history_record, $booking_info);
707 }
708 // always reset silence flag for NC
709 $this->setSilentNotificationCenter(false);
710 }
711
712 /**
713 * Check if booking-related framework should run for the current event type.
714 *
715 * @since 1.18.0 (J) - 1.8.0 (WP)
716 * @since 1.18.4 (J) - 1.8.4 (WP) added support for Door Access Control framework.
717 * @since 1.18.6 (J) - 1.8.6 (WP) added support for booking pre-checkin events.
718 */
719 if (!in_array($storingSignature, static::$signaturesList)) {
720 // we can safely invoke any booking-related framework for a unique event
721 if (in_array($type, $this->bookingEventsRecordNew)) {
722 // process the new booking confirmation tasks
723 VBOFactory::getTaskManager()->processBookingConfirmation($this->bookingInfo, $this->bookingRooms);
724 // process the new booking confirmation event
725 VBOFactory::getDoorAccessControl()->processBookingConfirmation($this->bookingInfo, $this->bookingRooms);
726 } elseif (in_array($type, $this->bookingEventsRecordCancelled)) {
727 // process the booking cancellation tasks
728 VBOFactory::getTaskManager()->processBookingCancellation($this->bookingInfo, $this->bookingRooms);
729 // process the booking cancellation event
730 VBOFactory::getDoorAccessControl()->processBookingCancellation($this->bookingInfo, $this->bookingRooms);
731 } elseif (in_array($type, $this->bookingEventsRecordModified)) {
732 // process the booking modification tasks
733 VBOFactory::getTaskManager()->processBookingModification($this->bookingInfo, $this->bookingRooms, (array) $this->prevBooking);
734 // process the booking modification event
735 VBOFactory::getDoorAccessControl()->processBookingModification($this->bookingInfo, $this->bookingRooms, (array) $this->prevBooking);
736 } elseif (in_array($type, $this->bookingPrecheckinEvents)) {
737 // process the booking pre-checkin event
738 VBOFactory::getDoorAccessControl()->processPrecheckinCompleted($this->bookingInfo, $this->bookingRooms);
739 }
740 }
741
742 /**
743 * Populate a static cache list to prevent duplicate operations from being executed, unless required.
744 *
745 * @since 1.18.0 (J) - 1.8.0 (WP)
746 */
747 static::$signaturesList[] = $storingSignature;
748
749 // operation completed with success
750 return true;
751 }
752
753 /**
754 * Loads the history records for the booking ID set.
755 *
756 * @param int $offset query offset limit start.
757 * @param int $limit query maximum records to fetch.
758 *
759 * @return array
760 *
761 * @since 1.16.5 (J) - 1.6.5 (WP) added arguments $offset and $limit.
762 */
763 public function loadHistory($offset = 0, $limit = 0)
764 {
765 $dbo = JFactory::getDbo();
766
767 if (empty($this->bid)) {
768 return [];
769 }
770
771 $q = $dbo->getQuery(true)
772 ->select('*')
773 ->from($dbo->qn('#__vikbooking_orderhistory'))
774 ->where($dbo->qn('idorder') . ' = ' . (int) $this->bid)
775 ->order($dbo->qn('dt') . ' DESC')
776 ->order($dbo->qn('id') . ' DESC');
777
778 $dbo->setQuery($q, $offset, $limit);
779
780 return $dbo->loadAssocList();
781 }
782
783 /**
784 * Checks whether this booking has an event of the given type.
785 *
786 * @param array|string $type the type of the event.
787 *
788 * @return mixed last date on success, false otherwise.
789 *
790 * @since 1.13.5 (J) - 1.3.5 (WP)
791 * @since 1.18.16 (J) - 1.8.16 (WP) argument $type can be also an array of strings.
792 */
793 public function hasEvent($type)
794 {
795 $dbo = JFactory::getDbo();
796
797 if (empty($type) || empty($this->bid)) {
798 return false;
799 }
800
801 if (is_array($type)) {
802 $type = array_filter(array_map([$dbo, 'q'], $type));
803 if (!$type) {
804 return false;
805 }
806 }
807
808 $q = $dbo->getQuery(true)
809 ->select($dbo->qn('dt'))
810 ->from($dbo->qn('#__vikbooking_orderhistory'))
811 ->where($dbo->qn('idorder') . ' = ' . (int) $this->bid)
812 ->order($dbo->qn('dt') . ' DESC');
813
814 if (is_array($type)) {
815 $q->where($dbo->qn('type') . ' IN (' . implode(', ', $type) . ')');
816 } else {
817 $q->where($dbo->qn('type') . ' = ' . $dbo->q($type));
818 }
819
820 $dbo->setQuery($q, 0, 1);
821
822 $dt = $dbo->loadResult();
823
824 if (!$dt) {
825 return false;
826 }
827
828 return $dt;
829 }
830
831 /**
832 * Returns a list of records with data defined for the given event type.
833 * Useful to get a list of transactions data for the refund operations.
834 *
835 * @param mixed $type String or array event type(s).
836 * @param ?callable $callvalid Optional callback for the data validation.
837 * @param bool $onlydata Whether to get just the event data.
838 *
839 * @return mixed False or array with data records.
840 *
841 * @since 1.14.0 (J) - 1.4.0 (WP)
842 */
843 public function getEventsWithData($type, $callvalid = null, $onlydata = true)
844 {
845 $dbo = JFactory::getDbo();
846
847 if (empty($type) || empty($this->bid)) {
848 return false;
849 }
850
851 if (!is_array($type)) {
852 $type = [$type];
853 }
854
855 // quote all given types
856 $types = array_map([$dbo, 'q'], $type);
857
858 $q = $dbo->getQuery(true)
859 ->select('*')
860 ->from($dbo->qn('#__vikbooking_orderhistory'))
861 ->where($dbo->qn('idorder') . ' = ' . (int) $this->bid)
862 ->where($dbo->qn('type') . ' IN (' . implode(', ', $types) . ')')
863 // always keep the ordering ascending
864 ->order($dbo->qn('dt') . ' ASC');
865
866 $dbo->setQuery($q);
867 $events = $dbo->loadAssocList();
868
869 if ($events) {
870 $datas = [];
871 foreach ($events as $k => $e) {
872 $data = $e['data'] ? json_decode($e['data']) : null;
873 if (!empty($data)) {
874 $events[$k]['data'] = $data;
875 }
876 $valid_data = true;
877 if (is_callable($callvalid)) {
878 $valid_data = call_user_func($callvalid, $events[$k]['data']);
879 }
880 if ($valid_data) {
881 array_push($datas, $events[$k]['data']);
882 }
883 }
884
885 return $onlydata ? $datas : $events;
886 }
887
888 return false;
889 }
890
891 /**
892 * Gets the latest history event per booking within a list.
893 * Only the latest event per booking will be considered.
894 *
895 * @param int $start the query start offset.
896 * @param int $limit the query limit.
897 * @param int $min_id optional minimum history ID to fetch.
898 * @param array $types optional list of event types (or groups) to fetch.
899 *
900 * @return array the list of recent history record objects.
901 *
902 * @since 1.15.0 (J) - 1.5.0 (WP)
903 * @since 1.16.0 (J) - 1.6.0 (WP) query modified to fetch the customer picture.
904 * @since 1.16.0 (J) - 1.6.0 (WP) fourth argument $types introduced.
905 */
906 public function getLatestBookingEvents($start = 0, $limit = 20, $min_id = 0, array $types = [])
907 {
908 $dbo = JFactory::getDbo();
909
910 $clauses = [];
911
912 if ($min_id > 0) {
913 $clauses[] = "`h`.`id` > " . (int)$min_id;
914 } else {
915 $clauses[] = "`h`.`id` > 0";
916 }
917
918 if ($types) {
919 // scan for groups
920 $groups = [];
921 foreach ($types as $k => $type) {
922 if ($type && strlen($type) === 3 && $group = $this->getTypeGroups($type)) {
923 $groups = array_merge($groups, $group['types']);
924 unset($types[$k]);
925 }
926 }
927
928 if ($groups) {
929 // merge all types
930 $types = array_merge(array_values($types), array_unique($groups));
931 }
932
933 // quote all values
934 $types = array_map(array($dbo, 'q'), array_filter($types));
935
936 if ($types) {
937 // add query clause
938 $clauses[] = "`h`.`type` IN (" . implode(', ', $types) . ")";
939 }
940 }
941
942 /**
943 * On some servers with just 30k records in the history and 5k in bookings,
944 * the INNER JOIN to group the latest history events by booking ID is taking
945 * ~25 seconds to complete, which is not acceptable. This doesn't seem to happen
946 * on every database, but since the admin widget "latest events" requires to get
947 * just the latest history event ID by passing the limit to 1, we should not
948 * use the INNER JOIN all the times to save server resources.
949 * Refactoring performed to abandon also the LEFT JOINS which were timing out in
950 * case of several thousands of records. The widget "latest_events" crashed websites.
951 *
952 * @since 1.15.6 (J) - 1.5.12 (WP)
953 * @since 1.16.0 (J) - 1.6.0 (WP) INNER JOIN dismissed because too slow and not needed.
954 * @since 1.16.1 (J) - 1.6.1 (WP) LEFT JOIN dismissed because too slow, queries split.
955 */
956
957 // query just the history records without joining any other table, or the records would become hundreds of thousands
958 $q = $dbo->getQuery(true)
959 ->select($dbo->qn('h') . '.*')
960 ->from($dbo->qn('#__vikbooking_orderhistory', 'h'))
961 ->where(implode(' AND ', $clauses))
962 ->order($dbo->qn('h.dt') . ' DESC')
963 ->order($dbo->qn('h.id') . ' DESC');
964
965 $dbo->setQuery($q, $start, $limit);
966 $rows = $dbo->loadObjectList();
967 if (!$rows) {
968 return [];
969 }
970
971 // build a list of reservation IDs
972 $history_orders = [];
973 foreach ($rows as $row) {
974 $history_orders[] = (int)$row->idorder;
975 }
976 $history_orders = array_unique($history_orders);
977
978 // fetch the rest of the information
979 $q = "SELECT `o`.`id` AS `idorder`, `o`.`custdata`, `o`.`status`, `o`.`days`, `o`.`checkin`, `o`.`checkout`, `o`.`totpaid`,
980 `o`.`total`, `o`.`idorderota`, `o`.`channel`, `o`.`country`, `o`.`closure`, `o`.`type` AS `booking_type`, `c`.`pic`,
981 (
982 SELECT CONCAT_WS(' ',`or`.`t_first_name`,`or`.`t_last_name`)
983 FROM `#__vikbooking_ordersrooms` AS `or`
984 WHERE `or`.`idorder`=`o`.`id` LIMIT 1
985 ) AS `nominative`
986 FROM `#__vikbooking_orders` AS `o`
987 LEFT JOIN `#__vikbooking_customers_orders` AS `co` ON `co`.`idorder`=`o`.`id`
988 LEFT JOIN `#__vikbooking_customers` AS `c` ON `c`.`id`=`co`.`idcustomer`
989 WHERE `o`.`id` IN (" . implode(', ', $history_orders) . ");";
990
991 $dbo->setQuery($q);
992 $rows_data = $dbo->loadObjectList();
993 if (!$rows_data) {
994 return [];
995 }
996
997 // merge information to object records
998 foreach ($rows as &$row) {
999 foreach ($rows_data as $row_data) {
1000 if ($row_data->idorder != $row->idorder) {
1001 continue;
1002 }
1003
1004 // merge object properties by using casting
1005 $row = (object) array_merge((array) $row, (array) $row_data);
1006
1007 break;
1008 }
1009 }
1010
1011 // unset last reference
1012 unset($row);
1013
1014 // return the final list
1015 return $rows;
1016 }
1017
1018 /**
1019 * Helper method to set a new list of worthy event types.
1020 *
1021 * @param array $worthy_events list of event type identifiers.
1022 *
1023 * @return VboBookingHistory
1024 *
1025 * @since 1.15.0 (J) - 1.5.0 (WP)
1026 */
1027 public function setWorthyEventTypes($worthy_events = [])
1028 {
1029 if (is_array($worthy_events)) {
1030 $this->worthy_events = $worthy_events;
1031 }
1032
1033 return $this;
1034 }
1035
1036 /**
1037 * Helper method to get the list of worthy event types.
1038 *
1039 * @return array
1040 *
1041 * @since 1.15.4 (J) - 1.5.10 (WP)
1042 */
1043 public function getWorthyEventTypes()
1044 {
1045 return $this->worthy_events;
1046 }
1047
1048 /**
1049 * Gets the latest history events worthy of a notification.
1050 *
1051 * @param int $min_id optional minimum history ID to fetch.
1052 * @param int $limit the query limit.
1053 *
1054 * @return array the list of worthy history record objects.
1055 *
1056 * @since 1.15.0 (J) - 1.5.0 (WP)
1057 * @since 1.16.0 (J) - 1.6.0 (WP) query modified to fetch the customer picture.
1058 * INNER JOIN dismissed.
1059 */
1060 public function getWorthyEvents($min_id = 0, $limit = 0)
1061 {
1062 $dbo = JFactory::getDbo();
1063
1064 $clauses = [
1065 "`o`.`status` IS NOT NULL",
1066 "`o`.`closure` = 0"
1067 ];
1068
1069 if ($min_id > 0) {
1070 $clauses[] = "`h`.`id` > " . (int)$min_id;
1071 }
1072
1073 if ($this->worthy_events) {
1074 // quote all worthy types of event
1075 $worthy_types = array_map(array($dbo, 'q'), $this->worthy_events);
1076 $clauses[] = "`h`.`type` IN (" . implode(', ', $worthy_types) . ")";
1077 }
1078
1079 // join as few tables as possible to reduce the execution timing
1080 $q = $dbo->getQuery(true)
1081 ->select($dbo->qn('h') . '.*')
1082 ->select([
1083 $dbo->qn('o.status'),
1084 $dbo->qn('o.days'),
1085 $dbo->qn('o.checkin'),
1086 $dbo->qn('o.checkout'),
1087 $dbo->qn('o.totpaid'),
1088 $dbo->qn('o.total'),
1089 $dbo->qn('o.idorderota'),
1090 $dbo->qn('o.channel'),
1091 $dbo->qn('o.country'),
1092 ])
1093 ->from($dbo->qn('#__vikbooking_orderhistory', 'h'))
1094 ->leftJoin($dbo->qn('#__vikbooking_orders', 'o') . ' ON ' . $dbo->qn('h.idorder') . ' = ' . $dbo->qn('o.id'))
1095 ->where(implode(' AND ', $clauses))
1096 ->order($dbo->qn('h.dt') . ' DESC')
1097 ->order($dbo->qn('h.id') . ' DESC');
1098
1099 $dbo->setQuery($q, 0, $limit);
1100 $rows = $dbo->loadObjectList();
1101
1102 if (!$rows) {
1103 return [];
1104 }
1105
1106 // set the "pic" property to a default empty value
1107 foreach ($rows as &$row) {
1108 $row->pic = '';
1109 }
1110
1111 // unset last reference
1112 unset($row);
1113
1114 // build a list of reservation IDs
1115 $history_orders = [];
1116 foreach ($rows as $row) {
1117 $history_orders[] = (int)$row->idorder;
1118 }
1119 $history_orders = array_unique($history_orders);
1120
1121 // fetch the rest of the information
1122 $q = $dbo->getQuery(true)
1123 ->select([
1124 $dbo->qn('o.id'),
1125 $dbo->qn('c.pic'),
1126 ])
1127 ->from($dbo->qn('#__vikbooking_orders', 'o'))
1128 ->leftJoin($dbo->qn('#__vikbooking_customers_orders', 'co') . ' ON ' . $dbo->qn('co.idorder') . ' = ' . $dbo->qn('o.id'))
1129 ->leftJoin($dbo->qn('#__vikbooking_customers', 'c') . ' ON ' . $dbo->qn('c.id') . ' = ' . $dbo->qn('co.idcustomer'))
1130 ->where($dbo->qn('o.id') . ' IN (' . implode(', ', $history_orders) . ')');
1131
1132 $dbo->setQuery($q);
1133 $rows_data = $dbo->loadObjectList();
1134
1135 if ($rows_data) {
1136 // merge information
1137 foreach ($rows as &$row) {
1138 foreach ($rows_data as $row_data) {
1139 if ($row_data->id != $row->idorder) {
1140 continue;
1141 }
1142
1143 // set picture property
1144 $row->pic = $row_data->pic;
1145 break;
1146 }
1147 }
1148 }
1149
1150 // return all records found
1151 return $rows;
1152 }
1153
1154 /**
1155 * Builds a summary of what was changed with a booking modification event.
1156 *
1157 * @param array $booking_info the booking information record.
1158 *
1159 * @return string
1160 *
1161 * @since 1.16.8 (J) - 1.6.8 (WP)
1162 */
1163 public function getBookingModificationSummary(array $booking_info)
1164 {
1165 if (!$this->prevBooking) {
1166 return '';
1167 }
1168
1169 $data_changed = [];
1170
1171 // check the stay dates
1172 if (date('Y-m-d', $booking_info['checkin']) != date('Y-m-d', $this->prevBooking['checkin'])) {
1173 $data_changed[] = 'checkin';
1174 }
1175 if (date('Y-m-d', $booking_info['checkout']) != date('Y-m-d', $this->prevBooking['checkout'])) {
1176 $data_changed[] = 'checkout';
1177 }
1178 if ($booking_info['days'] != $this->prevBooking['days']) {
1179 $data_changed[] = 'days';
1180 }
1181
1182 // check rooms and guests
1183 if (!empty($this->prevBooking['rooms_info'])) {
1184 // build previous values
1185 $prev_room_ids = array_column($this->prevBooking['rooms_info'], 'idroom');
1186 $prev_adults = array_column($this->prevBooking['rooms_info'], 'adults');
1187 $prev_children = array_column($this->prevBooking['rooms_info'], 'children');
1188
1189 // build new values
1190 $new_rooms_info = VikBooking::loadOrdersRoomsData($booking_info['id']);
1191 if ($new_rooms_info && $prev_room_ids) {
1192 $now_room_ids = array_column($new_rooms_info, 'idroom');
1193 $now_adults = array_column($new_rooms_info, 'adults');
1194 $now_children = array_column($new_rooms_info, 'children');
1195
1196 // normalize arrays
1197 $prev_room_ids = array_map('intval', $prev_room_ids);
1198 $now_room_ids = array_map('intval', $now_room_ids);
1199 sort($prev_room_ids);
1200 sort($now_room_ids);
1201
1202 // check if anything has changed
1203 if ($prev_room_ids != $now_room_ids) {
1204 $data_changed[] = 'rooms';
1205 }
1206 if (array_sum($prev_adults) != array_sum($now_adults) || array_sum($prev_children) != array_sum($now_children)) {
1207 $data_changed[] = 'guests';
1208 }
1209 }
1210 }
1211
1212 // check totals
1213 if ($booking_info['total'] != $this->prevBooking['total']) {
1214 $data_changed[] = 'total';
1215 }
1216
1217 if (!$data_changed) {
1218 // no changes detected
1219 return '';
1220 }
1221
1222 // obtain the translations for what has changed
1223 $factors_changed = array_map(function($changed) {
1224 switch ($changed) {
1225 case 'checkin':
1226 return JText::translate('VBPICKUPAT');
1227
1228 case 'checkout':
1229 return JText::translate('VBRELEASEAT');
1230
1231 case 'days':
1232 return JText::translate('VBOINVTOTNIGHTS');
1233
1234 case 'rooms':
1235 return JText::translate('VBPVIEWORDERSTHREE');
1236
1237 case 'guests':
1238 return JText::translate('VBPVIEWORDERSPEOPLE');
1239
1240 case 'total':
1241 return JText::translate('VBPVIEWORDERSSEVEN');
1242
1243 default:
1244 return $changed;
1245 }
1246 }, $data_changed);
1247
1248 if (count($factors_changed) > 1) {
1249 // plural version
1250 return JText::sprintf('VBO_BOOK_MOD_SUMMARY_PLR', implode(', ', $factors_changed));
1251 }
1252
1253 // singular version
1254 return JText::sprintf('VBO_BOOK_MOD_SUMMARY_SNG', implode(', ', $factors_changed));
1255 }
1256
1257 /**
1258 * Adds the history event to the Notifications Center.
1259 *
1260 * @param object $history_record the history record created.
1261 * @param array $booking_info the booking information record.
1262 *
1263 * @return bool
1264 *
1265 * @since 1.16.8 (J) - 1.6.8 (WP)
1266 */
1267 protected function addToNotificationsCenter($history_record, array $booking_info)
1268 {
1269 if (VikBooking::isSite()) {
1270 // load language according to platform
1271 $lang = JFactory::getLanguage();
1272 if (VBOPlatformDetection::isJoomla()) {
1273 // make sure to load the back-end language definitions and any override
1274 $lang->load('com_vikbooking', JPATH_ADMINISTRATOR, $lang->getTag(), true);
1275 $lang->load('joomla', JPATH_ADMINISTRATOR, $lang->getTag(), true);
1276 } else {
1277 $lang->load('com_vikbooking', VIKBOOKING_LANG, $lang->getTag(), true);
1278
1279 // make sure to register the language handler
1280 $lib_base_path = defined('VIKBOOKING_LIBRARIES') ? VIKBOOKING_LIBRARIES : '';
1281 if (!$lib_base_path && defined('VIKCHANNELMANAGER_LIBRARIES')) {
1282 $lib_base_path = str_replace('vikchannelmanager' . DIRECTORY_SEPARATOR . 'libraries', 'vikbooking' . DIRECTORY_SEPARATOR . 'libraries', VIKCHANNELMANAGER_LIBRARIES);
1283 }
1284
1285 if ($lib_base_path) {
1286 $lang->attachHandler($lib_base_path . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR . 'admin.php', 'vikbooking');
1287 }
1288 }
1289
1290 // reload history types
1291 $this->typesMap = $this->getTypesMap();
1292 }
1293
1294 // notification payload extra options
1295 $notif_payload_opts = [];
1296
1297 // determine the notification sender
1298 $sender = 'website';
1299 $channel = null;
1300 if (!empty($booking_info['channel']) && !empty($booking_info['idorderota'])) {
1301 // set proper notification group
1302 $sender = 'otas';
1303 $channel = $booking_info['channel'];
1304
1305 /**
1306 * Set proper notification group and payload in case of a new OTA guest review.
1307 *
1308 * @since 1.16.10 (J) - 1.6.10 (WP)
1309 */
1310 if (!strcasecmp($history_record->type, 'GR')) {
1311 $sender = 'guests';
1312 $ev_edata = (array) $this->data;
1313 if (($ev_edata['review_id'] ?? null) || ($ev_edata['ota_review_id'] ?? null)) {
1314 // build CTA payload for the notification
1315 $notif_payload_opts = [
1316 'summary' => JText::sprintf('VBO_NEW_GUEST_REVIEW_SUMM', ($ev_edata['guest_name'] ?? ''), ($ev_edata['score'] ?? '?') . '/10'),
1317 'label' => JText::translate('VBO_NEW_GUEST_REVIEW'),
1318 'widget' => 'guest_reviews',
1319 'widget_options' => [
1320 'review_id' => $ev_edata['review_id'] ?? 0,
1321 'ota_review_id' => $ev_edata['ota_review_id'] ?? '',
1322 ],
1323 ];
1324 }
1325 }
1326 }
1327
1328 // build the notification summary
1329 $summary = $history_record->descr ?? null;
1330 if ($this->prevBooking) {
1331 // in case of booking modification, we compose a summary of just what was changed
1332 $summary = $this->getBookingModificationSummary($booking_info) ?: $summary;
1333 }
1334
1335 // store the notification
1336 try {
1337 $result = VBOFactory::getNotificationCenter()
1338 ->store([
1339 array_merge([
1340 'sender' => $sender,
1341 'type' => $history_record->type,
1342 'title' => $this->validType($history_record->type, true),
1343 'summary' => $summary,
1344 'idorder' => $history_record->idorder,
1345 'idorderota' => $booking_info['idorderota'] ?: null,
1346 'channel' => $channel,
1347 ], $notif_payload_opts),
1348 ]);
1349 } catch (Exception $e) {
1350 return false;
1351 }
1352
1353 return !empty($result['new_notifications']);
1354 }
1355 }
1356