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 / admin / helpers / src / booking / registry.php

registry.php in VikBooking Hotel Booking Engine & PMS 1.8.16, at admin/helpers/src/booking/registry.php

1,320 lines 41.4 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 core
5 * @author E4J s.r.l.
6 * @copyright Copyright (C) 2025 E4J s.r.l. All Rights Reserved.
7 * @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
8 * @link https://vikwp.com
9 */
10
11 // No direct access
12 defined('ABSPATH') or die('No script kiddies please!');
13
14 /**
15 * Booking registry implementation.
16 *
17 * @since 1.18.4 (J) - 1.8.4 (WP)
18 */
19 class VBOBookingRegistry
20 {
21 /**
22 * @var array
23 */
24 protected $registry = [];
25
26 /**
27 * @var array
28 */
29 protected $bookingRooms = [];
30
31 /**
32 * @var array
33 */
34 protected $previousBooking = [];
35
36 /**
37 * @var int
38 */
39 protected $currentRoomIndex = 0;
40
41 /**
42 * @var int
43 */
44 protected $currentRoomNumber = 0;
45
46 /**
47 * @var array
48 */
49 protected $dacData = [];
50
51 /**
52 * @var array
53 */
54 protected $roomDetails = [];
55
56 /**
57 * @var ?array
58 */
59 protected $customer = null;
60
61 /**
62 * Proxy to construct the object.
63 *
64 * @param array $options Associative list of booking information to bind.
65 * @param array $rooms Associative list of booking rooms to bind.
66 * @param array $previous Associative list of previous booking information to bind.
67 *
68 * @return VBOBookingRegistry
69 */
70 public static function getInstance(array $options, array $rooms = [], array $previous = [])
71 {
72 return new static($options, $rooms, $previous);
73 }
74
75 /**
76 * Class constructor.
77 *
78 * @param array $options Associative list of booking information to bind.
79 * @param array $rooms Associative list of booking rooms to bind.
80 * @param array $previous Associative list of previous booking information to bind.
81 *
82 * @throws Exception
83 */
84 public function __construct(array $options, array $rooms = [], array $previous = [])
85 {
86 // ensure we have enough booking details
87 if ($options === ['id' => $options['id'] ?? 0]) {
88 // load full booking details
89 $options = VikBooking::getBookingInfoFromID($options['id']);
90 }
91
92 if (empty($options['id'])) {
93 throw new Exception('Missing booking ID.', 500);
94 }
95
96 // bind booking options to internal registry
97 $this->bind($options);
98
99 if (!$rooms) {
100 // load booking rooms
101 $rooms = VikBooking::loadOrdersRoomsData($this->getID());
102 }
103
104 if (!$rooms) {
105 throw new Exception('No booking rooms found.', 404);
106 }
107
108 // bind booking rooms
109 $this->bookingRooms = $rooms;
110
111 // bind previous booking information in case of alteration
112 $this->previousBooking = $previous;
113 }
114
115 /**
116 * Binds the given options onto the internal booking registry.
117 *
118 * @param array $options The booking options to bind.
119 *
120 * @return void
121 */
122 public function bind(array $options)
123 {
124 $this->registry = array_merge($this->registry, $options);
125 }
126
127 /**
128 * Returns the current booking ID.
129 *
130 * @return int
131 */
132 public function getID()
133 {
134 return (int) $this->getProperty('id', 0);
135 }
136
137 /**
138 * Returns the number of nights of stay for the current booking ID.
139 *
140 * @param bool $roomLevel True to calculate the nights at room-level.
141 *
142 * @return int
143 */
144 public function getTotalNights(bool $roomLevel = false)
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
155 return (int) $this->getProperty('days', 1);
156 }
157
158 /**
159 * Tells whether the booking is actually a closure reservation.
160 *
161 * @return bool
162 */
163 public function isClosure()
164 {
165 return (bool) $this->getProperty('closure', 0);
166 }
167
168 /**
169 * Tells whether the booking status is confirmed.
170 *
171 * @return bool
172 */
173 public function isConfirmed()
174 {
175 return $this->getProperty('status', '') == 'confirmed';
176 }
177
178 /**
179 * Tells whether the booking status is pending (stand-by).
180 *
181 * @return bool
182 */
183 public function isPending()
184 {
185 return $this->getProperty('status', '') == 'standby';
186 }
187
188 /**
189 * Tells whether the booking status is cancelled.
190 *
191 * @return bool
192 */
193 public function isCancelled()
194 {
195 return $this->getProperty('status', '') == 'cancelled';
196 }
197
198 /**
199 * Tells whether the booking is flagged as overbooking.
200 *
201 * @return bool
202 */
203 public function isOverbooking()
204 {
205 return $this->getProperty('type', '') == 'overbooking';
206 }
207
208 /**
209 * Returns the requested registry property name.
210 *
211 * @param string $name The registry property to fetch.
212 * @param mixed $default The default value to return.
213 *
214 * @return mixed
215 */
216 public function getProperty(string $name, $default = null)
217 {
218 return $this->registry[$name] ?? $default;
219 }
220
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 /**
237 * Returns the requested previous booking property name.
238 *
239 * @param string $name The previous booking property to fetch.
240 * @param mixed $default The default value to return.
241 *
242 * @return mixed
243 */
244 public function getPreviousProperty(string $name, $default = null)
245 {
246 return $this->previousBooking[$name] ?? $default;
247 }
248
249 /**
250 * Returns the booking data.
251 *
252 * @return array
253 */
254 public function getData()
255 {
256 return $this->registry;
257 }
258
259 /**
260 * Returns the whole or provider-alias DAC data-registry.
261 *
262 * @param ?string $providerAlias Optional DAC provider alias identifier.
263 *
264 * @return array
265 */
266 public function getDACData(?string $providerAlias = null)
267 {
268 if ($providerAlias) {
269 return $this->dacData[$providerAlias] ?? [];
270 }
271
272 return $this->dacData;
273 }
274
275 /**
276 * Returns the requested DAC data-registry property name.
277 *
278 * @param string $providerAlias The DAC provider alias identifier.
279 * @param string $name The DAC registry property to fetch.
280 * @param mixed $default The default value to return.
281 *
282 * @return mixed
283 */
284 public function getDACProperty(string $providerAlias, string $name, $default = null)
285 {
286 return $this->dacData[$providerAlias][$name] ?? $default;
287 }
288
289 /**
290 * Sets a value within the DAC data-registry for a given property name.
291 *
292 * @param string $providerAlias The DAC provider alias identifier.
293 * @param string $name The DAC registry property to set.
294 * @param mixed $value The property value to set.
295 *
296 * @return self
297 */
298 public function setDACProperty(string $providerAlias, string $name, $value)
299 {
300 if (!isset($this->dacData[$providerAlias])) {
301 // start a new container
302 $this->dacData[$providerAlias] = [];
303 }
304
305 // set the value for the requested property name
306 $this->dacData[$providerAlias][$name] = $value;
307
308 return $this;
309 }
310
311 /**
312 * Returns the booking rooms data.
313 *
314 * @return array
315 */
316 public function getRooms()
317 {
318 return $this->bookingRooms;
319 }
320
321 /**
322 * Returns the booked listing IDs.
323 *
324 * @param bool $unique True to only get a unique list.
325 *
326 * @return array List of booked room ID integers, unique or not.
327 */
328 public function getBookedListingIds(bool $unique = true)
329 {
330 if (!$unique) {
331 return array_map('intval', array_column($this->bookingRooms, 'idroom'));
332 }
333
334 return array_values(array_unique(array_map('intval', array_column($this->bookingRooms, 'idroom'))));
335 }
336
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 /**
563 * Returns the previous booking data.
564 *
565 * @return array
566 */
567 public function getPrevious()
568 {
569 return $this->previousBooking;
570 }
571
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 /**
589 * Gets the current room index.
590 *
591 * @return int
592 */
593 public function getCurrentRoomIndex()
594 {
595 return $this->currentRoomIndex;
596 }
597
598 /**
599 * Sets the current room index.
600 *
601 * @param int $index The current room index.
602 *
603 * @return void
604 */
605 public function setCurrentRoomIndex(int $index)
606 {
607 $this->currentRoomIndex = $index;
608 }
609
610 /**
611 * Gets the current room number (1-based index).
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 *
659 * @return array
660 *
661 * @since 1.18.7 (J) - 1.8.7 (WP)
662 */
663 public function getRoomDetails(?int $listingId = null)
664 {
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 }
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
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 [
883 $this->getProperty('checkin'),
884 $this->getProperty('checkout'),
885 ];
886 }
887
888 /**
889 * Builds and returns the iterable date period interval for the nights of stay.
890 *
891 * @param string $duration The interval specification used for DateInterval::__construct().
892 * @param int $from_ts Optional from date period timestamp.
893 * @param int $to_ts Optional to date period timestamp.
894 *
895 * @return DatePeriod
896 */
897 public function buildStayPeriodInterval(string $duration = 'P1D', int $from_ts = 0, int $to_ts = 0)
898 {
899 if (empty($from_ts)) {
900 $from_ts = $this->getProperty('checkin');
901 }
902
903 if (empty($to_ts)) {
904 $to_ts = $this->getProperty('checkout');
905 }
906
907 // local timezone
908 $tz = new DateTimezone(date_default_timezone_get());
909
910 // get date bounds
911 $from_bound = new DateTime(date('Y-m-d H:i:s', $from_ts), $tz);
912 $to_bound = new DateTime(date('Y-m-d H:i:s', $to_ts), $tz);
913
914 // build iterable dates interval (period)
915 $date_range = new DatePeriod(
916 // start date included by default in the result set
917 $from_bound,
918 // interval between recurrences within the period
919 new DateInterval($duration),
920 // end date (check-out) excluded by default from the result set
921 $to_bound
922 );
923
924 return $date_range;
925 }
926
927 /**
928 * Returns the iterable date period range of dates for the nights of stay.
929 *
930 * @return DatePeriod
931 */
932 public function getStayPeriod()
933 {
934 if (($this->registry['stay_date_period'] ?? null) instanceof DatePeriod) {
935 // return cached value
936 return $this->registry['stay_date_period'];
937 }
938
939 // build iterable dates interval (period)
940 $date_range = $this->buildStayPeriodInterval('P1D');
941
942 // cache value
943 $this->bind(['stay_date_period' => $date_range]);
944
945 return $date_range;
946 }
947
948 /**
949 * Attempts to detect changes between the current and previous bookings.
950 *
951 * @param bool $roomLevel True to also detect alterations at room-level.
952 *
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.
956 */
957 public function detectAlterations(bool $roomLevel = false)
958 {
959 if ($this->getProperty('checkin') != $this->getPreviousProperty('checkin')) {
960 return true;
961 }
962
963 if ($this->getProperty('checkout') != $this->getPreviousProperty('checkout')) {
964 return true;
965 }
966
967 if ($this->getProperty('days') != $this->getPreviousProperty('days')) {
968 return true;
969 }
970
971 if ($this->getProperty('roomsnum') != $this->getPreviousProperty('roomsnum')) {
972 return true;
973 }
974
975 // get the rooms booked with the current reservation
976 $current_room_ids = array_column($this->getRooms(), 'idroom');
977 if (!$current_room_ids && is_array($this->getProperty('rooms_info'))) {
978 $current_room_ids = array_column($this->getProperty('rooms_info'), 'idroom');
979 }
980
981 // get the rooms booked with the previous reservation
982 $previous_room_ids = array_column((array) $this->getPreviousProperty('rooms_info', []), 'idroom');
983
984 // map and sort both room lists
985 $current_room_ids = array_map('intval', $current_room_ids);
986 $previous_room_ids = array_map('intval', $previous_room_ids);
987 sort($current_room_ids);
988 sort($previous_room_ids);
989
990 if (!$current_room_ids || !$previous_room_ids || $current_room_ids != $previous_room_ids) {
991 return true;
992 }
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
1008 // no significant changes to stay dates or listings could be proved
1009 return false;
1010 }
1011
1012 /**
1013 * Tells if the current date and time is between the stay dates.
1014 *
1015 * @return bool
1016 */
1017 public function isStaying()
1018 {
1019 $now = time();
1020
1021 return $this->getProperty('checkin', 0) <= $now && $now <= $this->getProperty('checkout', 0);
1022 }
1023
1024 /**
1025 * Tells if the booking arrival date is today.
1026 *
1027 * @return bool
1028 */
1029 public function isArrivingToday()
1030 {
1031 return date('Y-m-d', $this->getProperty('checkin', 0)) === date('Y-m-d');
1032 }
1033
1034 /**
1035 * Tells if the booking departure date is today.
1036 *
1037 * @return bool
1038 */
1039 public function isDepartingToday()
1040 {
1041 return date('Y-m-d', $this->getProperty('checkout', 0)) === date('Y-m-d');
1042 }
1043
1044 /**
1045 * Tells if the booking arrival date and time is in the future.
1046 *
1047 * @return bool
1048 */
1049 public function isFuture()
1050 {
1051 return $this->getProperty('checkin', 0) > time();
1052 }
1053
1054 /**
1055 * Tells if the booking departure date and time is in the past.
1056 *
1057 * @return bool
1058 */
1059 public function isPast()
1060 {
1061 return $this->getProperty('checkout', 0) < time();
1062 }
1063
1064 /**
1065 * Tells if the booking went through the pre-checkin process.
1066 *
1067 * @return bool
1068 *
1069 * @since 1.18.6 (J) - 1.8.6 (WP)
1070 */
1071 public function hasPreCheckedIn()
1072 {
1073 // check registry protected value
1074 $pre_checkin = $this->getProperty('_precheckin');
1075
1076 if (!is_int($pre_checkin)) {
1077 // fetch the actual status and cache it internally
1078 $pre_checkin = (int) boolval(VikBooking::getBookingHistoryInstance($this->getID())->hasEvent('PC'));
1079 $this->registry['_precheckin'] = $pre_checkin;
1080 }
1081
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);
1318 }
1319 }
1320