PluginProbe
VikBooking Hotel Booking Engine & PMS / trunk
VikBooking Hotel Booking Engine & PMS vtrunk
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 1.7.3 All 36 releases
vikbooking / admin / helpers / src / booking / registry.php

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

1,303 lines 40.8 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 * @return ?string
471 *
472 * @since 1.18.8 (J) - 1.8.8 (WP)
473 */
474 public function getPhoneNumber()
475 {
476 // give higher priority to booking-level phone number
477 $phoneNumber = $this->getProperty('phone');
478
479 if (!$phoneNumber) {
480 // fetch booking customer details
481 $customer = $this->getCustomer();
482
483 // check customer-level phone number
484 $phoneNumber = ($customer['phone'] ?? '') ?: $phoneNumber;
485 }
486
487 // trim phone number
488 $phoneNumber = (string) $phoneNumber;
489
490 // sanitize number by keeping the + char only if at the beginning of the string
491 $phoneNumber = preg_replace('/(?!^)\+|[^0-9+]/', '', $phoneNumber);
492
493 if (substr($phoneNumber, 0, 2) === '00') {
494 // safely convert 00 to +
495 $phoneNumber = substr_replace($phoneNumber, '+', 0, 2);
496 }
497
498 return $phoneNumber ?: null;
499 }
500
501 /**
502 * Returns the booking or customer email address, if any.
503 *
504 * @return ?string
505 *
506 * @since 1.18.8 (J) - 1.8.8 (WP)
507 */
508 public function getEmailAddress()
509 {
510 // give higher priority to booking-level email address
511 $emailAddr = $this->getProperty('custmail');
512
513 if (!$emailAddr) {
514 // fetch booking customer details
515 $customer = $this->getCustomer();
516
517 // check customer-level email address
518 $emailAddr = ($customer['email'] ?? '') ?: $emailAddr;
519 }
520
521 // trim email address
522 $emailAddr = (string) $emailAddr;
523
524 return $emailAddr ?: null;
525 }
526
527 /**
528 * Returns the booking OTA type-data information, if any.
529 *
530 * @return array Empty or associative array.
531 *
532 * @since 1.18.8 (J) - 1.8.8 (WP)
533 */
534 public function getOTATypeData()
535 {
536 $typeData = $this->getProperty('ota_type_data');
537
538 if ($typeData && is_scalar($typeData)) {
539 $typeData = (array) json_encode($typeData, true);
540 }
541
542 return $typeData ?: [];
543 }
544
545 /**
546 * Returns the previous booking data.
547 *
548 * @return array
549 */
550 public function getPrevious()
551 {
552 return $this->previousBooking;
553 }
554
555 /**
556 * Sets the previous booking data.
557 *
558 * @param array $previous Previous booking assoc data.
559 *
560 * @return self
561 *
562 * @since 1.18.11 (J) - 1.8.11 (WP)
563 */
564 public function setPrevious(array $previous)
565 {
566 $this->previousBooking = $previous;
567
568 return $this;
569 }
570
571 /**
572 * Gets the current room index.
573 *
574 * @return int
575 */
576 public function getCurrentRoomIndex()
577 {
578 return $this->currentRoomIndex;
579 }
580
581 /**
582 * Sets the current room index.
583 *
584 * @param int $index The current room index.
585 *
586 * @return void
587 */
588 public function setCurrentRoomIndex(int $index)
589 {
590 $this->currentRoomIndex = $index;
591 }
592
593 /**
594 * Gets the current room number (1-based index).
595 *
596 * @return int
597 *
598 * @since 1.18.7 (J) - 1.8.7 (WP)
599 */
600 public function getCurrentRoomNumber()
601 {
602 return $this->currentRoomNumber;
603 }
604
605 /**
606 * Sets the current room number (1-based index).
607 *
608 * @param int $number The current room number.
609 *
610 * @return void
611 *
612 * @since 1.18.7 (J) - 1.8.7 (WP)
613 */
614 public function setCurrentRoomNumber(int $number)
615 {
616 $this->currentRoomNumber = $number;
617 }
618
619 /**
620 * Gets the room ID from the current room index set.
621 *
622 * @return int
623 *
624 * @since 1.18.7 (J) - 1.8.7 (WP)
625 */
626 public function getCurrentRoomID()
627 {
628 foreach ($this->getRooms() as $index => $bookingRoom) {
629 if ($index == $this->currentRoomIndex) {
630 return (int) $bookingRoom['idroom'];
631 }
632 }
633
634 return (int) (($this->getRooms()[0]['idroom'] ?? 0) ?: 0);
635 }
636
637 /**
638 * Returns the details for the given room ID or for all rooms set.
639 *
640 * @param ?int $listingId Optional listing ID to get.
641 *
642 * @return array
643 *
644 * @since 1.18.7 (J) - 1.8.7 (WP)
645 */
646 public function getRoomDetails(?int $listingId = null)
647 {
648 if ($listingId && !($this->roomDetails[$listingId] ?? [])) {
649 // obtain the information for the requested listing ID
650 $listingDetails = VikBooking::getRoomInfo($listingId, ['name', 'img', 'units', 'params'], true);
651 if ($listingDetails) {
652 // cache value
653 $this->setRoomDetails($listingId, $listingDetails);
654 }
655 }
656
657 if (!$listingId) {
658 return $this->roomDetails;
659 }
660
661 return $this->roomDetails[$listingId] ?? [];
662 }
663
664 /**
665 * Sets the details for the given room ID.
666 *
667 * @param int $listingId The listing ID to update.
668 * @param array $data The details data to set.
669 *
670 * @return self
671 *
672 * @since 1.18.7 (J) - 1.8.7 (WP)
673 */
674 public function setRoomDetails(int $listingId, array $data)
675 {
676 $this->roomDetails[$listingId] = $data;
677
678 return $this;
679 }
680
681 /**
682 * Gets the booking customer details.
683 *
684 * @return array
685 *
686 * @since 1.18.7 (J) - 1.8.7 (WP)
687 */
688 public function getCustomer()
689 {
690 if ($this->customer === null) {
691 // load and cache customer details
692 $this->customer = VikBooking::getCPinInstance()->getCustomerFromBooking($this->getID());
693 }
694
695 return $this->customer;
696 }
697
698 /**
699 * Sets the booking customer details.
700 *
701 * @param array $customer Raw customer details.
702 *
703 * @return self
704 *
705 * @since 1.18.7 (J) - 1.8.7 (WP)
706 */
707 public function setCustomer(array $customer)
708 {
709 $this->customer = $customer;
710
711 return $this;
712 }
713
714 /**
715 * Gets the booking customer data: nominative, logo, provenience name.
716 *
717 * @return array Numeric list of booking-customer data value strings.
718 *
719 * @since 1.18.7 (J) - 1.8.7 (WP)
720 */
721 public function getBookingCustomerData()
722 {
723 // starting values
724 $customer_nominative = '';
725 $booking_avatar_src = '';
726 $booking_avatar_alt = '';
727
728 if ($this->customer === null) {
729 // attempt to load customer details first
730 $this->getCustomer();
731 }
732
733 if (!empty($this->customer['first_name']) || !empty($this->customer['last_name'])) {
734 // check if we need to display a profile picture or a channel logo
735 if (!empty($this->customer['pic'])) {
736 // customer profile picture
737 $booking_avatar_src = strpos($this->customer['pic'], 'http') === 0 ? $this->customer['pic'] : VBO_SITE_URI . 'resources/uploads/' . $this->customer['pic'];
738 $booking_avatar_alt = basename($booking_avatar_src);
739 } elseif ($this->getProperty('idorderota') && $this->getProperty('channel')) {
740 // channel logo
741 $logo_helper = VikBooking::getVcmChannelsLogo($this->getProperty('channel'), $get_istance = true);
742 if ($logo_helper !== false) {
743 $booking_avatar_src = $logo_helper->getSmallLogoURL();
744 $booking_avatar_alt = $logo_helper->provenience;
745 }
746 }
747
748 if (!empty($booking_avatar_src)) {
749 // make sure the alt attribute is not too long in case of broken images
750 $booking_avatar_alt = !empty($booking_avatar_alt) && strlen($booking_avatar_alt) > 15 ? '...' . substr($booking_avatar_alt, -12) : $booking_avatar_alt;
751 }
752
753 // customer name
754 $customer_fullname = trim($this->customer['first_name'] . ' ' . $this->customer['last_name']);
755 if (strlen($customer_fullname) > 26) {
756 if (function_exists('mb_substr')) {
757 $customer_fullname = trim(mb_substr($customer_fullname, 0, 26, 'UTF-8')) . '..';
758 } else {
759 $customer_fullname = trim(substr($customer_fullname, 0, 26)) . '..';
760 }
761 }
762 $customer_nominative = $customer_fullname;
763 } else {
764 // parse the customer data string
765 $custdata_parts = explode("\n", (string) $this->getProperty('custdata'));
766 $enoughinfo = false;
767 if (count($custdata_parts) > 2 && strpos($custdata_parts[0], ':') !== false && strpos($custdata_parts[1], ':') !== false) {
768 // get the first two fields
769 $custvalues = array();
770 foreach ($custdata_parts as $custdet) {
771 if (strlen($custdet) < 1) {
772 continue;
773 }
774 $custdet_parts = explode(':', $custdet);
775 if (count($custdet_parts) >= 2) {
776 unset($custdet_parts[0]);
777 array_push($custvalues, trim(implode(':', $custdet_parts)));
778 }
779 if (count($custvalues) > 1) {
780 break;
781 }
782 }
783 if (count($custvalues) > 1) {
784 $enoughinfo = true;
785 $customer_nominative = trim(implode(' ', $custvalues));
786 if (strlen($customer_nominative) > 26) {
787 if (function_exists('mb_substr')) {
788 $customer_nominative = trim(mb_substr($customer_nominative, 0, 26, 'UTF-8')) . '..';
789 } else {
790 $customer_nominative = trim(substr($customer_nominative, 0, 26)) . '..';
791 }
792 }
793 if ($this->getProperty('idorderota') && $this->getProperty('channel')) {
794 // add support for the channel logo for the imported OTA reservations with no customer record
795 $logo_helper = VikBooking::getVcmChannelsLogo($this->getProperty('channel'), $get_istance = true);
796 if ($logo_helper !== false) {
797 $booking_avatar_src = $logo_helper->getSmallLogoURL();
798 $booking_avatar_alt = $logo_helper->provenience;
799 // make sure the alt attribute is not too long in case of broken images
800 $booking_avatar_alt = !empty($booking_avatar_alt) && strlen($booking_avatar_alt) > 15 ? '...' . substr($booking_avatar_alt, -12) : $booking_avatar_alt;
801 }
802 }
803 }
804 }
805 if (!$enoughinfo) {
806 $customer_nominative = '#' . $this->getID();
807 }
808 }
809
810 return [
811 $customer_nominative,
812 $booking_avatar_src,
813 $booking_avatar_alt,
814 ];
815 }
816
817 /**
818 * Returns the booking (or current room booking record) stay timestamps.
819 *
820 * @param bool $roomLevel True to return the stay timestamps at booking room level.
821 *
822 * @return array
823 *
824 * @since 1.18.7 (J) - 1.8.7 (WP) added argument $roomLevel.
825 */
826 public function getStayTimestamps(bool $roomLevel = false)
827 {
828 if ($roomLevel) {
829 // current room index signature for the stay timestamps
830 $roomSignature = '_stay_timestamps' . $this->getCurrentRoomIndex();
831
832 if ($cachedTimestamps = $this->getProperty($roomSignature)) {
833 // return the cached stay timestamps for the current room index
834 return $cachedTimestamps;
835 }
836
837 // load room occupied records
838 $room_stay_dates = VikBooking::getAvailabilityInstance(true)->loadSplitStayBusyRecords($this->getID());
839
840 // default room-level stay timestamps
841 $roomLevelCheckin = $this->getProperty('checkin');
842 $roomLevelCheckout = $this->getProperty('checkout');
843
844 // split-stay booking or booked rooms for modified stay nights should rely on occupied records
845 if ($room_stay_dates[$this->getCurrentRoomIndex()] ?? []) {
846 // set booking-room-level stay timestamps
847 $roomLevelCheckin = $room_stay_dates[$this->getCurrentRoomIndex()]['checkin'];
848 $roomLevelCheckout = $room_stay_dates[$this->getCurrentRoomIndex()]['checkout'];
849 }
850
851 // build room-level stay timestamps list
852 $roomLevelStayList = [
853 $roomLevelCheckin,
854 $roomLevelCheckout,
855 ];
856
857 // cache stay timestamps for the current room index
858 $this->setProperty($roomSignature, $roomLevelStayList);
859
860 // return booking-room-level stay timestamps, if different than global stay dates
861 return $roomLevelStayList;
862 }
863
864 // global reservation stay timestamps
865 return [
866 $this->getProperty('checkin'),
867 $this->getProperty('checkout'),
868 ];
869 }
870
871 /**
872 * Builds and returns the iterable date period interval for the nights of stay.
873 *
874 * @param string $duration The interval specification used for DateInterval::__construct().
875 * @param int $from_ts Optional from date period timestamp.
876 * @param int $to_ts Optional to date period timestamp.
877 *
878 * @return DatePeriod
879 */
880 public function buildStayPeriodInterval(string $duration = 'P1D', int $from_ts = 0, int $to_ts = 0)
881 {
882 if (empty($from_ts)) {
883 $from_ts = $this->getProperty('checkin');
884 }
885
886 if (empty($to_ts)) {
887 $to_ts = $this->getProperty('checkout');
888 }
889
890 // local timezone
891 $tz = new DateTimezone(date_default_timezone_get());
892
893 // get date bounds
894 $from_bound = new DateTime(date('Y-m-d H:i:s', $from_ts), $tz);
895 $to_bound = new DateTime(date('Y-m-d H:i:s', $to_ts), $tz);
896
897 // build iterable dates interval (period)
898 $date_range = new DatePeriod(
899 // start date included by default in the result set
900 $from_bound,
901 // interval between recurrences within the period
902 new DateInterval($duration),
903 // end date (check-out) excluded by default from the result set
904 $to_bound
905 );
906
907 return $date_range;
908 }
909
910 /**
911 * Returns the iterable date period range of dates for the nights of stay.
912 *
913 * @return DatePeriod
914 */
915 public function getStayPeriod()
916 {
917 if (($this->registry['stay_date_period'] ?? null) instanceof DatePeriod) {
918 // return cached value
919 return $this->registry['stay_date_period'];
920 }
921
922 // build iterable dates interval (period)
923 $date_range = $this->buildStayPeriodInterval('P1D');
924
925 // cache value
926 $this->bind(['stay_date_period' => $date_range]);
927
928 return $date_range;
929 }
930
931 /**
932 * Attempts to detect changes between the current and previous bookings.
933 *
934 * @param bool $roomLevel True to also detect alterations at room-level.
935 *
936 * @return bool False if no changes were actually proved, true otherwise.
937 *
938 * @since 1.18.7 (J) - 1.8.7 (WP) added argument $roomLevel.
939 */
940 public function detectAlterations(bool $roomLevel = false)
941 {
942 if ($this->getProperty('checkin') != $this->getPreviousProperty('checkin')) {
943 return true;
944 }
945
946 if ($this->getProperty('checkout') != $this->getPreviousProperty('checkout')) {
947 return true;
948 }
949
950 if ($this->getProperty('days') != $this->getPreviousProperty('days')) {
951 return true;
952 }
953
954 if ($this->getProperty('roomsnum') != $this->getPreviousProperty('roomsnum')) {
955 return true;
956 }
957
958 // get the rooms booked with the current reservation
959 $current_room_ids = array_column($this->getRooms(), 'idroom');
960 if (!$current_room_ids && is_array($this->getProperty('rooms_info'))) {
961 $current_room_ids = array_column($this->getProperty('rooms_info'), 'idroom');
962 }
963
964 // get the rooms booked with the previous reservation
965 $previous_room_ids = array_column((array) $this->getPreviousProperty('rooms_info', []), 'idroom');
966
967 // map and sort both room lists
968 $current_room_ids = array_map('intval', $current_room_ids);
969 $previous_room_ids = array_map('intval', $previous_room_ids);
970 sort($current_room_ids);
971 sort($previous_room_ids);
972
973 if (!$current_room_ids || !$previous_room_ids || $current_room_ids != $previous_room_ids) {
974 return true;
975 }
976
977 // attempt to also detect changes at room-level
978 if ($roomLevel) {
979 // check subunits
980 $current_room_indexes = array_map('intval', array_column($this->getRooms(), 'roomindex'));
981 $previous_room_indexes = array_map('intval', array_column((array) $this->getPreviousProperty('rooms_info', []), 'roomindex'));
982 sort($current_room_indexes);
983 sort($previous_room_indexes);
984 // ensure room subunits information is available for current and previous booking
985 if ($current_room_indexes && $previous_room_indexes && $current_room_indexes != $previous_room_indexes) {
986 // room subunits alteration detected
987 return true;
988 }
989 }
990
991 // no significant changes to stay dates or listings could be proved
992 return false;
993 }
994
995 /**
996 * Tells if the current date and time is between the stay dates.
997 *
998 * @return bool
999 */
1000 public function isStaying()
1001 {
1002 $now = time();
1003
1004 return $this->getProperty('checkin', 0) <= $now && $now <= $this->getProperty('checkout', 0);
1005 }
1006
1007 /**
1008 * Tells if the booking arrival date is today.
1009 *
1010 * @return bool
1011 */
1012 public function isArrivingToday()
1013 {
1014 return date('Y-m-d', $this->getProperty('checkin', 0)) === date('Y-m-d');
1015 }
1016
1017 /**
1018 * Tells if the booking departure date is today.
1019 *
1020 * @return bool
1021 */
1022 public function isDepartingToday()
1023 {
1024 return date('Y-m-d', $this->getProperty('checkout', 0)) === date('Y-m-d');
1025 }
1026
1027 /**
1028 * Tells if the booking arrival date and time is in the future.
1029 *
1030 * @return bool
1031 */
1032 public function isFuture()
1033 {
1034 return $this->getProperty('checkin', 0) > time();
1035 }
1036
1037 /**
1038 * Tells if the booking departure date and time is in the past.
1039 *
1040 * @return bool
1041 */
1042 public function isPast()
1043 {
1044 return $this->getProperty('checkout', 0) < time();
1045 }
1046
1047 /**
1048 * Tells if the booking went through the pre-checkin process.
1049 *
1050 * @return bool
1051 *
1052 * @since 1.18.6 (J) - 1.8.6 (WP)
1053 */
1054 public function hasPreCheckedIn()
1055 {
1056 // check registry protected value
1057 $pre_checkin = $this->getProperty('_precheckin');
1058
1059 if (!is_int($pre_checkin)) {
1060 // fetch the actual status and cache it internally
1061 $pre_checkin = (int) boolval(VikBooking::getBookingHistoryInstance($this->getID())->hasEvent('PC'));
1062 $this->registry['_precheckin'] = $pre_checkin;
1063 }
1064
1065 return (bool) $pre_checkin;
1066 }
1067
1068 /**
1069 * Tells if the booking comes from an OTA.
1070 *
1071 * @return bool
1072 *
1073 * @since 1.18.8 (J) - 1.8.8 (WP)
1074 */
1075 public function isFromOTA()
1076 {
1077 $otaId = $this->getProperty('idorderota', null);
1078 $channel = $this->getProperty('channel', null);
1079
1080 return !empty($otaId) && !empty($channel);
1081 }
1082
1083 /**
1084 * Tells if the booking is fully paid as of now.
1085 * Note that OTA payouts may still need to be received,
1086 * or an outstanding payment may still be due in case of upselling events.
1087 * Do NOT rely on this method alone to determine if payments are needed.
1088 *
1089 * @return bool Even if true, there could be pending payments.
1090 *
1091 * @see getOutstandingBalance()
1092 *
1093 * @since 1.18.8 (J) - 1.8.8 (WP)
1094 */
1095 public function isFullyPaid()
1096 {
1097 // get current totals
1098 $bookingTotal = (float) $this->getProperty('total', 0);
1099 $otaCompensation = (float) $this->getProperty('cmms', 0);
1100 $amountPaid = (float) $this->getProperty('totpaid', 0);
1101
1102 if ($this->isFromOTA()) {
1103 return ($bookingTotal - $amountPaid - $otaCompensation) < 1;
1104 }
1105
1106 return $amountPaid >= $bookingTotal;
1107 }
1108
1109 /**
1110 * Calculates the outstanding balance as of now, if any.
1111 *
1112 * @return ?float
1113 *
1114 * @since 1.18.8 (J) - 1.8.8 (WP)
1115 */
1116 public function getOutstandingBalance()
1117 {
1118 // get current totals
1119 $bookingTotal = (float) $this->getProperty('total', 0);
1120 $otaCompensation = (float) $this->getProperty('cmms', 0);
1121 $amountPaid = (float) $this->getProperty('totpaid', 0);
1122 $amountPayable = (float) $this->getProperty('payable', 0);
1123 $paymentCount = abs((int) $this->getProperty('paymcount', 0));
1124 $bookingDamDep = (float) $this->getProperty('tot_damage_dep', 0);
1125
1126 // check if the amounts look as fully paid
1127 $looksFullyPaid = $this->isFullyPaid();
1128
1129 if ($looksFullyPaid && !$amountPayable) {
1130 // no upselling events involved, nothing is due
1131 return null;
1132 }
1133
1134 // obtain damage deposit details
1135 $damage_deposit_payment = VBORoomHelper::getInstance()->getDamageDepositSplitPayment($this->getData(), $this->getRooms());
1136
1137 // obtain previous damage deposit payments, if any
1138 $prev_dd_payments = [];
1139 if ($damage_deposit_payment['damagedep_gross'] ?? 0) {
1140 $prev_dd_payments = VikBooking::getBookingHistoryInstance($this->getID())
1141 ->getEventsWithData('PN', function($data) {
1142 return (is_object($data) && !empty($data->damage_deposit));
1143 });
1144 if (!$prev_dd_payments) {
1145 // check also the first payment event in case of OTA bookings
1146 $prev_dd_payments = VikBooking::getBookingHistoryInstance($this->getID())
1147 ->getEventsWithData('P0', function($data) {
1148 return (is_object($data) && !empty($data->damage_deposit));
1149 });
1150 }
1151 }
1152
1153 // outstanding balance
1154 $outstanding = $bookingTotal - $amountPaid;
1155 if ($prev_dd_payments && $bookingDamDep) {
1156 // deduct previously paid damage deposit
1157 $outstanding -= $bookingDamDep;
1158 }
1159
1160 // tell if the booking looks payable
1161 $isPayable = (($amountPaid > 0 && ($amountPaid + ($damage_deposit_payment['damagedep_gross'] ?? 0)) < $bookingTotal && $paymentCount) || $amountPayable > 0);
1162
1163 // additional payment flags
1164 $otaWillPay = false;
1165 $payableLater = false;
1166
1167 // determine booking pay-ability
1168 if ($isPayable && $this->isConfirmed() && $this->isFromOTA() && $amountPayable > 0 && !$prev_dd_payments) {
1169 if (($damage_deposit_payment['damagedep_gross'] ?? 0) == $amountPayable && ($damage_deposit_payment['payment_window']['pay_id'] ?? 0)) {
1170 // upselling event must have added the damage deposit to an OTA booking, and this is the only outstanding amount that will be paid separately
1171 $isPayable = false;
1172 // check if the damage deposit is payable later
1173 if (!($damage_deposit_payment['payment_window']['payable'] ?? 0) && ($damage_deposit_payment['payment_window']['payment_from_dt'] ?? null)) {
1174 // will be payable on a date in the future
1175 $payableLater = true;
1176 }
1177 }
1178 }
1179 if ($isPayable && $this->isFromOTA() && $otaCompensation && $looksFullyPaid) {
1180 // the difference of the amount paid is equal to the OTA commissions amount
1181 $isPayable = false;
1182 if ($amountPayable > 0 && $bookingTotal > $amountPaid && round(($bookingTotal - $amountPaid), 0) == round($amountPayable, 0)) {
1183 // there must have been an upselling event or a payment request with an amount equal to the OTA commissions
1184 $isPayable = true;
1185 }
1186 }
1187 if ($isPayable && $this->isFromOTA() && !$amountPayable) {
1188 // access OTA payout events
1189 $prev_ota_payments = VikBooking::getBookingHistoryInstance($this->getID())
1190 ->getEventsWithData('PO', null, false);
1191 if (!$prev_ota_payments) {
1192 // the OTA will pay the remaining balance
1193 $isPayable = false;
1194 $otaWillPay = true;
1195 }
1196 }
1197
1198 if ($outstanding && !$isPayable && $payableLater) {
1199 // deduct the damage deposit amount that will be payable in a future date
1200 $outstanding -= $bookingDamDep;
1201 }
1202
1203 if ($outstanding && !$isPayable && $otaWillPay) {
1204 // deduct OTA commissions that has not been paid yet
1205 $outstanding -= $otaCompensation;
1206 }
1207
1208 return $outstanding > 0 ? $outstanding : null;
1209 }
1210
1211 /**
1212 * Returns a unique list of booked listing names.
1213 *
1214 * @return array Linear array of strings, if any.
1215 *
1216 * @since 1.18.8 (J) - 1.8.8 (WP)
1217 */
1218 public function getListingNames()
1219 {
1220 $listingIds = $this->getBookedListingIds();
1221 $roomNames = VikBooking::getAvailabilityInstance(true)->loadRooms($listingIds);
1222
1223 return array_map(function($id) use ($roomNames) {
1224 return $roomNames[$id]['name'] ?? $id;
1225 }, $listingIds);
1226 }
1227
1228 /**
1229 * Returns a unique list of booked listing addresses.
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 getListingAddresses()
1236 {
1237 if (!class_exists('VCMOtaListing')) {
1238 // prevent errors when the CM is not installed
1239 return [];
1240 }
1241
1242 $addresses = [];
1243
1244 foreach ($this->getBookedListingIds() as $listingId) {
1245 $addresses[] = VCMOtaListing::getInstance()->getLocation($listingId)['address'] ?? null;
1246 }
1247
1248 return array_values(array_unique($addresses));
1249 }
1250
1251 /**
1252 * Returns the booking routed URI, either in its regular or shorten forms.
1253 *
1254 * @return string
1255 *
1256 * @since 1.18.13 (J) - 1.8.13 (WP)
1257 */
1258 public function getBookingLink(bool $shorten = false)
1259 {
1260 // construct booking link
1261 $useSid = !$this->getProperty('sid') && $this->getProperty('idorderota') ? $this->getProperty('idorderota') : $this->getProperty('sid');
1262 $bestItemid = VikBooking::findProperItemIdType(['booking'], ($this->getProperty('lang') ?: null));
1263 $langSuffix = $bestItemid && $this->getProperty('lang') ? '&lang=' . $this->getProperty('lang') : '';
1264
1265 $bookingLink = VikBooking::externalroute("index.php?option=com_vikbooking&view=booking&sid=" . $useSid . "&ts=" . $this->getProperty('ts') . $langSuffix, false, ($bestItemid ?: null));
1266
1267 if (!$shorten) {
1268 return $bookingLink;
1269 }
1270
1271 // access the model for shortening URLs
1272 $model = VBOModelShortenurl::getInstance($onlyRouted = false)->setBooking($this->getData());
1273
1274 return $model->getShortUrl($bookingLink);
1275 }
1276
1277 /**
1278 * Returns the booking pre-check-in routed URI, either in its regular or shorten forms.
1279 *
1280 * @return string
1281 *
1282 * @since 1.18.13 (J) - 1.8.13 (WP)
1283 */
1284 public function getPrecheckinLink(bool $shorten = false)
1285 {
1286 // obtain booking link
1287 $useSid = !$this->getProperty('sid') && $this->getProperty('idorderota') ? $this->getProperty('idorderota') : $this->getProperty('sid');
1288 $bestItemid = VikBooking::findProperItemIdType(['booking'], ($this->getProperty('lang') ?: null));
1289 $langSuffix = $bestItemid && $this->getProperty('lang') ? '&lang=' . $this->getProperty('lang') : '';
1290
1291 $precheckinLink = VikBooking::externalroute("index.php?option=com_vikbooking&view=precheckin&sid=" . $useSid . "&ts=" . $this->getProperty('ts') . $langSuffix, false, ($bestItemid ?: null));
1292
1293 if (!$shorten) {
1294 return $precheckinLink;
1295 }
1296
1297 // access the model for shortening URLs
1298 $model = VBOModelShortenurl::getInstance($onlyRouted = false)->setBooking($this->getData());
1299
1300 return $model->getShortUrl($precheckinLink);
1301 }
1302 }
1303