PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.8
VikBooking Hotel Booking Engine & PMS v1.8.8
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.8, at admin/helpers/src/booking/registry.php

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