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

1,343 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 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 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 */
792 public function hasEvent($type)
793 {
794 $dbo = JFactory::getDbo();
795
796 if (empty($type) || empty($this->bid)) {
797 return false;
798 }
799
800 $q = $dbo->getQuery(true)
801 ->select($dbo->qn('dt'))
802 ->from($dbo->qn('#__vikbooking_orderhistory'))
803 ->where($dbo->qn('idorder') . ' = ' . (int) $this->bid)
804 ->where($dbo->qn('type') . ' = ' . $dbo->q($type))
805 ->order($dbo->qn('dt') . ' DESC');
806
807 $dbo->setQuery($q, 0, 1);
808
809 $dt = $dbo->loadResult();
810
811 if (!$dt) {
812 return false;
813 }
814
815 return $dt;
816 }
817
818 /**
819 * Returns a list of records with data defined for the given event type.
820 * Useful to get a list of transactions data for the refund operations.
821 *
822 * @param mixed $type String or array event type(s).
823 * @param ?callable $callvalid Optional callback for the data validation.
824 * @param bool $onlydata Whether to get just the event data.
825 *
826 * @return mixed False or array with data records.
827 *
828 * @since 1.14.0 (J) - 1.4.0 (WP)
829 */
830 public function getEventsWithData($type, $callvalid = null, $onlydata = true)
831 {
832 $dbo = JFactory::getDbo();
833
834 if (empty($type) || empty($this->bid)) {
835 return false;
836 }
837
838 if (!is_array($type)) {
839 $type = [$type];
840 }
841
842 // quote all given types
843 $types = array_map([$dbo, 'q'], $type);
844
845 $q = $dbo->getQuery(true)
846 ->select('*')
847 ->from($dbo->qn('#__vikbooking_orderhistory'))
848 ->where($dbo->qn('idorder') . ' = ' . (int) $this->bid)
849 ->where($dbo->qn('type') . ' IN (' . implode(', ', $types) . ')')
850 // always keep the ordering ascending
851 ->order($dbo->qn('dt') . ' ASC');
852
853 $dbo->setQuery($q);
854 $events = $dbo->loadAssocList();
855
856 if ($events) {
857 $datas = [];
858 foreach ($events as $k => $e) {
859 $data = $e['data'] ? json_decode($e['data']) : null;
860 if (!empty($data)) {
861 $events[$k]['data'] = $data;
862 }
863 $valid_data = true;
864 if (is_callable($callvalid)) {
865 $valid_data = call_user_func($callvalid, $events[$k]['data']);
866 }
867 if ($valid_data) {
868 array_push($datas, $events[$k]['data']);
869 }
870 }
871
872 return $onlydata ? $datas : $events;
873 }
874
875 return false;
876 }
877
878 /**
879 * Gets the latest history event per booking within a list.
880 * Only the latest event per booking will be considered.
881 *
882 * @param int $start the query start offset.
883 * @param int $limit the query limit.
884 * @param int $min_id optional minimum history ID to fetch.
885 * @param array $types optional list of event types (or groups) to fetch.
886 *
887 * @return array the list of recent history record objects.
888 *
889 * @since 1.15.0 (J) - 1.5.0 (WP)
890 * @since 1.16.0 (J) - 1.6.0 (WP) query modified to fetch the customer picture.
891 * @since 1.16.0 (J) - 1.6.0 (WP) fourth argument $types introduced.
892 */
893 public function getLatestBookingEvents($start = 0, $limit = 20, $min_id = 0, array $types = [])
894 {
895 $dbo = JFactory::getDbo();
896
897 $clauses = [];
898
899 if ($min_id > 0) {
900 $clauses[] = "`h`.`id` > " . (int)$min_id;
901 } else {
902 $clauses[] = "`h`.`id` > 0";
903 }
904
905 if ($types) {
906 // scan for groups
907 $groups = [];
908 foreach ($types as $k => $type) {
909 if ($type && strlen($type) === 3 && $group = $this->getTypeGroups($type)) {
910 $groups = array_merge($groups, $group['types']);
911 unset($types[$k]);
912 }
913 }
914
915 if ($groups) {
916 // merge all types
917 $types = array_merge(array_values($types), array_unique($groups));
918 }
919
920 // quote all values
921 $types = array_map(array($dbo, 'q'), array_filter($types));
922
923 if ($types) {
924 // add query clause
925 $clauses[] = "`h`.`type` IN (" . implode(', ', $types) . ")";
926 }
927 }
928
929 /**
930 * On some servers with just 30k records in the history and 5k in bookings,
931 * the INNER JOIN to group the latest history events by booking ID is taking
932 * ~25 seconds to complete, which is not acceptable. This doesn't seem to happen
933 * on every database, but since the admin widget "latest events" requires to get
934 * just the latest history event ID by passing the limit to 1, we should not
935 * use the INNER JOIN all the times to save server resources.
936 * Refactoring performed to abandon also the LEFT JOINS which were timing out in
937 * case of several thousands of records. The widget "latest_events" crashed websites.
938 *
939 * @since 1.15.6 (J) - 1.5.12 (WP)
940 * @since 1.16.0 (J) - 1.6.0 (WP) INNER JOIN dismissed because too slow and not needed.
941 * @since 1.16.1 (J) - 1.6.1 (WP) LEFT JOIN dismissed because too slow, queries split.
942 */
943
944 // query just the history records without joining any other table, or the records would become hundreds of thousands
945 $q = $dbo->getQuery(true)
946 ->select($dbo->qn('h') . '.*')
947 ->from($dbo->qn('#__vikbooking_orderhistory', 'h'))
948 ->where(implode(' AND ', $clauses))
949 ->order($dbo->qn('h.dt') . ' DESC')
950 ->order($dbo->qn('h.id') . ' DESC');
951
952 $dbo->setQuery($q, $start, $limit);
953 $rows = $dbo->loadObjectList();
954 if (!$rows) {
955 return [];
956 }
957
958 // build a list of reservation IDs
959 $history_orders = [];
960 foreach ($rows as $row) {
961 $history_orders[] = (int)$row->idorder;
962 }
963 $history_orders = array_unique($history_orders);
964
965 // fetch the rest of the information
966 $q = "SELECT `o`.`id` AS `idorder`, `o`.`custdata`, `o`.`status`, `o`.`days`, `o`.`checkin`, `o`.`checkout`, `o`.`totpaid`,
967 `o`.`total`, `o`.`idorderota`, `o`.`channel`, `o`.`country`, `o`.`closure`, `o`.`type` AS `booking_type`, `c`.`pic`,
968 (
969 SELECT CONCAT_WS(' ',`or`.`t_first_name`,`or`.`t_last_name`)
970 FROM `#__vikbooking_ordersrooms` AS `or`
971 WHERE `or`.`idorder`=`o`.`id` LIMIT 1
972 ) AS `nominative`
973 FROM `#__vikbooking_orders` AS `o`
974 LEFT JOIN `#__vikbooking_customers_orders` AS `co` ON `co`.`idorder`=`o`.`id`
975 LEFT JOIN `#__vikbooking_customers` AS `c` ON `c`.`id`=`co`.`idcustomer`
976 WHERE `o`.`id` IN (" . implode(', ', $history_orders) . ");";
977
978 $dbo->setQuery($q);
979 $rows_data = $dbo->loadObjectList();
980 if (!$rows_data) {
981 return [];
982 }
983
984 // merge information to object records
985 foreach ($rows as &$row) {
986 foreach ($rows_data as $row_data) {
987 if ($row_data->idorder != $row->idorder) {
988 continue;
989 }
990
991 // merge object properties by using casting
992 $row = (object) array_merge((array) $row, (array) $row_data);
993
994 break;
995 }
996 }
997
998 // unset last reference
999 unset($row);
1000
1001 // return the final list
1002 return $rows;
1003 }
1004
1005 /**
1006 * Helper method to set a new list of worthy event types.
1007 *
1008 * @param array $worthy_events list of event type identifiers.
1009 *
1010 * @return VboBookingHistory
1011 *
1012 * @since 1.15.0 (J) - 1.5.0 (WP)
1013 */
1014 public function setWorthyEventTypes($worthy_events = [])
1015 {
1016 if (is_array($worthy_events)) {
1017 $this->worthy_events = $worthy_events;
1018 }
1019
1020 return $this;
1021 }
1022
1023 /**
1024 * Helper method to get the list of worthy event types.
1025 *
1026 * @return array
1027 *
1028 * @since 1.15.4 (J) - 1.5.10 (WP)
1029 */
1030 public function getWorthyEventTypes()
1031 {
1032 return $this->worthy_events;
1033 }
1034
1035 /**
1036 * Gets the latest history events worthy of a notification.
1037 *
1038 * @param int $min_id optional minimum history ID to fetch.
1039 * @param int $limit the query limit.
1040 *
1041 * @return array the list of worthy history record objects.
1042 *
1043 * @since 1.15.0 (J) - 1.5.0 (WP)
1044 * @since 1.16.0 (J) - 1.6.0 (WP) query modified to fetch the customer picture.
1045 * INNER JOIN dismissed.
1046 */
1047 public function getWorthyEvents($min_id = 0, $limit = 0)
1048 {
1049 $dbo = JFactory::getDbo();
1050
1051 $clauses = [
1052 "`o`.`status` IS NOT NULL",
1053 "`o`.`closure` = 0"
1054 ];
1055
1056 if ($min_id > 0) {
1057 $clauses[] = "`h`.`id` > " . (int)$min_id;
1058 }
1059
1060 if ($this->worthy_events) {
1061 // quote all worthy types of event
1062 $worthy_types = array_map(array($dbo, 'q'), $this->worthy_events);
1063 $clauses[] = "`h`.`type` IN (" . implode(', ', $worthy_types) . ")";
1064 }
1065
1066 // join as few tables as possible to reduce the execution timing
1067 $q = $dbo->getQuery(true)
1068 ->select($dbo->qn('h') . '.*')
1069 ->select([
1070 $dbo->qn('o.status'),
1071 $dbo->qn('o.days'),
1072 $dbo->qn('o.checkin'),
1073 $dbo->qn('o.checkout'),
1074 $dbo->qn('o.totpaid'),
1075 $dbo->qn('o.total'),
1076 $dbo->qn('o.idorderota'),
1077 $dbo->qn('o.channel'),
1078 $dbo->qn('o.country'),
1079 ])
1080 ->from($dbo->qn('#__vikbooking_orderhistory', 'h'))
1081 ->leftJoin($dbo->qn('#__vikbooking_orders', 'o') . ' ON ' . $dbo->qn('h.idorder') . ' = ' . $dbo->qn('o.id'))
1082 ->where(implode(' AND ', $clauses))
1083 ->order($dbo->qn('h.dt') . ' DESC')
1084 ->order($dbo->qn('h.id') . ' DESC');
1085
1086 $dbo->setQuery($q, 0, $limit);
1087 $rows = $dbo->loadObjectList();
1088
1089 if (!$rows) {
1090 return [];
1091 }
1092
1093 // set the "pic" property to a default empty value
1094 foreach ($rows as &$row) {
1095 $row->pic = '';
1096 }
1097
1098 // unset last reference
1099 unset($row);
1100
1101 // build a list of reservation IDs
1102 $history_orders = [];
1103 foreach ($rows as $row) {
1104 $history_orders[] = (int)$row->idorder;
1105 }
1106 $history_orders = array_unique($history_orders);
1107
1108 // fetch the rest of the information
1109 $q = $dbo->getQuery(true)
1110 ->select([
1111 $dbo->qn('o.id'),
1112 $dbo->qn('c.pic'),
1113 ])
1114 ->from($dbo->qn('#__vikbooking_orders', 'o'))
1115 ->leftJoin($dbo->qn('#__vikbooking_customers_orders', 'co') . ' ON ' . $dbo->qn('co.idorder') . ' = ' . $dbo->qn('o.id'))
1116 ->leftJoin($dbo->qn('#__vikbooking_customers', 'c') . ' ON ' . $dbo->qn('c.id') . ' = ' . $dbo->qn('co.idcustomer'))
1117 ->where($dbo->qn('o.id') . ' IN (' . implode(', ', $history_orders) . ')');
1118
1119 $dbo->setQuery($q);
1120 $rows_data = $dbo->loadObjectList();
1121
1122 if ($rows_data) {
1123 // merge information
1124 foreach ($rows as &$row) {
1125 foreach ($rows_data as $row_data) {
1126 if ($row_data->id != $row->idorder) {
1127 continue;
1128 }
1129
1130 // set picture property
1131 $row->pic = $row_data->pic;
1132 break;
1133 }
1134 }
1135 }
1136
1137 // return all records found
1138 return $rows;
1139 }
1140
1141 /**
1142 * Builds a summary of what was changed with a booking modification event.
1143 *
1144 * @param array $booking_info the booking information record.
1145 *
1146 * @return string
1147 *
1148 * @since 1.16.8 (J) - 1.6.8 (WP)
1149 */
1150 public function getBookingModificationSummary(array $booking_info)
1151 {
1152 if (!$this->prevBooking) {
1153 return '';
1154 }
1155
1156 $data_changed = [];
1157
1158 // check the stay dates
1159 if (date('Y-m-d', $booking_info['checkin']) != date('Y-m-d', $this->prevBooking['checkin'])) {
1160 $data_changed[] = 'checkin';
1161 }
1162 if (date('Y-m-d', $booking_info['checkout']) != date('Y-m-d', $this->prevBooking['checkout'])) {
1163 $data_changed[] = 'checkout';
1164 }
1165 if ($booking_info['days'] != $this->prevBooking['days']) {
1166 $data_changed[] = 'days';
1167 }
1168
1169 // check rooms and guests
1170 if (!empty($this->prevBooking['rooms_info'])) {
1171 // build previous values
1172 $prev_room_ids = array_column($this->prevBooking['rooms_info'], 'idroom');
1173 $prev_adults = array_column($this->prevBooking['rooms_info'], 'adults');
1174 $prev_children = array_column($this->prevBooking['rooms_info'], 'children');
1175
1176 // build new values
1177 $new_rooms_info = VikBooking::loadOrdersRoomsData($booking_info['id']);
1178 if ($new_rooms_info && $prev_room_ids) {
1179 $now_room_ids = array_column($new_rooms_info, 'idroom');
1180 $now_adults = array_column($new_rooms_info, 'adults');
1181 $now_children = array_column($new_rooms_info, 'children');
1182
1183 // normalize arrays
1184 $prev_room_ids = array_map('intval', $prev_room_ids);
1185 $now_room_ids = array_map('intval', $now_room_ids);
1186 sort($prev_room_ids);
1187 sort($now_room_ids);
1188
1189 // check if anything has changed
1190 if ($prev_room_ids != $now_room_ids) {
1191 $data_changed[] = 'rooms';
1192 }
1193 if (array_sum($prev_adults) != array_sum($now_adults) || array_sum($prev_children) != array_sum($now_children)) {
1194 $data_changed[] = 'guests';
1195 }
1196 }
1197 }
1198
1199 // check totals
1200 if ($booking_info['total'] != $this->prevBooking['total']) {
1201 $data_changed[] = 'total';
1202 }
1203
1204 if (!$data_changed) {
1205 // no changes detected
1206 return '';
1207 }
1208
1209 // obtain the translations for what has changed
1210 $factors_changed = array_map(function($changed) {
1211 switch ($changed) {
1212 case 'checkin':
1213 return JText::translate('VBPICKUPAT');
1214
1215 case 'checkout':
1216 return JText::translate('VBRELEASEAT');
1217
1218 case 'days':
1219 return JText::translate('VBOINVTOTNIGHTS');
1220
1221 case 'rooms':
1222 return JText::translate('VBPVIEWORDERSTHREE');
1223
1224 case 'guests':
1225 return JText::translate('VBPVIEWORDERSPEOPLE');
1226
1227 case 'total':
1228 return JText::translate('VBPVIEWORDERSSEVEN');
1229
1230 default:
1231 return $changed;
1232 }
1233 }, $data_changed);
1234
1235 if (count($factors_changed) > 1) {
1236 // plural version
1237 return JText::sprintf('VBO_BOOK_MOD_SUMMARY_PLR', implode(', ', $factors_changed));
1238 }
1239
1240 // singular version
1241 return JText::sprintf('VBO_BOOK_MOD_SUMMARY_SNG', implode(', ', $factors_changed));
1242 }
1243
1244 /**
1245 * Adds the history event to the Notifications Center.
1246 *
1247 * @param object $history_record the history record created.
1248 * @param array $booking_info the booking information record.
1249 *
1250 * @return bool
1251 *
1252 * @since 1.16.8 (J) - 1.6.8 (WP)
1253 */
1254 protected function addToNotificationsCenter($history_record, array $booking_info)
1255 {
1256 if (VikBooking::isSite()) {
1257 // load language according to platform
1258 $lang = JFactory::getLanguage();
1259 if (VBOPlatformDetection::isJoomla()) {
1260 // make sure to load the back-end language definitions and any override
1261 $lang->load('com_vikbooking', JPATH_ADMINISTRATOR, $lang->getTag(), true);
1262 $lang->load('joomla', JPATH_ADMINISTRATOR, $lang->getTag(), true);
1263 } else {
1264 $lang->load('com_vikbooking', VIKBOOKING_LANG, $lang->getTag(), true);
1265
1266 // make sure to register the language handler
1267 $lib_base_path = defined('VIKBOOKING_LIBRARIES') ? VIKBOOKING_LIBRARIES : '';
1268 if (!$lib_base_path && defined('VIKCHANNELMANAGER_LIBRARIES')) {
1269 $lib_base_path = str_replace('vikchannelmanager' . DIRECTORY_SEPARATOR . 'libraries', 'vikbooking' . DIRECTORY_SEPARATOR . 'libraries', VIKCHANNELMANAGER_LIBRARIES);
1270 }
1271
1272 if ($lib_base_path) {
1273 $lang->attachHandler($lib_base_path . DIRECTORY_SEPARATOR . 'language' . DIRECTORY_SEPARATOR . 'admin.php', 'vikbooking');
1274 }
1275 }
1276
1277 // reload history types
1278 $this->typesMap = $this->getTypesMap();
1279 }
1280
1281 // notification payload extra options
1282 $notif_payload_opts = [];
1283
1284 // determine the notification sender
1285 $sender = 'website';
1286 $channel = null;
1287 if (!empty($booking_info['channel']) && !empty($booking_info['idorderota'])) {
1288 // set proper notification group
1289 $sender = 'otas';
1290 $channel = $booking_info['channel'];
1291
1292 /**
1293 * Set proper notification group and payload in case of a new OTA guest review.
1294 *
1295 * @since 1.16.10 (J) - 1.6.10 (WP)
1296 */
1297 if (!strcasecmp($history_record->type, 'GR')) {
1298 $sender = 'guests';
1299 $ev_edata = (array) $this->data;
1300 if (($ev_edata['review_id'] ?? null) || ($ev_edata['ota_review_id'] ?? null)) {
1301 // build CTA payload for the notification
1302 $notif_payload_opts = [
1303 'summary' => JText::sprintf('VBO_NEW_GUEST_REVIEW_SUMM', ($ev_edata['guest_name'] ?? ''), ($ev_edata['score'] ?? '?') . '/10'),
1304 'label' => JText::translate('VBO_NEW_GUEST_REVIEW'),
1305 'widget' => 'guest_reviews',
1306 'widget_options' => [
1307 'review_id' => $ev_edata['review_id'] ?? 0,
1308 'ota_review_id' => $ev_edata['ota_review_id'] ?? '',
1309 ],
1310 ];
1311 }
1312 }
1313 }
1314
1315 // build the notification summary
1316 $summary = $history_record->descr ?? null;
1317 if ($this->prevBooking) {
1318 // in case of booking modification, we compose a summary of just what was changed
1319 $summary = $this->getBookingModificationSummary($booking_info) ?: $summary;
1320 }
1321
1322 // store the notification
1323 try {
1324 $result = VBOFactory::getNotificationCenter()
1325 ->store([
1326 array_merge([
1327 'sender' => $sender,
1328 'type' => $history_record->type,
1329 'title' => $this->validType($history_record->type, true),
1330 'summary' => $summary,
1331 'idorder' => $history_record->idorder,
1332 'idorderota' => $booking_info['idorderota'] ?: null,
1333 'channel' => $channel,
1334 ], $notif_payload_opts),
1335 ]);
1336 } catch (Exception $e) {
1337 return false;
1338 }
1339
1340 return !empty($result['new_notifications']);
1341 }
1342 }
1343