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
← All changes | admin/helpers/src/booking/registry.php +812 -13 1.8.6 → 1.8.16 View file →
@@ -38,13 +38,28 @@
38 38 */
39 39 protected $currentRoomIndex = 0;
40 40
41 41 /**
42 + * @var int
43 + */
44 + protected $currentRoomNumber = 0;
45 +
46 + /**
42 47 * @var array
43 48 */
44 49 protected $dacData = [];
45 50
46 51 /**
52 + * @var array
53 + */
54 + protected $roomDetails = [];
55 +
56 + /**
57 + * @var ?array
58 + */
59 + protected $customer = null;
60 +
61 + /**
47 62 * Proxy to construct the object.
48 63 *
49 64 * @param array $options Associative list of booking information to bind.
50 65 * @param array $rooms Associative list of booking rooms to bind.
@@ -67,18 +82,18 @@
67 82 * @throws Exception
68 83 */
69 84 public function __construct(array $options, array $rooms = [], array $previous = [])
70 85 {
71 - if (empty($options['id'])) {
72 - throw new Exception('Missing booking ID.', 500);
73 - }
74 -
75 86 // ensure we have enough booking details
76 - if ($options === ['id' => $options['id']]) {
87 + if ($options === ['id' => $options['id'] ?? 0]) {
77 88 // load full booking details
78 89 $options = VikBooking::getBookingInfoFromID($options['id']);
79 90 }
80 91
92 + if (empty($options['id'])) {
93 + throw new Exception('Missing booking ID.', 500);
94 + }
95 +
81 96 // bind booking options to internal registry
82 97 $this->bind($options);
83 98
84 99 if (!$rooms) {
@@ -85,8 +100,12 @@
85 100 // load booking rooms
86 101 $rooms = VikBooking::loadOrdersRoomsData($this->getID());
87 102 }
88 103
104 + if (!$rooms) {
105 + throw new Exception('No booking rooms found.', 404);
106 + }
107 +
89 108 // bind booking rooms
90 109 $this->bookingRooms = $rooms;
91 110
92 111 // bind previous booking information in case of alteration
@@ -117,12 +136,23 @@
117 136
118 137 /**
119 138 * Returns the number of nights of stay for the current booking ID.
120 139 *
140 + * @param bool $roomLevel True to calculate the nights at room-level.
141 + *
121 142 * @return int
122 143 */
123 - public function getTotalNights()
144 + public function getTotalNights(bool $roomLevel = false)
124 145 {
146 + if ($roomLevel) {
147 + // get the stay timestamps at room-level
148 + $stayTimestamps = $this->getStayTimestamps(true);
149 +
150 + // return the room-level nights of stay
151 + return VikBooking::getAvailabilityInstance()->countNightsOfStay($stayTimestamps[0], $stayTimestamps[1]) ?: 1;
152 + }
153 +
154 + // return the booking nights of stay
125 155 return (int) $this->getProperty('days', 1);
126 156 }
127 157
128 158 /**
@@ -188,8 +218,23 @@
188 218 return $this->registry[$name] ?? $default;
189 219 }
190 220
191 221 /**
222 + * Sets a value for the requested registry property name.
223 + *
224 + * @param string $name The registry property to set.
225 + * @param mixed $value The value to set.
226 + *
227 + * @return self
228 + */
229 + public function setProperty(string $name, $value)
230 + {
231 + $this->registry[$name] = $value;
232 +
233 + return $this;
234 + }
235 +
236 + /**
192 237 * Returns the requested previous booking property name.
193 238 *
194 239 * @param string $name The previous booking property to fetch.
195 240 * @param mixed $default The default value to return.
@@ -289,8 +334,233 @@
289 334 return array_values(array_unique(array_map('intval', array_column($this->bookingRooms, 'idroom'))));
290 335 }
291 336
292 337 /**
338 + * Returns a list of booked listing IDs and subunits (0 if no subunit).
339 + *
340 + * @param bool $unique True to only get a unique list.
341 + *
342 + * @return array Linear array of strings as "roomid-subunit".
343 + *
344 + * @since 1.18.7 (J) - 1.8.7 (WP)
345 + */
346 + public function getBookedListingSubunits(bool $unique = true)
347 + {
348 + $bookedListingSubunits = [];
349 +
350 + foreach ($this->bookingRooms as $bookingRoom) {
351 + // push booked listing ID and related subunit number
352 + $bookedListingSubunits[] = sprintf('%d-%d', $bookingRoom['idroom'], (int) ($bookingRoom['roomindex'] ?? 0));
353 + }
354 +
355 + if ($unique) {
356 + // remove duplicate entries on listing-level with no subunits
357 + $bookedListingSubunits = array_values(array_unique($bookedListingSubunits));
358 + }
359 +
360 + return $bookedListingSubunits;
361 + }
362 +
363 + /**
364 + * Counts the number of total adults, either at booking or room level.
365 + *
366 + * @param bool $roomLevel True to count the adults of the current room.
367 + *
368 + * @return int
369 + *
370 + * @since 1.18.7 (J) - 1.8.7 (WP)
371 + */
372 + public function countTotalAdults(bool $roomLevel = false)
373 + {
374 + if ($roomLevel) {
375 + foreach ($this->bookingRooms as $index => $bookingRoom) {
376 + if ($index == $this->getCurrentRoomIndex()) {
377 + return (int) ($bookingRoom['adults'] ?? 0);
378 + }
379 + }
380 + }
381 +
382 + return array_sum(array_map('intval', array_column($this->bookingRooms, 'adults')));
383 + }
384 +
385 + /**
386 + * Counts the number of total children, either at booking or room level.
387 + *
388 + * @param bool $roomLevel True to count the children of the current room.
389 + *
390 + * @return int
391 + *
392 + * @since 1.18.7 (J) - 1.8.7 (WP)
393 + */
394 + public function countTotalChildren(bool $roomLevel = false)
395 + {
396 + if ($roomLevel) {
397 + foreach ($this->bookingRooms as $index => $bookingRoom) {
398 + if ($index == $this->getCurrentRoomIndex()) {
399 + return (int) ($bookingRoom['children'] ?? 0);
400 + }
401 + }
402 + }
403 +
404 + return array_sum(array_map('intval', array_column($this->bookingRooms, 'children')));
405 + }
406 +
407 + /**
408 + * Counts the number of total pets, either at booking or room level.
409 + *
410 + * @param bool $roomLevel True to count the pets of the current room.
411 + *
412 + * @return int
413 + *
414 + * @since 1.18.7 (J) - 1.8.7 (WP)
415 + */
416 + public function countTotalPets(bool $roomLevel = false)
417 + {
418 + if ($roomLevel) {
419 + foreach ($this->bookingRooms as $index => $bookingRoom) {
420 + if ($index == $this->getCurrentRoomIndex()) {
421 + return (int) ($bookingRoom['pets'] ?? 0);
422 + }
423 + }
424 + }
425 +
426 + return array_sum(array_map('intval', array_column($this->bookingRooms, 'pets')));
427 + }
428 +
429 + /**
430 + * Counts the number of total guests, either at booking or room level.
431 + *
432 + * @param bool $roomLevel True to count the guests of the current room.
433 + *
434 + * @return int
435 + *
436 + * @since 1.18.7 (J) - 1.8.7 (WP)
437 + */
438 + public function countTotalGuests(bool $roomLevel = false)
439 + {
440 + return $this->countTotalAdults($roomLevel) + $this->countTotalChildren($roomLevel);
441 + }
442 +
443 + /**
444 + * Returns the booking or customer country code, if any.
445 + *
446 + * @return ?string
447 + *
448 + * @since 1.18.8 (J) - 1.8.8 (WP)
449 + */
450 + public function getCountry()
451 + {
452 + // give higher priority to booking-level country
453 + $countryCode = $this->getProperty('country');
454 +
455 + if (!$countryCode) {
456 + // fetch booking customer details
457 + $customer = $this->getCustomer();
458 +
459 + // check customer-level country
460 + $countryCode = ($customer['country'] ?? '') ?: $countryCode;
461 + }
462 +
463 + return $countryCode ?: null;
464 + }
465 +
466 + /**
467 + * Returns the booking or customer phone number, if any.
468 + * Note that the + prefix may not be included.
469 + *
470 + * @param ?string $forcedPhone Optional phone number to sanitize.
471 + *
472 + * @return ?string
473 + *
474 + * @since 1.18.8 (J) - 1.8.8 (WP)
475 + * @since 1.18.16 (J) - 1.8.16 (WP) added argument $forcedPhone.
476 + */
477 + public function getPhoneNumber(?string $forcedPhone = null)
478 + {
479 + // give higher priority to passed or booking-level phone number
480 + $phoneNumber = $forcedPhone ?: $this->getProperty('phone');
481 +
482 + if (!$phoneNumber && $forcedPhone !== null) {
483 + // fetch booking customer details
484 + $customer = $this->getCustomer();
485 +
486 + // check customer-level phone number
487 + $phoneNumber = ($customer['phone'] ?? '') ?: $phoneNumber;
488 + }
489 +
490 + // trim phone number
491 + $phoneNumber = (string) $phoneNumber;
492 +
493 + // sanitize number by keeping the + char only if at the beginning of the string
494 + $phoneNumber = preg_replace('/(?!^)\+|[^0-9+]/', '', $phoneNumber);
495 +
496 + if (substr($phoneNumber, 0, 2) === '00') {
497 + // safely convert 00 to +
498 + $phoneNumber = substr_replace($phoneNumber, '+', 0, 2);
499 + }
500 +
501 + return $phoneNumber ?: null;
502 + }
503 +
504 + /**
505 + * Returns the booking alias phone number (masked phone), if any.
506 + *
507 + * @return ?string
508 + *
509 + * @since 1.18.16 (J) - 1.8.16 (WP)
510 + */
511 + public function getAliasPhoneNumber()
512 + {
513 + $phoneAlias = (string) $this->getProperty('phone_alias');
514 +
515 + return $phoneAlias ? $this->getPhoneNumber($phoneAlias) : null;
516 + }
517 +
518 + /**
519 + * Returns the booking or customer email address, if any.
520 + *
521 + * @return ?string
522 + *
523 + * @since 1.18.8 (J) - 1.8.8 (WP)
524 + */
525 + public function getEmailAddress()
526 + {
527 + // give higher priority to booking-level email address
528 + $emailAddr = $this->getProperty('custmail');
529 +
530 + if (!$emailAddr) {
531 + // fetch booking customer details
532 + $customer = $this->getCustomer();
533 +
534 + // check customer-level email address
535 + $emailAddr = ($customer['email'] ?? '') ?: $emailAddr;
536 + }
537 +
538 + // trim email address
539 + $emailAddr = (string) $emailAddr;
540 +
541 + return $emailAddr ?: null;
542 + }
543 +
544 + /**
545 + * Returns the booking OTA type-data information, if any.
546 + *
547 + * @return array Empty or associative array.
548 + *
549 + * @since 1.18.8 (J) - 1.8.8 (WP)
550 + */
551 + public function getOTATypeData()
552 + {
553 + $typeData = $this->getProperty('ota_type_data');
554 +
555 + if ($typeData && is_scalar($typeData)) {
556 + $typeData = (array) json_encode($typeData, true);
557 + }
558 +
559 + return $typeData ?: [];
560 + }
561 +
562 + /**
293 563 * Returns the previous booking data.
294 564 *
295 565 * @return array
296 566 */
@@ -299,8 +569,24 @@
299 569 return $this->previousBooking;
300 570 }
301 571
302 572 /**
573 + * Sets the previous booking data.
574 + *
575 + * @param array $previous Previous booking assoc data.
576 + *
577 + * @return self
578 + *
579 + * @since 1.18.11 (J) - 1.8.11 (WP)
580 + */
581 + public function setPrevious(array $previous)
582 + {
583 + $this->previousBooking = $previous;
584 +
585 + return $this;
586 + }
587 +
588 + /**
303 589 * Gets the current room index.
304 590 *
305 591 * @return int
306 592 */
@@ -321,20 +607,280 @@
321 607 $this->currentRoomIndex = $index;
322 608 }
323 609
324 610 /**
325 - * Returns the booking stay timestamps.
611 + * Gets the current room number (1-based index).
326 612 *
613 + * @return int
614 + *
615 + * @since 1.18.7 (J) - 1.8.7 (WP)
616 + */
617 + public function getCurrentRoomNumber()
618 + {
619 + return $this->currentRoomNumber;
620 + }
621 +
622 + /**
623 + * Sets the current room number (1-based index).
624 + *
625 + * @param int $number The current room number.
626 + *
627 + * @return void
628 + *
629 + * @since 1.18.7 (J) - 1.8.7 (WP)
630 + */
631 + public function setCurrentRoomNumber(int $number)
632 + {
633 + $this->currentRoomNumber = $number;
634 + }
635 +
636 + /**
637 + * Gets the room ID from the current room index set.
638 + *
639 + * @return int
640 + *
641 + * @since 1.18.7 (J) - 1.8.7 (WP)
642 + */
643 + public function getCurrentRoomID()
644 + {
645 + foreach ($this->getRooms() as $index => $bookingRoom) {
646 + if ($index == $this->currentRoomIndex) {
647 + return (int) $bookingRoom['idroom'];
648 + }
649 + }
650 +
651 + return (int) (($this->getRooms()[0]['idroom'] ?? 0) ?: 0);
652 + }
653 +
654 + /**
655 + * Returns the details for the given room ID or for all rooms set.
656 + *
657 + * @param ?int $listingId Optional listing ID to get.
658 + *
327 659 * @return array
660 + *
661 + * @since 1.18.7 (J) - 1.8.7 (WP)
328 662 */
329 - public function getStayTimestamps()
663 + public function getRoomDetails(?int $listingId = null)
330 664 {
331 - /**
332 - * @todo Do we need to do something different for split-stay bookings?
333 - * We are aware of the current room index when this method is called.
334 - */
665 + if ($listingId && !($this->roomDetails[$listingId] ?? [])) {
666 + // obtain the information for the requested listing ID
667 + $listingDetails = VikBooking::getRoomInfo($listingId, ['name', 'img', 'units', 'params'], true);
668 + if ($listingDetails) {
669 + // cache value
670 + $this->setRoomDetails($listingId, $listingDetails);
671 + }
672 + }
335 673
674 + if (!$listingId) {
675 + return $this->roomDetails;
676 + }
677 +
678 + return $this->roomDetails[$listingId] ?? [];
679 + }
680 +
681 + /**
682 + * Sets the details for the given room ID.
683 + *
684 + * @param int $listingId The listing ID to update.
685 + * @param array $data The details data to set.
686 + *
687 + * @return self
688 + *
689 + * @since 1.18.7 (J) - 1.8.7 (WP)
690 + */
691 + public function setRoomDetails(int $listingId, array $data)
692 + {
693 + $this->roomDetails[$listingId] = $data;
694 +
695 + return $this;
696 + }
697 +
698 + /**
699 + * Gets the booking customer details.
700 + *
701 + * @return array
702 + *
703 + * @since 1.18.7 (J) - 1.8.7 (WP)
704 + */
705 + public function getCustomer()
706 + {
707 + if ($this->customer === null) {
708 + // load and cache customer details
709 + $this->customer = VikBooking::getCPinInstance()->getCustomerFromBooking($this->getID());
710 + }
711 +
712 + return $this->customer;
713 + }
714 +
715 + /**
716 + * Sets the booking customer details.
717 + *
718 + * @param array $customer Raw customer details.
719 + *
720 + * @return self
721 + *
722 + * @since 1.18.7 (J) - 1.8.7 (WP)
723 + */
724 + public function setCustomer(array $customer)
725 + {
726 + $this->customer = $customer;
727 +
728 + return $this;
729 + }
730 +
731 + /**
732 + * Gets the booking customer data: nominative, logo, provenience name.
733 + *
734 + * @return array Numeric list of booking-customer data value strings.
735 + *
736 + * @since 1.18.7 (J) - 1.8.7 (WP)
737 + */
738 + public function getBookingCustomerData()
739 + {
740 + // starting values
741 + $customer_nominative = '';
742 + $booking_avatar_src = '';
743 + $booking_avatar_alt = '';
744 +
745 + if ($this->customer === null) {
746 + // attempt to load customer details first
747 + $this->getCustomer();
748 + }
749 +
750 + if (!empty($this->customer['first_name']) || !empty($this->customer['last_name'])) {
751 + // check if we need to display a profile picture or a channel logo
752 + if (!empty($this->customer['pic'])) {
753 + // customer profile picture
754 + $booking_avatar_src = strpos($this->customer['pic'], 'http') === 0 ? $this->customer['pic'] : VBO_SITE_URI . 'resources/uploads/' . $this->customer['pic'];
755 + $booking_avatar_alt = basename($booking_avatar_src);
756 + } elseif ($this->getProperty('idorderota') && $this->getProperty('channel')) {
757 + // channel logo
758 + $logo_helper = VikBooking::getVcmChannelsLogo($this->getProperty('channel'), $get_istance = true);
759 + if ($logo_helper !== false) {
760 + $booking_avatar_src = $logo_helper->getSmallLogoURL();
761 + $booking_avatar_alt = $logo_helper->provenience;
762 + }
763 + }
764 +
765 + if (!empty($booking_avatar_src)) {
766 + // make sure the alt attribute is not too long in case of broken images
767 + $booking_avatar_alt = !empty($booking_avatar_alt) && strlen($booking_avatar_alt) > 15 ? '...' . substr($booking_avatar_alt, -12) : $booking_avatar_alt;
768 + }
769 +
770 + // customer name
771 + $customer_fullname = trim($this->customer['first_name'] . ' ' . $this->customer['last_name']);
772 + if (strlen($customer_fullname) > 26) {
773 + if (function_exists('mb_substr')) {
774 + $customer_fullname = trim(mb_substr($customer_fullname, 0, 26, 'UTF-8')) . '..';
775 + } else {
776 + $customer_fullname = trim(substr($customer_fullname, 0, 26)) . '..';
777 + }
778 + }
779 + $customer_nominative = $customer_fullname;
780 + } else {
781 + // parse the customer data string
782 + $custdata_parts = explode("\n", (string) $this->getProperty('custdata'));
783 + $enoughinfo = false;
784 + if (count($custdata_parts) > 2 && strpos($custdata_parts[0], ':') !== false && strpos($custdata_parts[1], ':') !== false) {
785 + // get the first two fields
786 + $custvalues = array();
787 + foreach ($custdata_parts as $custdet) {
788 + if (strlen($custdet) < 1) {
789 + continue;
790 + }
791 + $custdet_parts = explode(':', $custdet);
792 + if (count($custdet_parts) >= 2) {
793 + unset($custdet_parts[0]);
794 + array_push($custvalues, trim(implode(':', $custdet_parts)));
795 + }
796 + if (count($custvalues) > 1) {
797 + break;
798 + }
799 + }
800 + if (count($custvalues) > 1) {
801 + $enoughinfo = true;
802 + $customer_nominative = trim(implode(' ', $custvalues));
803 + if (strlen($customer_nominative) > 26) {
804 + if (function_exists('mb_substr')) {
805 + $customer_nominative = trim(mb_substr($customer_nominative, 0, 26, 'UTF-8')) . '..';
806 + } else {
807 + $customer_nominative = trim(substr($customer_nominative, 0, 26)) . '..';
808 + }
809 + }
810 + if ($this->getProperty('idorderota') && $this->getProperty('channel')) {
811 + // add support for the channel logo for the imported OTA reservations with no customer record
812 + $logo_helper = VikBooking::getVcmChannelsLogo($this->getProperty('channel'), $get_istance = true);
813 + if ($logo_helper !== false) {
814 + $booking_avatar_src = $logo_helper->getSmallLogoURL();
815 + $booking_avatar_alt = $logo_helper->provenience;
816 + // make sure the alt attribute is not too long in case of broken images
817 + $booking_avatar_alt = !empty($booking_avatar_alt) && strlen($booking_avatar_alt) > 15 ? '...' . substr($booking_avatar_alt, -12) : $booking_avatar_alt;
818 + }
819 + }
820 + }
821 + }
822 + if (!$enoughinfo) {
823 + $customer_nominative = '#' . $this->getID();
824 + }
825 + }
826 +
336 827 return [
828 + $customer_nominative,
829 + $booking_avatar_src,
830 + $booking_avatar_alt,
831 + ];
832 + }
833 +
834 + /**
835 + * Returns the booking (or current room booking record) stay timestamps.
836 + *
837 + * @param bool $roomLevel True to return the stay timestamps at booking room level.
838 + *
839 + * @return array
840 + *
841 + * @since 1.18.7 (J) - 1.8.7 (WP) added argument $roomLevel.
842 + */
843 + public function getStayTimestamps(bool $roomLevel = false)
844 + {
845 + if ($roomLevel) {
846 + // current room index signature for the stay timestamps
847 + $roomSignature = '_stay_timestamps' . $this->getCurrentRoomIndex();
848 +
849 + if ($cachedTimestamps = $this->getProperty($roomSignature)) {
850 + // return the cached stay timestamps for the current room index
851 + return $cachedTimestamps;
852 + }
853 +
854 + // load room occupied records
855 + $room_stay_dates = VikBooking::getAvailabilityInstance(true)->loadSplitStayBusyRecords($this->getID());
856 +
857 + // default room-level stay timestamps
858 + $roomLevelCheckin = $this->getProperty('checkin');
859 + $roomLevelCheckout = $this->getProperty('checkout');
860 +
861 + // split-stay booking or booked rooms for modified stay nights should rely on occupied records
862 + if ($room_stay_dates[$this->getCurrentRoomIndex()] ?? []) {
863 + // set booking-room-level stay timestamps
864 + $roomLevelCheckin = $room_stay_dates[$this->getCurrentRoomIndex()]['checkin'];
865 + $roomLevelCheckout = $room_stay_dates[$this->getCurrentRoomIndex()]['checkout'];
866 + }
867 +
868 + // build room-level stay timestamps list
869 + $roomLevelStayList = [
870 + $roomLevelCheckin,
871 + $roomLevelCheckout,
872 + ];
873 +
874 + // cache stay timestamps for the current room index
875 + $this->setProperty($roomSignature, $roomLevelStayList);
876 +
877 + // return booking-room-level stay timestamps, if different than global stay dates
878 + return $roomLevelStayList;
879 + }
880 +
881 + // global reservation stay timestamps
882 + return [
337 883 $this->getProperty('checkin'),
338 884 $this->getProperty('checkout'),
339 885 ];
340 886 }
@@ -401,11 +947,15 @@
401 947
402 948 /**
403 949 * Attempts to detect changes between the current and previous bookings.
404 950 *
951 + * @param bool $roomLevel True to also detect alterations at room-level.
952 + *
405 953 * @return bool False if no changes were actually proved, true otherwise.
954 + *
955 + * @since 1.18.7 (J) - 1.8.7 (WP) added argument $roomLevel.
406 956 */
407 - public function detectAlterations()
957 + public function detectAlterations(bool $roomLevel = false)
408 958 {
409 959 if ($this->getProperty('checkin') != $this->getPreviousProperty('checkin')) {
410 960 return true;
411 961 }
@@ -440,8 +990,22 @@
440 990 if (!$current_room_ids || !$previous_room_ids || $current_room_ids != $previous_room_ids) {
441 991 return true;
442 992 }
443 993
994 + // attempt to also detect changes at room-level
995 + if ($roomLevel) {
996 + // check subunits
997 + $current_room_indexes = array_map('intval', array_column($this->getRooms(), 'roomindex'));
998 + $previous_room_indexes = array_map('intval', array_column((array) $this->getPreviousProperty('rooms_info', []), 'roomindex'));
999 + sort($current_room_indexes);
1000 + sort($previous_room_indexes);
1001 + // ensure room subunits information is available for current and previous booking
1002 + if ($current_room_indexes && $previous_room_indexes && $current_room_indexes != $previous_room_indexes) {
1003 + // room subunits alteration detected
1004 + return true;
1005 + }
1006 + }
1007 +
444 1008 // no significant changes to stay dates or listings could be proved
445 1009 return false;
446 1010 }
447 1011
@@ -515,6 +1079,241 @@
515 1079 $this->registry['_precheckin'] = $pre_checkin;
516 1080 }
517 1081
518 1082 return (bool) $pre_checkin;
1083 + }
1084 +
1085 + /**
1086 + * Tells if the booking comes from an OTA.
1087 + *
1088 + * @return bool
1089 + *
1090 + * @since 1.18.8 (J) - 1.8.8 (WP)
1091 + */
1092 + public function isFromOTA()
1093 + {
1094 + $otaId = $this->getProperty('idorderota', null);
1095 + $channel = $this->getProperty('channel', null);
1096 +
1097 + return !empty($otaId) && !empty($channel);
1098 + }
1099 +
1100 + /**
1101 + * Tells if the booking is fully paid as of now.
1102 + * Note that OTA payouts may still need to be received,
1103 + * or an outstanding payment may still be due in case of upselling events.
1104 + * Do NOT rely on this method alone to determine if payments are needed.
1105 + *
1106 + * @return bool Even if true, there could be pending payments.
1107 + *
1108 + * @see getOutstandingBalance()
1109 + *
1110 + * @since 1.18.8 (J) - 1.8.8 (WP)
1111 + */
1112 + public function isFullyPaid()
1113 + {
1114 + // get current totals
1115 + $bookingTotal = (float) $this->getProperty('total', 0);
1116 + $otaCompensation = (float) $this->getProperty('cmms', 0);
1117 + $amountPaid = (float) $this->getProperty('totpaid', 0);
1118 +
1119 + if ($this->isFromOTA()) {
1120 + return ($bookingTotal - $amountPaid - $otaCompensation) < 1;
1121 + }
1122 +
1123 + return $amountPaid >= $bookingTotal;
1124 + }
1125 +
1126 + /**
1127 + * Calculates the outstanding balance as of now, if any.
1128 + *
1129 + * @return ?float
1130 + *
1131 + * @since 1.18.8 (J) - 1.8.8 (WP)
1132 + */
1133 + public function getOutstandingBalance()
1134 + {
1135 + // get current totals
1136 + $bookingTotal = (float) $this->getProperty('total', 0);
1137 + $otaCompensation = (float) $this->getProperty('cmms', 0);
1138 + $amountPaid = (float) $this->getProperty('totpaid', 0);
1139 + $amountPayable = (float) $this->getProperty('payable', 0);
1140 + $paymentCount = abs((int) $this->getProperty('paymcount', 0));
1141 + $bookingDamDep = (float) $this->getProperty('tot_damage_dep', 0);
1142 +
1143 + // check if the amounts look as fully paid
1144 + $looksFullyPaid = $this->isFullyPaid();
1145 +
1146 + if ($looksFullyPaid && !$amountPayable) {
1147 + // no upselling events involved, nothing is due
1148 + return null;
1149 + }
1150 +
1151 + // obtain damage deposit details
1152 + $damage_deposit_payment = VBORoomHelper::getInstance()->getDamageDepositSplitPayment($this->getData(), $this->getRooms());
1153 +
1154 + // obtain previous damage deposit payments, if any
1155 + $prev_dd_payments = [];
1156 + if ($damage_deposit_payment['damagedep_gross'] ?? 0) {
1157 + $prev_dd_payments = VikBooking::getBookingHistoryInstance($this->getID())
1158 + ->getEventsWithData('PN', function($data) {
1159 + return (is_object($data) && !empty($data->damage_deposit));
1160 + });
1161 + if (!$prev_dd_payments) {
1162 + // check also the first payment event in case of OTA bookings
1163 + $prev_dd_payments = VikBooking::getBookingHistoryInstance($this->getID())
1164 + ->getEventsWithData('P0', function($data) {
1165 + return (is_object($data) && !empty($data->damage_deposit));
1166 + });
1167 + }
1168 + }
1169 +
1170 + // outstanding balance
1171 + $outstanding = $bookingTotal - $amountPaid;
1172 + if ($prev_dd_payments && $bookingDamDep) {
1173 + // deduct previously paid damage deposit
1174 + $outstanding -= $bookingDamDep;
1175 + }
1176 +
1177 + // tell if the booking looks payable
1178 + $isPayable = (($amountPaid > 0 && ($amountPaid + ($damage_deposit_payment['damagedep_gross'] ?? 0)) < $bookingTotal && $paymentCount) || $amountPayable > 0);
1179 +
1180 + // additional payment flags
1181 + $otaWillPay = false;
1182 + $payableLater = false;
1183 +
1184 + // determine booking pay-ability
1185 + if ($isPayable && $this->isConfirmed() && $this->isFromOTA() && $amountPayable > 0 && !$prev_dd_payments) {
1186 + if (($damage_deposit_payment['damagedep_gross'] ?? 0) == $amountPayable && ($damage_deposit_payment['payment_window']['pay_id'] ?? 0)) {
1187 + // upselling event must have added the damage deposit to an OTA booking, and this is the only outstanding amount that will be paid separately
1188 + $isPayable = false;
1189 + // check if the damage deposit is payable later
1190 + if (!($damage_deposit_payment['payment_window']['payable'] ?? 0) && ($damage_deposit_payment['payment_window']['payment_from_dt'] ?? null)) {
1191 + // will be payable on a date in the future
1192 + $payableLater = true;
1193 + }
1194 + }
1195 + }
1196 + if ($isPayable && $this->isFromOTA() && $otaCompensation && $looksFullyPaid) {
1197 + // the difference of the amount paid is equal to the OTA commissions amount
1198 + $isPayable = false;
1199 + if ($amountPayable > 0 && $bookingTotal > $amountPaid && round(($bookingTotal - $amountPaid), 0) == round($amountPayable, 0)) {
1200 + // there must have been an upselling event or a payment request with an amount equal to the OTA commissions
1201 + $isPayable = true;
1202 + }
1203 + }
1204 + if ($isPayable && $this->isFromOTA() && !$amountPayable) {
1205 + // access OTA payout events
1206 + $prev_ota_payments = VikBooking::getBookingHistoryInstance($this->getID())
1207 + ->getEventsWithData('PO', null, false);
1208 + if (!$prev_ota_payments) {
1209 + // the OTA will pay the remaining balance
1210 + $isPayable = false;
1211 + $otaWillPay = true;
1212 + }
1213 + }
1214 +
1215 + if ($outstanding && !$isPayable && $payableLater) {
1216 + // deduct the damage deposit amount that will be payable in a future date
1217 + $outstanding -= $bookingDamDep;
1218 + }
1219 +
1220 + if ($outstanding && !$isPayable && $otaWillPay) {
1221 + // deduct OTA commissions that has not been paid yet
1222 + $outstanding -= $otaCompensation;
1223 + }
1224 +
1225 + return $outstanding > 0 ? $outstanding : null;
1226 + }
1227 +
1228 + /**
1229 + * Returns a unique list of booked listing names.
1230 + *
1231 + * @return array Linear array of strings, if any.
1232 + *
1233 + * @since 1.18.8 (J) - 1.8.8 (WP)
1234 + */
1235 + public function getListingNames()
1236 + {
1237 + $listingIds = $this->getBookedListingIds();
1238 + $roomNames = VikBooking::getAvailabilityInstance(true)->loadRooms($listingIds);
1239 +
1240 + return array_map(function($id) use ($roomNames) {
1241 + return $roomNames[$id]['name'] ?? $id;
1242 + }, $listingIds);
1243 + }
1244 +
1245 + /**
1246 + * Returns a unique list of booked listing addresses.
1247 + *
1248 + * @return array Linear array of strings, if any.
1249 + *
1250 + * @since 1.18.8 (J) - 1.8.8 (WP)
1251 + */
1252 + public function getListingAddresses()
1253 + {
1254 + if (!class_exists('VCMOtaListing')) {
1255 + // prevent errors when the CM is not installed
1256 + return [];
1257 + }
1258 +
1259 + $addresses = [];
1260 +
1261 + foreach ($this->getBookedListingIds() as $listingId) {
1262 + $addresses[] = VCMOtaListing::getInstance()->getLocation($listingId)['address'] ?? null;
1263 + }
1264 +
1265 + return array_values(array_unique($addresses));
1266 + }
1267 +
1268 + /**
1269 + * Returns the booking routed URI, either in its regular or shorten forms.
1270 + *
1271 + * @return string
1272 + *
1273 + * @since 1.18.13 (J) - 1.8.13 (WP)
1274 + */
1275 + public function getBookingLink(bool $shorten = false)
1276 + {
1277 + // construct booking link
1278 + $useSid = !$this->getProperty('sid') && $this->getProperty('idorderota') ? $this->getProperty('idorderota') : $this->getProperty('sid');
1279 + $bestItemid = VikBooking::findProperItemIdType(['booking'], ($this->getProperty('lang') ?: null));
1280 + $langSuffix = $bestItemid && $this->getProperty('lang') ? '&lang=' . $this->getProperty('lang') : '';
1281 +
1282 + $bookingLink = VikBooking::externalroute("index.php?option=com_vikbooking&view=booking&sid=" . $useSid . "&ts=" . $this->getProperty('ts') . $langSuffix, false, ($bestItemid ?: null));
1283 +
1284 + if (!$shorten) {
1285 + return $bookingLink;
1286 + }
1287 +
1288 + // access the model for shortening URLs
1289 + $model = VBOModelShortenurl::getInstance($onlyRouted = false)->setBooking($this->getData());
1290 +
1291 + return $model->getShortUrl($bookingLink);
1292 + }
1293 +
1294 + /**
1295 + * Returns the booking pre-check-in routed URI, either in its regular or shorten forms.
1296 + *
1297 + * @return string
1298 + *
1299 + * @since 1.18.13 (J) - 1.8.13 (WP)
1300 + */
1301 + public function getPrecheckinLink(bool $shorten = false)
1302 + {
1303 + // obtain booking link
1304 + $useSid = !$this->getProperty('sid') && $this->getProperty('idorderota') ? $this->getProperty('idorderota') : $this->getProperty('sid');
1305 + $bestItemid = VikBooking::findProperItemIdType(['booking'], ($this->getProperty('lang') ?: null));
1306 + $langSuffix = $bestItemid && $this->getProperty('lang') ? '&lang=' . $this->getProperty('lang') : '';
1307 +
1308 + $precheckinLink = VikBooking::externalroute("index.php?option=com_vikbooking&view=precheckin&sid=" . $useSid . "&ts=" . $this->getProperty('ts') . $langSuffix, false, ($bestItemid ?: null));
1309 +
1310 + if (!$shorten) {
1311 + return $precheckinLink;
1312 + }
1313 +
1314 + // access the model for shortening URLs
1315 + $model = VBOModelShortenurl::getInstance($onlyRouted = false)->setBooking($this->getData());
1316 +
1317 + return $model->getShortUrl($precheckinLink);
519 1318 }
520 1319 }