PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.6
VikBooking Hotel Booking Engine & PMS v1.8.6
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 / availability.php

availability.php in VikBooking Hotel Booking Engine & PMS 1.8.6, at admin/helpers/availability.php

2,704 lines 81.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 com_vikbooking
5 * @author Alessio Gaggii - e4j - Extensionsforjoomla.com
6 * @copyright Copyright (C) 2022 e4j - Extensionsforjoomla.com. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 * @link https://vikwp.com
9 */
10
11 defined('ABSPATH') or die('No script kiddies please!');
12
13 /**
14 * Availability handler class for Vik Booking.
15 * Also used to handle website inquiry reservations.
16 *
17 * @since 1.15.0 (J) - 1.5.0 (WP)
18 */
19 class VikBookingAvailability
20 {
21 /**
22 * The singleton instance of the class.
23 *
24 * @var VikBookingAvailability
25 */
26 protected static $instance = null;
27
28 /**
29 * An array containing the stay dates.
30 *
31 * @var array
32 */
33 protected $stay_dates = [];
34
35 /**
36 * An array containing the stay date timestamps.
37 *
38 * @var array
39 */
40 protected $stay_ts = [];
41
42 /**
43 * An array containing the room parties.
44 *
45 * @var array
46 */
47 protected $room_parties = [];
48
49 /**
50 * The total number of days to go "back and forth".
51 *
52 * @var int
53 */
54 protected $back_and_forth = 14;
55
56 /**
57 * A list of the room ids to be checked.
58 *
59 * @var array
60 */
61 protected $room_ids = [];
62
63 /**
64 * Whether to ignore restrictions.
65 *
66 * @var bool
67 */
68 protected $ignore_restrictions = false;
69
70 /**
71 * Whether to ignore rooms availability.
72 *
73 * @var bool
74 */
75 protected $ignore_availability = false;
76
77 /**
78 * Whether check-ins on check-outs are allowed.
79 *
80 * @var bool
81 */
82 protected $inonout_allowed = true;
83
84 /**
85 * The percent ratio for nights/transfers in split stays.
86 *
87 * @var int
88 */
89 protected $nights_transfers_ratio = 100;
90
91 /**
92 * Whether we need to behave for the front-end booking process.
93 *
94 * @var bool
95 */
96 protected $is_front_booking = false;
97
98 /**
99 * The warning string occurred.
100 *
101 * @var string
102 */
103 protected $warning = '';
104
105 /**
106 * The error string occurred.
107 *
108 * @var string
109 */
110 protected $error = '';
111
112 /**
113 * The last error code occurred in TACVBO.
114 *
115 * @var int
116 */
117 protected $errorCode = 0;
118
119 /**
120 * A list of fully booked room ids.
121 *
122 * @var array
123 */
124 protected $fully_booked = [];
125
126 /**
127 * Associative list of all rooms.
128 *
129 * @var array
130 */
131 protected $all_rooms = [];
132
133 /**
134 * Associative list of all rate plans.
135 *
136 * @var array
137 */
138 protected $all_rplans = [];
139
140 /**
141 * Map of min/max LOS tariffs defined per room.
142 *
143 * @var array
144 */
145 protected $min_max_los_tariffs_map = [];
146
147 /**
148 * Class constructor is protected.
149 *
150 * @see getInstance()
151 */
152 protected function __construct()
153 {
154 // load dependencies
155 if (!class_exists('TACVBO')) {
156 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'tac.vikbooking.php';
157 }
158
159 /**
160 * Set default nights transfer ratio.
161 *
162 * @since 1.18.5 (J) - 1.8.5 (WP)
163 */
164 $this->setNightsTransfersRatio();
165 }
166
167 /**
168 * Returns the global object, either a new instance or the existing instance
169 * if the class was already instantiated, unless a new instance is requested.
170 *
171 * @param bool $anew True for forcing a new instance.
172 *
173 * @return VikBookingAvailability
174 */
175 public static function getInstance($anew = false)
176 {
177 if (is_null(static::$instance) || $anew) {
178 static::$instance = new static();
179 }
180
181 return static::$instance;
182 }
183
184 /**
185 * Counts the total number of nights of stay according to the stay dates.
186 *
187 * @param int $from_ts optional check-in timestamp.
188 * @param int $to_ts optional check-out timestamp.
189 *
190 * @return int the total number of nights of stay.
191 *
192 * @since 1.16.0 (J) - 1.6.0 (WP) added args to make this an helper method.
193 */
194 public function countNightsOfStay($from_ts = null, $to_ts = null)
195 {
196 if (!count($this->stay_ts) && (empty($from_ts) || empty($to_ts))) {
197 return 1;
198 }
199
200 if (!empty($from_ts) && !empty($to_ts)) {
201 $use_from = $from_ts;
202 $use_to = $to_ts;
203 } else {
204 $use_from = $this->stay_ts[0];
205 $use_to = $this->stay_ts[1];
206 }
207
208 $secdiff = $use_to - $use_from;
209 $daysdiff = $secdiff / 86400;
210 if (is_int($daysdiff)) {
211 $daysdiff = $daysdiff < 1 ? 1 : $daysdiff;
212 } else {
213 if ($daysdiff < 1) {
214 $daysdiff = 1;
215 } else {
216 $sum = floor($daysdiff) * 86400;
217 $newdiff = $secdiff - $sum;
218 $maxhmore = VikBooking::getHoursMoreRb() * 3600;
219 if ($maxhmore >= $newdiff) {
220 $daysdiff = floor($daysdiff);
221 } else {
222 $daysdiff = ceil($daysdiff);
223 }
224 }
225 }
226
227 return $daysdiff;
228 }
229
230 /**
231 * Explains the error code occurred or passed by using translation strings.
232 *
233 * @param int $force_code optional error code to explain.
234 *
235 * @return string the explanation of the error, or an empty string.
236 */
237 public function explainErrorCode($force_code = 0)
238 {
239 // the error code to parse
240 $use_ecode = $force_code ? $force_code : $this->errorCode;
241
242 if (empty($use_ecode)) {
243 return '';
244 }
245
246 /**
247 * Error code identifier:
248 *
249 * 1 = missing/invalid request options.
250 * 2 = invalid authentication.
251 * 3 = no rooms found for the given party.
252 * 4 = not compliant with booking restrictions.
253 * 5 = not compliant with global closing dates.
254 * 6 = no rates defined for the given length of stay.
255 * 7 = no availability for the dates requested.
256 * 8 = no rooms available due to restrictions at room or rate plan level.
257 */
258
259 switch ($use_ecode) {
260 case 1:
261 return 'Missing or invalid request options.';
262 case 2:
263 return 'Invalid request authentication.';
264 case 3:
265 $expl = JText::translate('VBO_AV_ECODE_3');
266 return $expl != 'VBO_AV_ECODE_3' ? $expl : 'No rooms found for the given party.';
267 case 4:
268 return 'Not compliant with the booking restrictions.';
269 case 5:
270 return 'Not compliant with the global closing dates.';
271 case 6:
272 return 'No rates defined for the given length of stay.';
273 case 7:
274 $expl = JText::translate('VBO_AV_ECODE_7');
275 return $expl != 'VBO_AV_ECODE_7' ? $expl : 'No availability for the dates requested.';
276 case 8:
277 return 'No rooms available due to room or rate plan restrictions.';
278 default:
279 return 'Unknown error code.';
280 }
281 }
282
283 /**
284 * Returns a list of room IDs from the given category IDs.
285 *
286 * @param array $category_ids List of category IDs to filter.
287 *
288 * @return array List of involved and unique room IDs.
289 *
290 * @since 1.17.6 (J) - 1.7.6 (WP)
291 */
292 public function filterRoomCategories(array $category_ids)
293 {
294 $category_ids = array_filter(array_map('abs', array_map('intval', $category_ids)));
295
296 if (!$category_ids) {
297 return [];
298 }
299
300 $dbo = JFactory::getDbo();
301
302 $q = $dbo->getQuery(true)
303 ->select($dbo->qn('id'))
304 ->from($dbo->qn('#__vikbooking_rooms'));
305
306 foreach ($category_ids as $cat_id) {
307 $q->where([
308 $dbo->qn('idcat') . ' = ' . $dbo->q($cat_id . ';'),
309 $dbo->qn('idcat') . ' LIKE ' . $dbo->q($cat_id . ';%'),
310 $dbo->qn('idcat') . ' LIKE ' . $dbo->q('%;' . $cat_id . ';%'),
311 $dbo->qn('idcat') . ' LIKE ' . $dbo->q('%;' . $cat_id . ';'),
312 ], 'OR');
313 }
314
315 $dbo->setQuery($q);
316
317 return array_values(array_unique(array_map('intval', $dbo->loadColumn())));
318 }
319
320 /**
321 * Loads a list of room categories.
322 *
323 * @param bool $public True for excluding the "private" categories.
324 *
325 * @return array The associative list of categories.
326 *
327 * @since 1.17.6 (J) - 1.7.6 (WP)
328 * @since 1.18.3 (J) - 1.8.3 (WP) added argument $public.
329 */
330 public function loadRoomCategories($public = false)
331 {
332 $dbo = JFactory::getDbo();
333
334 $dbo->setQuery(
335 $dbo->getQuery(true)
336 ->select([
337 $dbo->qn('id'),
338 $dbo->qn('name'),
339 ])
340 ->from($dbo->qn('#__vikbooking_categories'))
341 ->order($dbo->qn('name') . ' ASC')
342 );
343
344 $categories = $dbo->loadAssocList();
345
346 if ($public) {
347 $categories = array_values(array_filter($categories, function($category) {
348 return substr((string) $category['name'], 0, 1) !== '_';
349 }));
350 }
351
352 return $categories;
353 }
354
355 /**
356 * Loads all rooms in VBO and maps them into an associative array.
357 *
358 * @param array $ids optional list of IDs to load.
359 * @param int $max optional rooms limit to fetch.
360 * @param bool $anew true to avoid object caching.
361 *
362 * @return array the associative list of rooms.
363 *
364 * @since 1.16.10 (J) - 1.6.10 (WP) added arguments $ids, $max.
365 * @since 1.17.5 (J) - 1.7.5 (WP) added argument $anew.
366 * @since 1.18.6 (J) - 1.8.6 (WP) added support to negative IDs for category IDs.
367 */
368 public function loadRooms(array $ids = [], $max = 0, $anew = false)
369 {
370 if ($this->all_rooms && !$anew) {
371 // return previously cached array if available
372 return $this->all_rooms;
373 }
374
375 $dbo = JFactory::getDbo();
376
377 $q = $dbo->getQuery(true)
378 ->select([
379 $dbo->qn('id'),
380 $dbo->qn('name'),
381 $dbo->qn('img'),
382 $dbo->qn('idcat'),
383 $dbo->qn('avail'),
384 $dbo->qn('units'),
385 $dbo->qn('fromadult'),
386 $dbo->qn('toadult'),
387 $dbo->qn('fromchild'),
388 $dbo->qn('tochild'),
389 $dbo->qn('totpeople'),
390 $dbo->qn('mintotpeople'),
391 ])
392 ->from($dbo->qn('#__vikbooking_rooms'));
393
394 if ($ids) {
395 // cast all IDs to signed integers
396 $ids = array_map('intval', $ids);
397
398 // normalize listing IDs to support (negative) category IDs
399 $categoryIds = array_filter($ids, function($id) {
400 return $id < 0;
401 });
402
403 if ($categoryIds) {
404 // filter out negative IDs
405 $ids = array_values(array_filter($ids, function($id) {
406 return $id > 0;
407 }));
408
409 // include the listing IDs from category IDs, if any
410 $ids = array_values(array_unique(array_merge($ids, $this->filterRoomCategories($categoryIds))));
411 }
412
413 // set query clause
414 $q->where($dbo->qn('id') . ' IN (' . implode(', ', $ids) . ')');
415 }
416
417 $q->order($dbo->qn('avail') . ' DESC');
418 $q->order($dbo->qn('name') . ' ASC');
419
420 $dbo->setQuery($q, 0, $max);
421 $room_rows = $dbo->loadAssocList();
422
423 if (!$room_rows) {
424 return $anew ? [] : $this->all_rooms;
425 }
426
427 if ($this->isFrontBooking()) {
428 // apply translations on rooms
429 $vbo_tn = VikBooking::getTranslator();
430 $vbo_tn->translateContents($room_rows, '#__vikbooking_rooms');
431 }
432
433 $assoc_rooms = [];
434 foreach ($room_rows as $room) {
435 $assoc_rooms[$room['id']] = $room;
436 }
437
438 if (!$anew) {
439 // cache room records
440 $this->all_rooms = $assoc_rooms;
441 }
442
443 return $anew ? $assoc_rooms : $this->all_rooms;
444 }
445
446 /**
447 * Sets the current rooms as an associative array of information. The
448 * array keys represent the room IDs as an associative array of details.
449 *
450 * @param array $rooms the associatve list of rooms.
451 *
452 * @return self
453 */
454 public function setRooms(array $rooms = [])
455 {
456 $this->all_rooms = $rooms;
457
458 return $this;
459 }
460
461 /**
462 * Filters all rooms by keeping just the ones published/available.
463 *
464 * @return array associative array of published (available) rooms.
465 */
466 public function filterPublishedRooms()
467 {
468 $rooms = $this->loadRooms();
469
470 foreach ($rooms as $k => $room) {
471 if (!($room['avail'] ?? 1)) {
472 unset($rooms[$k]);
473 }
474 }
475
476 return $rooms;
477 }
478
479 /**
480 * Finds a room by name.
481 *
482 * @param string $name The room name to look for.
483 *
484 * @return array
485 *
486 * @since 1.16.10 (J) - 1.6.10 (WP)
487 */
488 public function getRoomByName(string $name)
489 {
490 $dbo = JFactory::getDbo();
491
492 $q = $dbo->getQuery(true)
493 ->select('*')
494 ->from($dbo->qn('#__vikbooking_rooms'))
495 ->order($dbo->qn('avail') . ' DESC')
496 ->order($dbo->qn('name') . ' ASC');
497
498 foreach (array_filter(preg_split("/[\s\-_.,]+/", $name)) as $nm_part) {
499 $q->where($dbo->qn('name') . ' LIKE ' . $dbo->q("%{$nm_part}%"));
500 }
501
502 $dbo->setQuery($q, 0, 1);
503 $record = $dbo->loadAssoc();
504
505 return $record ?? [];
506 }
507
508 /**
509 * Loads all rate plans in VBO and maps them into an associative array.
510 *
511 * @param bool $no_cache True to avoid internal caching.
512 *
513 * @return array the associative list of rate plans.
514 *
515 * @since 1.17.6 (J) - 1.7.6 (WP) added $no_cache argument.
516 */
517 public function loadRatePlans($no_cache = false)
518 {
519 if (!$no_cache && $this->all_rplans) {
520 // return previously cached array if available
521 return $this->all_rplans;
522 }
523
524 $dbo = JFactory::getDbo();
525
526 $rate_plans = [];
527 $derived_rplans = [];
528
529 $q = "SELECT * FROM `#__vikbooking_prices` ORDER BY `name` ASC;";
530 $dbo->setQuery($q);
531 $rplan_rows = $dbo->loadAssocList();
532
533 foreach ($rplan_rows as $rplan) {
534 if (!empty($rplan['derived_id']) && !empty($rplan['derived_data'])) {
535 // decode the derived data information
536 $rplan['derived_data'] = json_decode($rplan['derived_data'], true);
537 // add rate plan ID
538 $derived_rplans[] = $rplan['id'];
539 }
540 $rate_plans[$rplan['id']] = $rplan;
541 }
542
543 // add the information about the parent rate plans, if any
544 foreach ($rate_plans as $rplan_id => $rplan_data) {
545 if (in_array($rplan_id, $derived_rplans) && isset($rate_plans[$rplan_data['derived_id']])) {
546 // set parent rate details
547 $rate_plans[$rplan_id]['parent_rate_id'] = $rate_plans[$rplan_data['derived_id']]['id'];
548 $rate_plans[$rplan_id]['parent_rate_name'] = $rate_plans[$rplan_data['derived_id']]['name'];
549 }
550 }
551
552 // sort rate plans
553 $rate_plans = VikBooking::sortRatePlans($rate_plans, true);
554
555 if (!$no_cache) {
556 $this->all_rplans = $rate_plans;
557 }
558
559 return $rate_plans;
560 }
561
562 /**
563 * Returns a list of rate plans derived from the given parent rate plan ID.
564 *
565 * @param int $parent_rate_id The parent rate plan ID.
566 * @param ?array $all_rate_plans Optional cached rate plans list.
567 *
568 * @return array
569 *
570 * @since 1.16.10 (J) - 1.6.10 (WP)
571 * @since 1.18.5 (J) - 1.8.5 (WP) added argument $all_rate_plans.
572 */
573 public function getDerivedRatePlans(int $parent_rate_id, ?array $all_rate_plans = null)
574 {
575 $derived_rplans = [];
576
577 foreach (($all_rate_plans ?: $this->loadRatePlans()) as $rp_id => $rplan) {
578 if ($rplan['derived_id'] && $rplan['derived_id'] == $parent_rate_id && $rplan['derived_data']) {
579 // this rate plan is derived from the given parent rate plan
580 $derived_rplans[] = $rplan;
581 }
582 }
583
584 return $derived_rplans;
585 }
586
587 /**
588 * Returns the orphan dates for the given or set rooms and dates.
589 *
590 * @param array $options List of fetching options.
591 *
592 * @return array Associative list of rooms orphan dates.
593 *
594 * @since 1.17.7 (J) - 1.7.7 (WP)
595 */
596 public function getOrphanDates(array $options = [])
597 {
598 // flag to indicate whether room-level restrictions were loaded
599 $use_room_level_restr = false;
600
601 // load and set rooms and restrictions
602 if ($options['room_ids'] ?? []) {
603 // force the requested rooms to be loaded
604 $this->loadRooms((array) $options['room_ids']);
605
606 // load restrictions with room filters
607 $all_restrictions = VikBooking::loadRestrictions(true, (array) $options['room_ids']);
608
609 // turn flag on for room-level restrictions being used
610 $use_room_level_restr = true;
611 } else {
612 // load restrictions with no filters
613 $all_restrictions = VikBooking::loadRestrictions(false);
614 }
615
616 // default minimum stay
617 $def_min_stay = VikBooking::getDefaultNightsCalendar();
618
619 // figure out dates range
620 if (($current_stay_dates = $this->getStayDates()) && !($options['from_date'] ?? null)) {
621 // populate current stay dates
622 $options['from_date'] = $current_stay_dates[0];
623 $options['to_date'] = $current_stay_dates[1];
624 } else {
625 // check given fetching options
626 if (!($options['from_date'] ?? null)) {
627 // default to today's date
628 $options['from_date'] = date('Y-m-d');
629 }
630
631 if (!($options['to_date'] ?? null)) {
632 // default to next week's date
633 $options['to_date'] = date('Y-m-d', strtotime('+1 week'));
634 }
635 }
636
637 // calculate the highest minimum stay for a better accuracy
638 $all_min_los = [($def_min_stay > 1 ? $def_min_stay : 0)];
639 foreach ($all_restrictions as $index => $restrs) {
640 $all_min_los = array_merge($all_min_los, array_column($restrs, 'minlos'));
641 }
642 $max_min_los = max(array_map('intval', $all_min_los));
643
644 if (!$max_min_los) {
645 // no minimum stay restrictions to apply, hence no orphan dates
646 return [];
647 }
648
649 // calculate past limit timestamp for today at midnight
650 $lim_past_ts = strtotime(date('Y-m-d'));
651
652 // always fetch a wider availability window for better accuracy
653 $fetch_from_date = date('Y-m-d', strtotime("-{$max_min_los} days", strtotime($options['from_date'])));
654 $fetch_to_date = date('Y-m-d', strtotime("+{$max_min_los} days", strtotime($options['to_date'])));
655 if (strtotime($fetch_from_date) < $lim_past_ts) {
656 // start fetching the inventory from today's date (past dates not accepted)
657 $fetch_from_date = date('Y-m-d');
658 }
659
660 // always set stay dates
661 $this->setStayDates($fetch_from_date, $fetch_to_date);
662
663 // obtain the availability inventory by ignoring restrictions
664 $ari = $this->getInventory(false);
665
666 if (!$ari) {
667 return [];
668 }
669
670 // build orphan dates pool
671 $orphans_pool = [];
672
673 // UTC timezone
674 $utc_tz = new DateTimezone('UTC');
675
676 // get date bounds
677 $from_bound = new DateTime($options['from_date'], $utc_tz);
678 $to_bound = new DateTime($options['to_date'], $utc_tz);
679
680 // build iterable dates interval (period)
681 $date_range = new DatePeriod(
682 // start date included by default in the result set
683 $from_bound,
684 // interval between recurrences within the period
685 new DateInterval('P1D'),
686 // end date excluded by default from the result set
687 $to_bound->modify('+1 day')
688 );
689
690 // scan rooms availability inventory
691 foreach ($ari as $idroom => $room_ari) {
692 // load proper room-level restrictions if not done already
693 $all_restrictions = $use_room_level_restr ? $all_restrictions : VikBooking::loadRestrictions(true, [$idroom]);
694
695 // iterate the dates interval
696 foreach ($date_range as $dt) {
697 // calculate date values
698 $day_key = $dt->format('Y-m-d');
699 $day_now_ts = strtotime($day_key);
700 $day_after_ts = strtotime('+1 day', $day_now_ts);
701
702 // parse room restrictions for the current inventory day and room to get the minimum stay
703 $restr = VikBooking::parseSeasonRestrictions($day_now_ts, $day_after_ts, 1, $all_restrictions);
704 $minimum_stay = (int) ($restr['minlos'] ?? $def_min_stay);
705
706 if ($minimum_stay < 2) {
707 // no real minimum stay restriction detected for this day
708 continue;
709 }
710
711 // scan rooms availability
712 foreach ($room_ari['inventory'] as $keypoint => $inventory) {
713 if ($inventory['day'] != $day_key) {
714 // not the date-key point we are looking for
715 continue;
716 }
717
718 // tell whether the listing is available on the current date-key point
719 $is_available = (bool) ($inventory['units_to_sell'] ?? $inventory['available'] ?? 0);
720 if (!$is_available) {
721 // this day is fully booked, hence no orphan dates
722 continue 2;
723 }
724
725 // scan the availability for the next minimum stay days from the current day-key point
726 for ($d = 0; $d < $minimum_stay; $d++) {
727 // build next date-key point value (start from current day-key point as it counts as one available night of stay)
728 $next_keypoint = $keypoint + $d;
729
730 if (!isset($room_ari['inventory'][$next_keypoint])) {
731 // no more data available
732 break;
733 }
734
735 // tell whether the listing will be available on this future date-key point (0th day will always be available)
736 $will_be_available = (bool) ($room_ari['inventory'][$next_keypoint]['units_to_sell'] ?? $room_ari['inventory'][$next_keypoint]['available'] ?? 0);
737
738 if (!$will_be_available) {
739 /**
740 * Probable orphan date found for bookings on days ahead. Ensure this
741 * is truly an orphan date by checking the days before. If staying
742 * on this (available) night is allowed, then this should not be
743 * considered as an orphan date, even though arriving is not allowed.
744 */
745 if (!($options['orphans_checkin'] ?? false)) {
746 // check previous dates before saying it's an orphan date
747 for ($backd = ($keypoint - 1), $distance = 1; $backd >= 0; $backd--, $distance++) {
748 if (!isset($room_ari['inventory'][$backd])) {
749 // nothing to do, let it be an orphan date
750 break;
751 }
752
753 // tell whether the listing was available on this previous date-key point
754 $was_available = (bool) ($room_ari['inventory'][$backd]['units_to_sell'] ?? $room_ari['inventory'][$backd]['available'] ?? 0);
755
756 if (!$was_available) {
757 // nothing to do, let it be an orphan date
758 break;
759 }
760
761 // calculate past date values
762 $past_day_now_ts = strtotime($room_ari['inventory'][$backd]['day']);
763 $past_day_after_ts = strtotime('+1 day', $past_day_now_ts);
764
765 // parse room restrictions for the current past day and room to get the minimum stay
766 $restr = VikBooking::parseSeasonRestrictions($past_day_now_ts, $past_day_after_ts, 1, $all_restrictions);
767 $minimum_stay = (int) ($restr['minlos'] ?? $def_min_stay);
768
769 if ($minimum_stay <= $distance) {
770 // free day with a minimum stay that allows to stay on the presumed orphan date
771 // break the cycles for the presumed orphan date-key point, because it's not truly an orphan
772 break 3;
773 }
774 }
775 }
776
777 // orphan date found
778 if (!isset($orphans_pool[$idroom])) {
779 // start container
780 $orphans_pool[$idroom] = [
781 'room_name' => $room_ari['room_name'],
782 'orphans' => [],
783 ];
784 }
785
786 // push orphan date information
787 $orphans_pool[$idroom]['orphans'][] = [
788 'day' => $day_key,
789 'current_min_stay_nights' => $minimum_stay,
790 'max_min_stay_nights_allowed' => $d,
791 ];
792
793 // break the cycles for this date-key point
794 break 2;
795 }
796 }
797 }
798 }
799 }
800
801 // return the associative list of room orphan dates found, if any
802 return $orphans_pool;
803 }
804
805 /**
806 * Returns the inventory for the rooms and dates set.
807 *
808 * @param bool $restrictions Whether to include booking restrictions data.
809 *
810 * @return array Associative list of rooms availability inventory.
811 *
812 * @since 1.16.10 (J) - 1.6.10 (WP)
813 */
814 public function getInventory($restrictions = false)
815 {
816 $stay_ts = $this->getStayDates(true);
817
818 if (!$stay_ts) {
819 $this->setError('No dates provided');
820 return [];
821 }
822
823 if ($stay_ts[0] > $stay_ts[1]) {
824 $this->setError('Invalid dates provided');
825 return [];
826 }
827
828 $info_from = getdate($stay_ts[0]);
829
830 $room_ids = array_column($this->loadRooms(), 'id');
831 $room_names = array_column($this->loadRooms(), 'name');
832 $room_units = array_column($this->loadRooms(), 'units');
833
834 if (!$room_ids) {
835 $this->setError('No rooms provided');
836 return [];
837 }
838
839 // load busy records
840 $busy_records = VikBooking::loadBusyRecords($room_ids, $stay_ts[0], $stay_ts[1]);
841
842 // room restrictions associative list
843 $room_restrictions = [];
844
845 // global minimum stay restriction
846 $glob_minlos = VikBooking::getDefaultNightsCalendar();
847 $glob_minlos = $glob_minlos < 1 ? 1 : (int) $glob_minlos;
848
849 // inventory pool
850 $ari = [];
851 foreach ($room_ids as $room_index => $room_id) {
852 $ari[$room_id] = [
853 'room_name' => $room_names[$room_index],
854 'inventory' => [],
855 ];
856 }
857
858 // loop through all dates in the range
859 while ($info_from[0] <= $stay_ts[1]) {
860 // build day after timestamp
861 $day_after_ts = mktime(0, 0, 0, $info_from['mon'], $info_from['mday'] + 1, $info_from['year']);
862
863 // scan the units booked for each requested room
864 foreach ($room_ids as $room_index => $room_id) {
865 $units_booked = 0;
866 // scan all the occupied records of the current room
867 foreach (($busy_records[$room_id] ?? []) as $busy) {
868 $info_in = getdate($busy['checkin']);
869 $info_out = getdate($busy['checkout']);
870 $in_ts = mktime(0, 0, 0, $info_in['mon'], $info_in['mday'], $info_in['year']);
871 $out_ts = mktime(0, 0, 0, $info_out['mon'], $info_out['mday'], $info_out['year']);
872 if ($info_from[0] >= $in_ts && $info_from[0] < $out_ts) {
873 // increase room units booked
874 $units_booked++;
875 }
876 }
877
878 // count units left
879 $units_left = $units_booked >= $room_units[$room_index] ? 0 : ($room_units[$room_index] - $units_booked);
880
881 // set room inventory date
882 $inventory_day = [
883 'day' => date('Y-m-d', $info_from[0]),
884 ];
885
886 if ($room_units[$room_index] == 1) {
887 // single-unit listing inventory structure
888 $inventory_day['available'] = (bool) $units_left;
889 } else {
890 // multi-unit room-type inventory structure
891 $inventory_day['units_booked'] = $units_booked;
892 $inventory_day['units_to_sell'] = $units_left;
893 }
894
895 // check for room-level restrictions
896 if ($restrictions) {
897 if (!isset($room_restrictions[$room_id])) {
898 // load room restrictions only once
899 $room_restrictions[$room_id] = VikBooking::loadRestrictions(true, [$room_id]);
900 }
901
902 // parse room restrictions for the current inventory day
903 $restr = VikBooking::parseSeasonRestrictions($info_from[0], $day_after_ts, 1, $room_restrictions[$room_id]);
904 if ($restr) {
905 $inventory_day['restrictions'] = [
906 'min_los' => (int) $restr['minlos'],
907 ];
908 if (($restr['maxlos'] ?? 0)) {
909 $inventory_day['restrictions']['max_los'] = (int) $restr['maxlos'];
910 }
911 if (!empty($restr['cta'])) {
912 $inventory_day['restrictions']['closed_to_arrival'] = array_map(function($wday) {
913 switch ($wday) {
914 case '1':
915 return 'Monday';
916 case '2':
917 return 'Tuesday';
918 case '3':
919 return 'Wednesday';
920 case '4':
921 return 'Thursday';
922 case '5':
923 return 'Friday';
924 case '6':
925 return 'Saturday';
926 default:
927 return 'Sunday';
928 }
929 }, $restr['cta']);
930 }
931 if (!empty($restr['ctd'])) {
932 $inventory_day['restrictions']['closed_to_departure'] = array_map(function($wday) {
933 switch ($wday) {
934 case '1':
935 return 'Monday';
936 case '2':
937 return 'Tuesday';
938 case '3':
939 return 'Wednesday';
940 case '4':
941 return 'Thursday';
942 case '5':
943 return 'Friday';
944 case '6':
945 return 'Saturday';
946 default:
947 return 'Sunday';
948 }
949 }, $restr['ctd']);
950 }
951 } else {
952 // set the global minimum stay restriction
953 $inventory_day['restrictions'] = [
954 'min_los' => $glob_minlos,
955 ];
956 }
957 }
958
959 // push room-day inventory
960 $ari[$room_id]['inventory'][] = $inventory_day;
961 }
962
963 // go to next date
964 $info_from = getdate($day_after_ts);
965 }
966
967 return $ari;
968 }
969
970 /**
971 * Gets the available room rates for the specified dates, party and rooms.
972 *
973 * @param array $params optional list of options to be forced for TACVBO.
974 *
975 * @return mixed boolean false in case of errors or array result of TACVBO class.
976 */
977 public function getRates($params = [])
978 {
979 // reset errors to their initial values
980 $this->error = '';
981 $this->errorCode = 0;
982
983 if (!$this->stay_dates) {
984 $this->setError('No dates provided');
985 return false;
986 }
987
988 if (!$this->room_parties) {
989 $this->setError('No room party provided');
990 return false;
991 }
992
993 // count injected rooms, if any
994 $tot_rooms = count($this->room_ids);
995
996 // build options array for TACVBO
997 $options = [
998 'hash' => md5('vbo.e4j.vbo'),
999 'req_type' => 'hotel_availability',
1000 'start_date' => $this->stay_dates[0],
1001 'end_date' => $this->stay_dates[1],
1002 'nights' => $this->countNightsOfStay(),
1003 'num_rooms' => ($tot_rooms > 0 ? $tot_rooms : 1),
1004 'adults' => [$this->getPartyGuests('adults', 0)],
1005 'children' => [$this->getPartyGuests('children', 0)],
1006 'only_rates' => 1,
1007 'wtax' => $params['wtax'] ?? null,
1008 ];
1009
1010 if ($tot_rooms > 1 && count($this->room_parties) == $tot_rooms) {
1011 // re-build list of adults and children
1012 $options['adults'] = [];
1013 $options['children'] = [];
1014 for ($i = 0; $i < $tot_rooms; $i++) {
1015 $options['adults'][] = $this->getPartyGuests('adults', $i);
1016 $options['children'][] = $this->getPartyGuests('children', $i);
1017 }
1018 }
1019
1020 // check for implicit settings
1021 if (!empty($params['max_rooms_limit'])) {
1022 // set rooms maximum limit
1023 TACVBO::$maxRoomsLimit = (int) $params['max_rooms_limit'];
1024 // unset implicit setting
1025 unset($params['max_rooms_limit']);
1026 } else {
1027 // reset to initial value for subsequent calls
1028 TACVBO::$maxRoomsLimit = 0;
1029 }
1030
1031 if (is_array(($params['forced_room_ids'] ?? null))) {
1032 // set allowed room IDs
1033 TACVBO::$forcedRoomIds = $params['forced_room_ids'];
1034 // unset implicit setting
1035 unset($params['forced_room_ids']);
1036 } else {
1037 // reset to initial value for subsequent calls
1038 TACVBO::$forcedRoomIds = [];
1039 }
1040
1041 // merge default options with params, if any
1042 $options = array_merge($options, $params);
1043
1044 // invoke TACVBO class by injecting the options
1045 TACVBO::$getArray = true;
1046 TACVBO::$ignoreRestrictions = $this->ignore_restrictions;
1047 TACVBO::$ignoreAvailability = $this->ignore_availability;
1048 $website_rates = TACVBO::tac_av_l($options);
1049
1050 // store the error code occurred (if any)
1051 $this->errorCode = TACVBO::getErrorCode();
1052
1053 if (!is_array($website_rates)) {
1054 // critical error
1055 $this->setError(str_replace('e4j.error.', '', $website_rates));
1056 return false;
1057 }
1058
1059 if (isset($website_rates['e4j.error'])) {
1060 // calculation/availability error
1061 $this->setError($website_rates['e4j.error']);
1062 // check if reserved keys like "fullybooked" are present
1063 if (isset($website_rates['fullybooked']) && is_array($website_rates['fullybooked'])) {
1064 // store fully booked rooms array
1065 $this->fully_booked = $website_rates['fullybooked'];
1066 }
1067 // always return false
1068 return false;
1069 }
1070
1071 // optional filter by room IDs will be applied on this flow
1072 $found_rids = array_keys($website_rates);
1073 $unwanted_rids = $tot_rooms ? array_diff($found_rids, $this->room_ids) : [];
1074 foreach ($unwanted_rids as $rid) {
1075 unset($website_rates[$rid]);
1076 }
1077
1078 return $website_rates;
1079 }
1080
1081 /**
1082 * Finds the available suggestions in case of no availability previously occurred
1083 * while getting the rates. This method should be called after getRates() so that
1084 * a valid errorCode to be analized will be available, unless code is forced.
1085 * Suggestions can include closest booking dates and/or different room-guest parties.
1086 *
1087 * @param int $force_code the error code to force (no availability or party unsatisfied).
1088 * @param array $force_rooms an optional list of room IDs to consider for the suggestions.
1089 *
1090 * @return array array of alternative dates, alternative room-parties and split stays.
1091 *
1092 * @since 1.16.0 (J) - 1.6.0 (WP) list of split stays available is also returned.
1093 */
1094 public function findSuggestions($force_code = 0, $force_rooms = [])
1095 {
1096 // reset error and warning strings to start a new calculation
1097 $this->error = '';
1098 $this->warning = '';
1099
1100 // build containers for the two types of suggestions
1101 $alternative_dates = [];
1102 $alternative_parties = [];
1103 $split_stay_sols = [];
1104
1105 // the error code to parse
1106 $use_ecode = $force_code ? $force_code : $this->errorCode;
1107
1108 if (empty($use_ecode)) {
1109 // do not continue if no valid errors previously occurred or forced
1110 $this->setError('Empty error code');
1111 return [$alternative_dates, $alternative_parties, $split_stay_sols];
1112 }
1113
1114 if ($use_ecode == 7 && (count($this->fully_booked) || count($force_rooms))) {
1115 // get the closest booking dates for the compatible, yet unavailable, rooms
1116 $use_rooms = count($force_rooms) ? $force_rooms : $this->fully_booked;
1117 $alternative_dates = $this->findClosestRoomDateSolutions($use_rooms);
1118 // calculate the split stay solutions available for the compatible rooms
1119 $split_stay_sols = $this->findSplitStays($use_rooms);
1120 }
1121
1122 if ($use_ecode == 3) {
1123 // no rooms found for the given party, suggest alternative parties
1124 $active_rooms = count($force_rooms) ? $force_rooms : array_keys($this->filterPublishedRooms());
1125 // match all the available rooms in the requested or near dates
1126 $all_solutions = $this->findClosestRoomDateSolutions($active_rooms);
1127 // sort solutions by bigger rooms
1128 $all_solutions = $this->sortBiggerRoomSolutions($all_solutions);
1129 // find matching solutions for the requested party
1130 $alternative_parties = $this->matchSolutionsParty($all_solutions);
1131 }
1132
1133 return [$alternative_dates, $alternative_parties, $split_stay_sols];
1134 }
1135
1136 /**
1137 * Given a list of unavailable room IDs, yet compatible with the party and LOS requested,
1138 * we build a list of available solutions for booking split stays on the same dates. The
1139 * visibility should be public so that other Views could use just this method.
1140 *
1141 * @param array $room_ids list of unavailable, yet compatible, room IDs.
1142 * @param ?array $busy_list optional list of busy records for the involved dates.
1143 *
1144 * @return array associative list of available split stays, or empty array.
1145 *
1146 * @since 1.16.0 (J) - 1.6.0 (WP)
1147 */
1148 public function findSplitStays(array $room_ids = [], ?array $busy_list = null)
1149 {
1150 if (!$room_ids) {
1151 return [];
1152 }
1153
1154 // get all website rooms
1155 $all_rooms = $this->loadRooms();
1156
1157 // validate max occupancy for the given rooms
1158 if (count($this->room_parties) === 1) {
1159 // we only use the first party for the occupancy validation
1160 $party_adults = $this->getPartyGuests('adults', $party = 0);
1161 $party_children = $this->getPartyGuests('children', $party = 0);
1162 foreach ($room_ids as $rindex => $rid) {
1163 if (!isset($all_rooms[$rid])) {
1164 unset($room_ids[$rindex]);
1165 continue;
1166 }
1167 if ($party_adults > $all_rooms[$rid]['toadult'] || $party_children > $all_rooms[$rid]['tochild'] || ($party_adults + $party_children) > $all_rooms[$rid]['totpeople']) {
1168 // max occupancy not met
1169 unset($room_ids[$rindex]);
1170 continue;
1171 }
1172 }
1173
1174 if (!$room_ids) {
1175 return [];
1176 }
1177
1178 // reset array keys
1179 $room_ids = array_values($room_ids);
1180 }
1181
1182 // get original check-in and check-out timestamps
1183 list($orig_checkin_ts, $orig_checkout_ts) = $this->getStayDates(true);
1184 $info_from = getdate($orig_checkin_ts);
1185 $info_to = getdate($orig_checkout_ts);
1186
1187 // the final check-out date
1188 $final_checkout_ymd = date('Y-m-d', $orig_checkout_ts);
1189
1190 // count original length of stay and nights involved
1191 $tot_nights = $this->countNightsOfStay();
1192 $groupdays = VikBooking::getGroupDays($orig_checkin_ts, $orig_checkout_ts, $tot_nights);
1193
1194 if ($tot_nights < 2) {
1195 // useless to waste time on finding a split stay if not at least 2 nights
1196 return [];
1197 }
1198
1199 // load the occupied records for these dates and rooms
1200 $busy_records = !is_null($busy_list) ? $busy_list : VikBooking::loadBusyRecords($room_ids, $orig_checkin_ts, strtotime('+1 day', $orig_checkout_ts));
1201
1202 // calculate available rooms for each night
1203 $avroom_nights = [];
1204 foreach ($room_ids as $rid) {
1205 if (!isset($all_rooms[$rid])) {
1206 continue;
1207 }
1208 $room = $all_rooms[$rid];
1209 foreach ($groupdays as $gday) {
1210 $day_key = date('Y-m-d', $gday);
1211 $bfound = 0;
1212 if (!isset($busy_records[$rid])) {
1213 $busy_records[$rid] = [];
1214 }
1215 foreach ($busy_records[$rid] as $bu) {
1216 $busy_info_in = getdate($bu['checkin']);
1217 $busy_info_out = getdate($bu['checkout']);
1218 $busy_in_ts = mktime(0, 0, 0, $busy_info_in['mon'], $busy_info_in['mday'], $busy_info_in['year']);
1219 $busy_out_ts = mktime(0, 0, 0, $busy_info_out['mon'], $busy_info_out['mday'], $busy_info_out['year']);
1220 if ($gday >= $busy_in_ts && $gday == $busy_out_ts && !$this->inonout_allowed && $room['units'] < 2) {
1221 // check-ins on check-outs not allowed
1222 $bfound++;
1223 if ($bfound >= $room['units']) {
1224 break;
1225 }
1226 }
1227 if ($gday >= $busy_in_ts && $gday < $busy_out_ts) {
1228 $bfound++;
1229 if ($bfound >= $room['units']) {
1230 break;
1231 }
1232 }
1233 }
1234 if ($bfound < $room['units']) {
1235 // push this night as available for this room
1236 if (!isset($avroom_nights[$rid])) {
1237 $avroom_nights[$rid] = [];
1238 }
1239 $avroom_nights[$rid][] = $day_key;
1240 } else {
1241 // room not available on this night, make sure to unset any previous value
1242 if (isset($avroom_nights[$rid]) && in_array($day_key, $avroom_nights[$rid])) {
1243 $unav_key = array_search($day_key, $avroom_nights[$rid]);
1244 unset($avroom_nights[$rid][$unav_key]);
1245 $avroom_nights[$rid] = array_values($avroom_nights[$rid]);
1246 }
1247 }
1248 }
1249 }
1250
1251 if (!count($avroom_nights)) {
1252 // no rooms available at all, there's no way to do anything
1253 return [];
1254 }
1255
1256 // make sure all nights requested can be satisfied by at least one room
1257 foreach ($groupdays as $gday_index => $gday) {
1258 $day_key = date('Y-m-d', $gday);
1259 $day_av = false;
1260 foreach ($avroom_nights as $rid => $av_nights) {
1261 if (in_array($day_key, $av_nights)) {
1262 // night was found
1263 $day_av = true;
1264 break;
1265 }
1266 }
1267 // ensure there's availability for this day, or permit if it's the check-out day
1268 if (!$day_av && !($gday_index === (count($groupdays) - 1) && $this->inonout_allowed)) {
1269 // this night of stay is not available in any room, unable to proceed
1270 return [];
1271 }
1272 }
1273
1274 // count the number of consecutive nights per room
1275 $cons_room_nights = [];
1276 $tot_gdays = count($groupdays);
1277 foreach ($groupdays as $k => $gday) {
1278 $day_key = date('Y-m-d', $gday);
1279 if (!isset($cons_room_nights[$day_key])) {
1280 $cons_room_nights[$day_key] = [];
1281 }
1282 foreach ($avroom_nights as $rid => $av_nights) {
1283 if (in_array($day_key, $av_nights)) {
1284 if (!isset($cons_room_nights[$day_key][$rid])) {
1285 $cons_room_nights[$day_key][$rid] = [];
1286 }
1287 // count the next consecutive nights of stay
1288 $cons_room_nights[$day_key][$rid][] = $day_key;
1289 for ($j = ($k + 1); $j < $tot_gdays; $j++) {
1290 $next_day_key = date('Y-m-d', $groupdays[$j]);
1291 if (in_array($next_day_key, $av_nights)) {
1292 $cons_room_nights[$day_key][$rid][] = $next_day_key;
1293 } else {
1294 break;
1295 }
1296 }
1297 }
1298 }
1299 }
1300
1301 // sort the solutions with the highest number of consecutive nights to reduce the splits
1302 $cons_room_nights_sorted = [];
1303 foreach ($cons_room_nights as $day_key => $cons_nights) {
1304 $cons_room_nights_cnt = [];
1305 foreach ($cons_nights as $rid => $cons_dates) {
1306 $cons_room_nights_cnt[$rid] = count($cons_dates);
1307 }
1308 // sort the array in a descending order
1309 arsort($cons_room_nights_cnt);
1310 // restore sorted values in cloned array
1311 $cons_room_nights_sorted[$day_key] = [];
1312 foreach ($cons_room_nights_cnt as $rid => $tot_cons_nights) {
1313 $cons_room_nights_sorted[$day_key][$rid] = $cons_room_nights[$day_key][$rid];
1314 }
1315 }
1316 $cons_room_nights = $cons_room_nights_sorted;
1317
1318 // validate the data just built
1319 $first_day_key = date('Y-m-d', $groupdays[0]);
1320 if (!isset($cons_room_nights[$first_day_key]) || !count($cons_room_nights[$first_day_key]) || count($cons_room_nights) != count($groupdays)) {
1321 // unable to proceed
1322 return [];
1323 }
1324
1325 // remove the consecutive nights from the check-out date as this won't be a night of stay
1326 unset($cons_room_nights[$final_checkout_ymd]);
1327
1328 // build the split stay solutions
1329 $split_stay_sols = [];
1330
1331 // the number of rooms available on the first night should determine the number of split stay solutions
1332 foreach ($cons_room_nights[$first_day_key] as $start_rid => $cons_nights) {
1333 // start container of the various splits for this stay
1334 $split_stay_sol = [];
1335
1336 // calculate last consecutive night available
1337 $leave_date = end($cons_nights);
1338 $leave_date_info = getdate(strtotime($leave_date));
1339
1340 // set the check-out date to the day after the last night
1341 $checkout_ymd = date('Y-m-d', mktime(0, 0, 0, $leave_date_info['mon'], ($leave_date_info['mday'] + 1), $leave_date_info['year']));
1342
1343 // define the first stay
1344 $split_stay = [
1345 'idroom' => $start_rid,
1346 'room_name' => $all_rooms[$start_rid]['name'],
1347 'checkin' => $cons_nights[0],
1348 'checkout' => $checkout_ymd,
1349 'nights' => $this->countNightsOfStay(strtotime($cons_nights[0]), strtotime($checkout_ymd)),
1350 ];
1351
1352 // make sure this room has got a tariff defined for this number of nights of stay
1353 if (!$this->roomNightsAllowed($start_rid, $split_stay['nights'])) {
1354 // no tariffs found for this los
1355 continue;
1356 }
1357
1358 // push first stay
1359 $split_stay_sol[] = $split_stay;
1360
1361 // loop through the next stays
1362 while (isset($cons_room_nights[$checkout_ymd])) {
1363 /**
1364 * For the next splits, we use just the first available rooms, which is
1365 * the one with the highest number of consecutive nights available.
1366 */
1367 foreach ($cons_room_nights[$checkout_ymd] as $rid => $split_cons_nights) {
1368 // calculate last consecutive night available
1369 $leave_date = end($split_cons_nights);
1370 $leave_date_info = getdate(strtotime($leave_date));
1371
1372 // set the check-out date to the day after the last night
1373 $checkout_ymd = date('Y-m-d', mktime(0, 0, 0, $leave_date_info['mon'], ($leave_date_info['mday'] + 1), $leave_date_info['year']));
1374 if ($leave_date == $final_checkout_ymd) {
1375 // check-out date reached
1376 $checkout_ymd = $final_checkout_ymd;
1377 }
1378
1379 // count nights of stay
1380 $split_nights = $this->countNightsOfStay(strtotime($split_cons_nights[0]), strtotime($checkout_ymd));
1381
1382 // make sure this room has got a tariff defined for this number of nights of stay
1383 if (!$this->roomNightsAllowed($rid, $split_nights)) {
1384 // no tariffs found for this los, abort solution
1385 $split_stay_sol = [];
1386 break 2;
1387 }
1388
1389 // push split stay
1390 $split_stay_sol[] = [
1391 'idroom' => $rid,
1392 'room_name' => $all_rooms[$rid]['name'],
1393 'checkin' => $split_cons_nights[0],
1394 'checkout' => $checkout_ymd,
1395 'nights' => $split_nights,
1396 ];
1397
1398 // we try to reduce the number of splits by considering just the first room
1399 break;
1400 }
1401 }
1402
1403 if (count($split_stay_sol) < 2) {
1404 // not a split stay, but rather a fully available room
1405 continue;
1406 }
1407
1408 // push split stay solution
1409 $split_stay_sols[] = $split_stay_sol;
1410 }
1411
1412 /**
1413 * Load rooms involved in all split stays in order to validate
1414 * global/room-level restrictions and closing dates on the stay.
1415 */
1416 $rooms_involved = [];
1417 foreach ($split_stay_sols as $split_stay_sol) {
1418 foreach ($split_stay_sol as $split_stay) {
1419 if (!in_array($split_stay['idroom'], $rooms_involved)) {
1420 $rooms_involved[] = $split_stay['idroom'];
1421 }
1422 }
1423 }
1424
1425 // load restrictions for all rooms involved
1426 $all_restrictions = VikBooking::loadRestrictions(true, $rooms_involved);
1427 $glob_restrictions = VikBooking::globalRestrictions($all_restrictions);
1428 $invalid_room_restr = [];
1429
1430 // validate global restrictions
1431 if (VikBooking::validateRoomRestriction($glob_restrictions, $info_from, $info_to, $tot_nights)) {
1432 // global restrictions apply over this stay
1433 return [];
1434 }
1435
1436 // validate closing dates
1437 if (VikBooking::validateClosingDates($orig_checkin_ts, $orig_checkout_ts)) {
1438 // global closing dates apply over this stay
1439 return [];
1440 }
1441
1442 // validate restrictions at room level
1443 foreach ($rooms_involved as $rid) {
1444 // load restrictions at room level
1445 $room_level_restr = VikBooking::roomRestrictions($rid, $all_restrictions);
1446 if (VikBooking::validateRoomRestriction($room_level_restr, $info_from, $info_to, $tot_nights)) {
1447 // room-level restrictions apply over this stay
1448 $invalid_room_restr[] = $rid;
1449 }
1450 }
1451
1452 // unset the split stays with the restricted rooms (if any)
1453 $altered_sols = false;
1454 foreach ($invalid_room_restr as $rid) {
1455 foreach ($split_stay_sols as $k => $split_stay_sol) {
1456 foreach ($split_stay_sol as $split_stay) {
1457 if ($rid == $split_stay['idroom']) {
1458 // this booking split stay cannot be suggested because of this room
1459 unset($split_stay_sols[$k]);
1460 $altered_sols = true;
1461 continue 2;
1462 }
1463 }
1464 }
1465 }
1466
1467 // apply nights/transfers ratio limit (unless disabled)
1468 $nights_transfers_ratio = $this->getNightsTransfersRatio();
1469 if ($nights_transfers_ratio > 0 && $nights_transfers_ratio < 100) {
1470 // count and apply limits
1471 foreach ($split_stay_sols as $k => $split_stay_sol) {
1472 // count nights and transfers
1473 $split_stay_transfers = count($split_stay_sol) - 1;
1474 $split_stay_nights = 0;
1475 foreach ($split_stay_sol as $split_stay_room) {
1476 $split_stay_nights += $split_stay_room['nights'];
1477 }
1478 // max allowed transfers
1479 $max_transfers = round($split_stay_nights * $nights_transfers_ratio / 100, 0);
1480 if (!$split_stay_transfers || $split_stay_transfers > $max_transfers) {
1481 // unset solution
1482 unset($split_stay_sols[$k]);
1483 $altered_sols = true;
1484 }
1485 }
1486 } else {
1487 // split stays have been disabled
1488 $split_stay_sols = [];
1489 }
1490
1491 if ($altered_sols && count($split_stay_sols)) {
1492 // restore the array keys
1493 $split_stay_sols = array_values($split_stay_sols);
1494 }
1495
1496 // return the available booking split stay solutions (if any)
1497 return $split_stay_sols;
1498 }
1499
1500 /**
1501 * Returns the number of guests requested from the given room-party index.
1502 *
1503 * @param string $guest either "adults", "children" or "guests".
1504 * @param int $party the party index number, 0 by default.
1505 *
1506 * @return int the total number of guests requested in the party.
1507 */
1508 protected function getPartyGuests($guest = 'adults', $party = 0)
1509 {
1510 if (!isset($this->room_parties[$party])) {
1511 return 0;
1512 }
1513
1514 if (!strcasecmp($guest, 'adults')) {
1515 // adults
1516 return $this->room_parties[$party]['adults'];
1517 }
1518
1519 if (!strcasecmp($guest, 'children')) {
1520 // children
1521 return $this->room_parties[$party]['children'];
1522 }
1523
1524 // total guests
1525 $tot_guests = 0;
1526 foreach ($this->room_parties as $rparty) {
1527 $tot_guests += $rparty['adults'];
1528 $tot_guests += $rparty['children'];
1529 }
1530
1531 return $tot_guests;
1532 }
1533
1534 /**
1535 * Given a list of unavailable room IDs, yet compatible with the party and LOS requested,
1536 * we build a list of available dates when such rooms could be booked for the same LOS.
1537 *
1538 * @param array $room_ids list of unavailable, yet compatible, room IDs.
1539 *
1540 * @return array associative list of available room-dates, or empty array.
1541 */
1542 protected function findClosestRoomDateSolutions($room_ids = [])
1543 {
1544 if (!$room_ids) {
1545 return [];
1546 }
1547
1548 // get all website rooms
1549 $all_rooms = $this->loadRooms();
1550
1551 // get original check-in and check-out timestamps
1552 list($orig_checkin_ts, $orig_checkout_ts) = $this->getStayDates(true);
1553 $info_from = getdate($orig_checkin_ts);
1554 $info_to = getdate($orig_checkout_ts);
1555
1556 // count original length of stay
1557 $tot_nights = $this->countNightsOfStay();
1558
1559 // earliest checkin timestamp allowed
1560 $lim_past_ts = mktime(0, 0, 0, date('n'), ((int)date('j') + VikBooking::getMinDaysAdvance()), date('Y'));
1561
1562 // suggested range of dates (+/- "back and forth" days from original dates)
1563 $sug_from_ts = mktime($info_from['hours'], $info_from['minutes'], $info_from['seconds'], $info_from['mon'], ($info_from['mday'] - $this->back_and_forth), $info_from['year']);
1564 if ($sug_from_ts < $lim_past_ts) {
1565 $sug_from_ts = $lim_past_ts;
1566 // since we are close to the requested check-in, double up the "back and forth" for the max date
1567 $this->setBackForthDays($this->getBackForthDays() * 2);
1568 }
1569 $sug_to_ts = mktime($info_to['hours'], $info_to['minutes'], $info_to['seconds'], $info_to['mon'], ($info_to['mday'] + $this->back_and_forth), $info_to['year']);
1570 $sug_to_ts = $sug_to_ts < $sug_from_ts ? $sug_from_ts : $sug_to_ts;
1571
1572 // get days timestamps for suggestions
1573 $groupdays = [];
1574 $sug_start_info = getdate($sug_from_ts);
1575 $sug_from_midnight = mktime(0, 0, 0, $sug_start_info['mon'], $sug_start_info['mday'], $sug_start_info['year']);
1576 $sug_start_info = getdate($sug_from_midnight);
1577 while ($sug_start_info[0] <= $sug_to_ts) {
1578 array_push($groupdays, $sug_start_info[0]);
1579 $sug_start_info = getdate(mktime(0, 0, 0, $sug_start_info['mon'], ($sug_start_info['mday'] + 1), $sug_start_info['year']));
1580 }
1581
1582 // build suggestions array of dates with some availability for the given rooms
1583 $suggestions = [];
1584 $busy_records = VikBooking::loadBusyRecords($room_ids, $sug_from_ts, strtotime('+1 day', $sug_to_ts));
1585 foreach ($room_ids as $rid) {
1586 if (!isset($all_rooms[$rid])) {
1587 continue;
1588 }
1589 $room = $all_rooms[$rid];
1590 foreach ($groupdays as $gday) {
1591 $day_key = date('Y-m-d', $gday);
1592 $bfound = 0;
1593 if (!isset($busy_records[$rid])) {
1594 $busy_records[$rid] = [];
1595 }
1596 foreach ($busy_records[$rid] as $bu) {
1597 $busy_info_in = getdate($bu['checkin']);
1598 $busy_info_out = getdate($bu['checkout']);
1599 $busy_in_ts = mktime(0, 0, 0, $busy_info_in['mon'], $busy_info_in['mday'], $busy_info_in['year']);
1600 $busy_out_ts = mktime(0, 0, 0, $busy_info_out['mon'], $busy_info_out['mday'], $busy_info_out['year']);
1601 if ($gday >= $busy_in_ts && $gday == $busy_out_ts && !$this->inonout_allowed && $room['units'] < 2) {
1602 // check-ins on check-outs not allowed
1603 $bfound++;
1604 if ($bfound >= $room['units']) {
1605 break;
1606 }
1607 }
1608 if ($gday >= $busy_in_ts && $gday < $busy_out_ts) {
1609 $bfound++;
1610 if ($bfound >= $room['units']) {
1611 break;
1612 }
1613 }
1614 }
1615 if ($bfound < $room['units']) {
1616 if (!isset($suggestions[$day_key])) {
1617 $suggestions[$day_key] = [];
1618 }
1619 $room_day = $room;
1620 $room_day['units_left'] = $room['units'] - $bfound;
1621 $suggestions[$day_key] = $suggestions[$day_key] + [$rid => $room_day];
1622 }
1623 }
1624 }
1625
1626 if (!$suggestions) {
1627 // no available nights found for the prior and next "back and forth" days for the given rooms
1628 return [];
1629 }
1630
1631 // build the solutions array with keys=checkin, values=all rooms suited for the requested number of nights
1632 $solutions = [];
1633 // get all rooms available for the number of nights requested in the suggestions array of dates
1634 foreach ($suggestions as $kday => $rooms) {
1635 $day_ts_info = getdate(strtotime($kday));
1636 foreach ($rooms as $rid => $room) {
1637 $suitable = true;
1638 $room_days_av_left = [$kday => $room['units_left']];
1639 for ($i = 1; $i < $tot_nights; $i++) {
1640 $next_night = mktime(0, 0, 0, $day_ts_info['mon'], ($day_ts_info['mday'] + $i), $day_ts_info['year']);
1641 $next_night_dt = date('Y-m-d', $next_night);
1642 if (!isset($suggestions[$next_night_dt]) || !isset($suggestions[$next_night_dt][$rid])) {
1643 $suitable = false;
1644 break;
1645 }
1646 $room_days_av_left[$next_night_dt] = $suggestions[$next_night_dt][$rid]['units_left'];
1647 }
1648 if ($suitable === true) {
1649 if (!isset($solutions[$kday])) {
1650 $solutions[$kday] = [];
1651 }
1652 unset($room['units_left']);
1653 $room['days_av_left'] = $room_days_av_left;
1654 $solutions[$kday] = $solutions[$kday] + [$rid => $room];
1655 }
1656 }
1657 }
1658
1659 if (!count($solutions)) {
1660 // the requested length of stay could not be satisfied for any available night
1661 return [];
1662 }
1663
1664 // sort the solutions by the closest checkin date to the one requested
1665 $sortmap = [];
1666 $orig_checkin_ymd = date('Y-m-d', $orig_checkin_ts);
1667 foreach ($solutions as $kday => $solution) {
1668 $kdayts = strtotime($kday);
1669 $sortmap[$kdayts] = $kdayts > $orig_checkin_ts ? ($kdayts - $orig_checkin_ts) : ($orig_checkin_ts - $kdayts);
1670 if ($orig_checkin_ymd == $kday) {
1671 // the original check-in day is available, so we want it to be first, regardless of the check-in time
1672 $sortmap[$kdayts] = 1;
1673 }
1674 }
1675 asort($sortmap);
1676 $sorted = [];
1677 foreach ($sortmap as $kdayts => $v) {
1678 $kday = date('Y-m-d', $kdayts);
1679 $sorted[$kday] = $solutions[$kday];
1680 }
1681 $solutions = $sorted;
1682 unset($sorted);
1683
1684 /**
1685 * Load rooms involved in the final alternative solutions in order
1686 * to validate global/room-level restrictions and closing dates.
1687 *
1688 * @since 1.15.4 (J) - 1.5.10 (WP)
1689 */
1690 $rooms_involved = [];
1691 foreach ($solutions as $arrive_ymd => $roomsol) {
1692 foreach (array_keys($roomsol) as $rid) {
1693 if (!in_array($rid, $rooms_involved)) {
1694 $rooms_involved[] = $rid;
1695 }
1696 }
1697 }
1698
1699 // load restrictions for all rooms involved
1700 $all_restrictions = VikBooking::loadRestrictions(true, $rooms_involved);
1701 $glob_restrictions = VikBooking::globalRestrictions($all_restrictions);
1702 $room_level_restr = [];
1703
1704 foreach ($solutions as $arrive_ymd => $roomsol) {
1705 // build suggested stay dates
1706 $sug_in = getdate(strtotime($arrive_ymd));
1707 $sug_out = getdate(mktime(0, 0, 0, $sug_in['mon'], ($sug_in['mday'] + $tot_nights), $sug_in['year']));
1708 // validate global restrictions
1709 if (VikBooking::validateRoomRestriction($glob_restrictions, $sug_in, $sug_out, $tot_nights)) {
1710 // global restrictions apply over this stay
1711 unset($solutions[$arrive_ymd]);
1712 continue;
1713 }
1714 // validate closing dates
1715 if (VikBooking::validateClosingDates($sug_in[0], $sug_out[0])) {
1716 // global closing dates apply over this stay
1717 unset($solutions[$arrive_ymd]);
1718 continue;
1719 }
1720 // validate restrictions at room level
1721 foreach ($roomsol as $rid => $rdata) {
1722 if (!isset($room_level_restr[$rid])) {
1723 // load restrictions at room level
1724 $room_level_restr[$rid] = VikBooking::roomRestrictions($rid, $all_restrictions);
1725 }
1726 if (VikBooking::validateRoomRestriction($room_level_restr[$rid], $sug_in, $sug_out, $tot_nights)) {
1727 // room-level restrictions apply over this stay
1728 unset($solutions[$arrive_ymd][$rid]);
1729 if (!count($solutions[$arrive_ymd])) {
1730 // unset the entire suggested date
1731 unset($solutions[$arrive_ymd]);
1732 break;
1733 }
1734 continue;
1735 }
1736 }
1737 }
1738
1739 if (!count($solutions)) {
1740 // the calculated suggestions do not meet the restrictions or the closing dates
1741 return [];
1742 }
1743
1744 // return the solution alternative dates for all rooms available
1745 return $solutions;
1746 }
1747
1748 /**
1749 * Sorts an associative array of room-solutions by the bigger rooms.
1750 *
1751 * @param array $solutions the date solutions obtained for some rooms.
1752 *
1753 * @return array the same array sorted by bigger rooms on top.
1754 */
1755 protected function sortBiggerRoomSolutions($solutions)
1756 {
1757 if (!is_array($solutions) || !$solutions) {
1758 return $solutions;
1759 }
1760
1761 // sort rooms-solutions by max-adults, 'max-guests', 'max-children', in a descending order
1762 foreach ($solutions as $kday => $solution) {
1763 // with this sorting, we will have the bigger rooms on top to quickly fit the party requested
1764 uasort($solutions[$kday], function($a, $b) {
1765 if ($a['toadult'] == $b['toadult']) {
1766 if ($a['totpeople'] == $b['totpeople']) {
1767 return $a['tochild'] > $b['tochild'] ? -1 : 1;
1768 }
1769 return $a['totpeople'] > $b['totpeople'] ? -1 : 1;
1770 }
1771 return $a['toadult'] > $b['toadult'] ? -1 : 1;
1772 });
1773 }
1774
1775 return $solutions;
1776 }
1777
1778 /**
1779 * Given a list of available dates and rooms (solutions), attempts
1780 * to match a party of rooms that fits the party requested.
1781 *
1782 * @param array $solutions the list of available dates and related rooms.
1783 *
1784 * @return array list of alternative party solutions, if any.
1785 */
1786 protected function matchSolutionsParty($solutions)
1787 {
1788 if (!is_array($solutions) || !$solutions) {
1789 return [];
1790 }
1791
1792 // build the list of alternative parties
1793 $alternative_parties = [];
1794
1795 // build list of party guests
1796 $party_guests = [
1797 'adults' => 0,
1798 'children' => 0,
1799 ];
1800 foreach ($this->room_parties as $rparty) {
1801 $party_guests['adults'] += $rparty['adults'];
1802 $party_guests['children'] += $rparty['children'];
1803 }
1804
1805 // check if the rooms of each solution can fit the number of guests requested, unset the solution otherwise
1806 foreach ($solutions as $kday => $solution) {
1807 $solution_guests = [
1808 'adults' => 0,
1809 'children' => 0,
1810 ];
1811 foreach ($solution as $rid => $roomsol) {
1812 // count minimum units left for this room
1813 $room_min_uleft = min($roomsol['days_av_left']);
1814 // check if this solution of rooms can allocate all the guests requested
1815 if ($roomsol['totpeople'] < ($roomsol['toadult'] + $roomsol['tochild']) && !$party_guests['children']) {
1816 // in case of no children requested, we ignore them to avoid adjusting the room capacity
1817 $roomsol['tochild'] = 0;
1818 }
1819 if ($roomsol['totpeople'] < ($roomsol['toadult'] + $roomsol['tochild'])) {
1820 /**
1821 * The sum of the max_adults and max_children exceeds the max_guests: lower the adults
1822 * we can take first (if party children > 0), then the children, until sum=max_guests
1823 */
1824 while (($roomsol['toadult'] > 0 || $roomsol['tochild'] > 0)) {
1825 if (!$party_guests['children'] && $roomsol['totpeople'] == $roomsol['toadult']) {
1826 /**
1827 * When no children requested in the party, we cannot under-utilize rooms.
1828 * Break the loop without lowering the 'toadult'.
1829 */
1830 $roomsol['tochild'] = 0;
1831 break;
1832 }
1833 if ($party_guests['children'] && $solution_guests['children'] >= $party_guests['children']) {
1834 /**
1835 * If all the children requested were allocated in other solutions,
1836 * we should not under-utilize rooms by reducing the number of adults.
1837 */
1838 break;
1839 }
1840 if ($roomsol['toadult'] > 0 && $party_guests['children'] > 0 && !($roomsol['tochild'] > $party_guests['children'])) {
1841 /**
1842 * We lower first the adults that we put in this room, only if there are children in the party
1843 * and if the children in the party are more than the 'max_children' of this room.
1844 */
1845 $roomsol['toadult']--;
1846 if ($roomsol['totpeople'] >= ($roomsol['toadult'] + $roomsol['tochild'])) {
1847 break;
1848 }
1849 }
1850 if ($roomsol['tochild'] > 0) {
1851 // if the max_guests is still greater than the sum of adults+children we take, take out one child
1852 $roomsol['tochild']--;
1853 if ($roomsol['totpeople'] >= ($roomsol['toadult'] + $roomsol['tochild'])) {
1854 break;
1855 }
1856 }
1857 if ($roomsol['toadult'] > 0) {
1858 // if even at this point we still have a high sum of guests to take compared to the max_guests, take out again one adult
1859 $roomsol['toadult']--;
1860 if ($roomsol['totpeople'] >= ($roomsol['toadult'] + $roomsol['tochild'])) {
1861 break;
1862 }
1863 }
1864 }
1865 }
1866 $solution_guests['adults'] += $roomsol['toadult'] * $room_min_uleft;
1867 $solution_guests['children'] += $roomsol['tochild'] * $room_min_uleft;
1868 // update 'max_adults' and 'max_children' for this solution (for later guests allocation)
1869 $solution[$rid]['toadult'] = $roomsol['toadult'];
1870 $solution[$rid]['tochild'] = $roomsol['tochild'];
1871 }
1872
1873 $solutions[$kday] = $solution;
1874 if ($solution_guests['adults'] < $party_guests['adults'] || $solution_guests['children'] < $party_guests['children']) {
1875 // the guests we can allocate with the solution of this day are not enough: unset the solution
1876 unset($solutions[$kday]);
1877 continue;
1878 }
1879
1880 // if we get to this point we can suggest a booking solution for the party requested, but in different rooms
1881 if (!isset($alternative_parties[$kday])) {
1882 $alternative_parties[$kday] = [];
1883 }
1884
1885 // re-loop over the rooms in this solution to build the booking solution for this day
1886 $guests_allocated = [
1887 'adults' => 0,
1888 'children' => 0
1889 ];
1890
1891 /**
1892 * The rooms available for an alternative booking solutions have been sorted by capacity
1893 * in a descending order to quickly fit the guest party requested. However, if a smaller
1894 * and cheaper room was capable of fitting all guests, we should opt for this solution.
1895 *
1896 * @since 1.15.4 (J) - 1.5.9 (WP)
1897 */
1898 $smaller_fit_found = false;
1899 $smaller_solutions = array_reverse($solution, true);
1900 foreach ($smaller_solutions as $rid => $roomsol) {
1901 if ($party_guests['adults'] > 0 && $party_guests['adults'] > $roomsol['toadult']) {
1902 // too many adults requested for this small room
1903 continue;
1904 }
1905 if ($party_guests['children'] > 0 && $party_guests['children'] > $roomsol['tochild']) {
1906 // too many children requested for this small room
1907 continue;
1908 }
1909 if (($party_guests['adults'] + $party_guests['children']) > $roomsol['totpeople']) {
1910 // too many guests requested for this small room
1911 continue;
1912 }
1913 // we've got a fitting room which could be smaller
1914 $roomsol['guests_allocation'] = [
1915 'adults' => $party_guests['adults'],
1916 'children' => $party_guests['children'],
1917 ];
1918 array_push($alternative_parties[$kday], $roomsol);
1919 // turn flag on and break the loop
1920 $smaller_fit_found = true;
1921 break;
1922 }
1923 if ($smaller_fit_found) {
1924 // no need to parse the rooms from the largest to the smallest
1925 continue;
1926 }
1927
1928 foreach ($solution as $rid => $roomsol) {
1929 // count minimum units left for this room
1930 $room_min_uleft = min($roomsol['days_av_left']);
1931 // fullfil all the units of this room
1932 for ($units_taken = 0; $units_taken < $room_min_uleft; $units_taken++) {
1933 $current_allocation = [
1934 'adults' => 0,
1935 'children' => 0
1936 ];
1937 if ($guests_allocated['adults'] < $party_guests['adults']) {
1938 $humans_taken = $roomsol['toadult'];
1939 $missing_humans = $party_guests['adults'] - $guests_allocated['adults'];
1940 $humans_taken = $humans_taken > $missing_humans ? $missing_humans : $humans_taken;
1941
1942 $current_allocation['adults'] = $humans_taken;
1943 $guests_allocated['adults'] += $humans_taken;
1944 }
1945 if ($guests_allocated['children'] < $party_guests['children']) {
1946 $humans_taken = $roomsol['tochild'];
1947 $missing_humans = $party_guests['children'] - $guests_allocated['children'];
1948 $humans_taken = $humans_taken > $missing_humans ? $missing_humans : $humans_taken;
1949
1950 $current_allocation['children'] = $humans_taken;
1951 $guests_allocated['children'] += $humans_taken;
1952 }
1953 $roomsol['guests_allocation'] = $current_allocation;
1954 array_push($alternative_parties[$kday], $roomsol);
1955 if ($guests_allocated['adults'] >= $party_guests['adults'] && $guests_allocated['children'] >= $party_guests['children']) {
1956 // we have allocated all guests, exit the for-loop
1957 break;
1958 }
1959 }
1960 if ($guests_allocated['adults'] >= $party_guests['adults'] && $guests_allocated['children'] >= $party_guests['children']) {
1961 //we have allocated all guests with this solution, no need to loop over other rooms available in this day.
1962 break;
1963 }
1964 }
1965 }
1966
1967 // return the alternative parties found, if any
1968 return $alternative_parties;
1969 }
1970
1971 /**
1972 * Given a list of alternative dates obtained from an inquiry/request information,
1973 * composes a valid room-rate array to store the inquiry reservation. By calling this
1974 * method, the original stay dates will be overwritten.
1975 *
1976 * @param array $alt_dates the list of alternative dates found for the stay.
1977 * @param object $customer a stdClass object with the basic customer details.
1978 *
1979 * @return int the ID of the newly created inquiry reservation.
1980 */
1981 public function allocateAltDatesInquiry($alt_dates, $customer)
1982 {
1983 if (!is_array($alt_dates) || !$alt_dates) {
1984 return 0;
1985 }
1986
1987 foreach ($alt_dates as $ymd => $rooms) {
1988 // we expect just one room-type for the party, and we use the first suggestion
1989 foreach ($rooms as $rid => $alt_stay) {
1990 if (empty($alt_stay['days_av_left']) || !is_array($alt_stay['days_av_left'])) {
1991 // invalid structure
1992 continue;
1993 }
1994 // compose the new stay dates
1995 $sugg_checkin_dt = null;
1996 $sugg_checkout_dt = null;
1997 foreach ($alt_stay['days_av_left'] as $dayk => $uleft) {
1998 if (empty($sugg_checkin_dt)) {
1999 // grab the first date
2000 $sugg_checkin_dt = $dayk;
2001 }
2002 // always overwrite until last date
2003 $sugg_checkout_dt = $dayk;
2004 }
2005 // increase check-out date by one day (day after last night of stay)
2006 $sugg_out_info = getdate(strtotime($sugg_checkout_dt));
2007 $sugg_checkout_dt = date('Y-m-d', mktime(0, 0, 0, $sugg_out_info['mon'], ($sugg_out_info['mday'] + 1), $sugg_out_info['year']));
2008
2009 // set the new stay dates
2010 $this->setStayDates($sugg_checkin_dt, $sugg_checkout_dt);
2011
2012 // build the room rate plan array without any rate plan information
2013 $room_rplan = [
2014 'idroom' => $alt_stay['id'],
2015 ];
2016
2017 // create the inquiry reservation for the closest alternative dates
2018 return $this->createInquiryReservation($room_rplan, $customer);
2019 }
2020 }
2021
2022 return 0;
2023 }
2024
2025 /**
2026 * Given a list of alternative parties obtained from an inquiry/request information,
2027 * composes valid room-rate arrays to store the inquiry reservation. By calling this
2028 * method, the original stay dates and room party will be overwritten.
2029 *
2030 * @param array $alt_parties the list of alternative parties found for the stay.
2031 * @param object $customer a stdClass object with the basic customer details.
2032 *
2033 * @return int the ID of the newly created inquiry reservation.
2034 */
2035 public function allocateAltPartyInquiry($alt_parties, $customer)
2036 {
2037 if (!is_array($alt_parties) || !$alt_parties) {
2038 return 0;
2039 }
2040
2041 // build list of rooms to assign to the inquiry reservation
2042 $room_rates = [];
2043
2044 // start party counter
2045 $party_counter = 0;
2046
2047 foreach ($alt_parties as $ymd => $alt_rooms) {
2048 // we expect to have more than one room-type for the large party suggestion
2049 foreach ($alt_rooms as $alt_room) {
2050 if (empty($alt_room['guests_allocation']) || !is_array($alt_room['guests_allocation'])) {
2051 // invalid structure
2052 continue;
2053 }
2054 if (empty($alt_room['days_av_left']) || !is_array($alt_room['days_av_left'])) {
2055 // invalid structure
2056 continue;
2057 }
2058 // compose the new stay dates
2059 $sugg_checkin_dt = null;
2060 $sugg_checkout_dt = null;
2061 foreach ($alt_room['days_av_left'] as $dayk => $uleft) {
2062 if (empty($sugg_checkin_dt)) {
2063 // grab the first date
2064 $sugg_checkin_dt = $dayk;
2065 }
2066 // always overwrite until last date
2067 $sugg_checkout_dt = $dayk;
2068 }
2069 // increase check-out date by one day (day after last night of stay)
2070 $sugg_out_info = getdate(strtotime($sugg_checkout_dt));
2071 $sugg_checkout_dt = date('Y-m-d', mktime(0, 0, 0, $sugg_out_info['mon'], ($sugg_out_info['mday'] + 1), $sugg_out_info['year']));
2072
2073 // set the new stay dates
2074 $this->setStayDates($sugg_checkin_dt, $sugg_checkout_dt);
2075
2076 // set the current guests party (the first will replace the previous party, others will be pushed)
2077 $this->setRoomParty($alt_room['guests_allocation']['adults'], $alt_room['guests_allocation']['children'], ($party_counter === 0));
2078
2079 // increase party counter
2080 $party_counter++;
2081
2082 // push current room with no rate plan information
2083 array_push($room_rates, [
2084 'idroom' => $alt_room['id'],
2085 ]);
2086 }
2087
2088 if (count($room_rates)) {
2089 // we use the closest dates in the first suggestion party array
2090 break;
2091 }
2092 }
2093
2094 // count total rooms in the party
2095 $tot_room_party = count($room_rates);
2096
2097 if (!$tot_room_party) {
2098 // something went wrong
2099 return 0;
2100 }
2101
2102 // grab the main/first room reservation
2103 $room_rplan = $room_rates[0];
2104
2105 // build extra rooms
2106 $extra_rooms = [];
2107 if ($tot_room_party > 1) {
2108 // grab the remaining rooms
2109 unset($room_rates[0]);
2110 $extra_rooms = array_values($room_rates);
2111 }
2112
2113 // create the inquiry reservation for the closest alternative dates and rooms party
2114 return $this->createInquiryReservation($room_rplan, $customer, $extra_rooms);
2115 }
2116
2117 /**
2118 * Creates a new pending reservation from the inquiry/request information.
2119 * Requires a valid room-rate array to be available, or in case suggestions should
2120 * be applied, the room-rate array should be adjusted to comply with this method.
2121 *
2122 * @param array $room_rplan a room-rate array to allocate the booking.
2123 * @param object $customer a stdClass object with the basic customer details.
2124 * @param array $extra_rooms optional list of additional room-rate arrays to store
2125 * in case of alternative parties suggested.
2126 *
2127 * @return int the ID of the newly created inquiry reservation.
2128 */
2129 public function createInquiryReservation($room_rplan, $customer, $extra_rooms = array())
2130 {
2131 if (!is_array($room_rplan) || empty($room_rplan['idroom'])) {
2132 return 0;
2133 }
2134
2135 if (empty($this->stay_ts) || empty($this->room_parties)) {
2136 // no stay dates or room party set
2137 return 0;
2138 }
2139
2140 $dbo = JFactory::getDbo();
2141
2142 // build reservation object
2143 $res_obj = new stdClass;
2144 $res_obj->custdata = $customer->custdata;
2145 $res_obj->ts = time();
2146 $res_obj->status = 'standby';
2147 $res_obj->days = $this->countNightsOfStay();
2148 $res_obj->checkin = $this->stay_ts[0];
2149 $res_obj->checkout = $this->stay_ts[1];
2150 $res_obj->custmail = $customer->email;
2151 $res_obj->sid = VikBooking::getSecretLink();
2152 $res_obj->idpayment = $this->getDefaultPaymentId();
2153 $res_obj->roomsnum = count($this->room_parties);
2154 if (!empty($room_rplan['cost'])) {
2155 $res_obj->total = (float)$room_rplan['cost'];
2156 }
2157 $res_obj->adminnotes = $customer->adminnotes;
2158 $res_obj->lang = $customer->lang;
2159 $res_obj->country = $customer->country;
2160 if (!empty($room_rplan['taxes'])) {
2161 $res_obj->tot_taxes = (float)$room_rplan['taxes'];
2162 }
2163 if (!empty($room_rplan['city_taxes'])) {
2164 $res_obj->tot_city_taxes = (float)$room_rplan['city_taxes'];
2165 }
2166 $res_obj->phone = $customer->phone;
2167 $res_obj->type = 'inquiry';
2168
2169 // store record
2170 if (!$dbo->insertObject('#__vikbooking_orders', $res_obj, 'id')) {
2171 // could not store the booking record
2172 return 0;
2173 }
2174
2175 // get the ID of the newly created reservation
2176 $res_id = $res_obj->id;
2177
2178 // check if mandatory options should be assigned (not in case of suggestions)
2179 $room_options = null;
2180 if (!empty($room_rplan['idprice']) && isset($res_obj->tot_city_taxes)) {
2181 $mand_taxes = VikBooking::getMandatoryTaxesFees([$room_rplan['idroom']], $this->getPartyGuests('adults', 0), $res_obj->days);
2182 if (is_array($mand_taxes) && !empty($mand_taxes['options'])) {
2183 $room_options = implode(';', $mand_taxes['options']);
2184 }
2185 }
2186
2187 // build room-reservation object
2188 $room_res_obj = new stdClass;
2189 $room_res_obj->idorder = $res_id;
2190 $room_res_obj->idroom = $room_rplan['idroom'];
2191 $room_res_obj->adults = $this->getPartyGuests('adults', 0);
2192 $room_res_obj->children = $this->getPartyGuests('children', 0);
2193 if (!empty($room_rplan['idprice'])) {
2194 $room_res_obj->idtar = $this->getTariffId($room_rplan['idroom'], $room_rplan['idprice'], $res_obj->days);
2195 }
2196 $room_res_obj->optionals = $room_options;
2197 $room_res_obj->t_first_name = $customer->name;
2198 $room_res_obj->t_last_name = $customer->lname;
2199
2200 // store record
2201 if (!$dbo->insertObject('#__vikbooking_ordersrooms', $room_res_obj, 'id')) {
2202 // could not store the room-reservation record
2203 return $res_id;
2204 }
2205
2206 // in case of suggestions for alternative room parties, parse the extra rooms
2207 $party_index = 1;
2208 foreach ($extra_rooms as $extra_room) {
2209 if (!is_array($extra_room) || empty($extra_room['idroom'])) {
2210 continue;
2211 }
2212 // build additional room-reservation object
2213 $room_res_obj = new stdClass;
2214 $room_res_obj->idorder = $res_id;
2215 $room_res_obj->idroom = $extra_room['idroom'];
2216 $room_res_obj->adults = $this->getPartyGuests('adults', $party_index);
2217 $room_res_obj->children = $this->getPartyGuests('children', $party_index);
2218
2219 // store record
2220 $dbo->insertObject('#__vikbooking_ordersrooms', $room_res_obj, 'id');
2221
2222 // increase room party index
2223 $party_index++;
2224 }
2225
2226 // return the newly created reservation ID
2227 return $res_id;
2228 }
2229
2230 /**
2231 * Attempts to get the tariff ID for the given room, rate plan and nights.
2232 *
2233 * @param int $room_id the ID of the VBO room.
2234 * @param int $rplan_id the rate plan ID in VBO.
2235 * @param int $nights the number of nights of stay.
2236 *
2237 * @return int|null the tariff ID or null.
2238 */
2239 public function getTariffId($room_id, $rplan_id, $nights)
2240 {
2241 if (empty($room_id) || empty($rplan_id) || $nights < 1) {
2242 return null;
2243 }
2244
2245 $dbo = JFactory::getDbo();
2246
2247 $q = "SELECT `id` FROM `#__vikbooking_dispcost` WHERE `idroom`={$room_id} AND `days`={$nights} AND `idprice`={$rplan_id}";
2248 $dbo->setQuery($q, 0, 1);
2249 $res = $dbo->loadResult();
2250
2251 if ($res) {
2252 return (int)$res;
2253 }
2254
2255 return null;
2256 }
2257
2258 /**
2259 * Grabs the details of a given booking id.
2260 *
2261 * @param int $bid the booking ID to look for.
2262 *
2263 * @return array empty array or booking record details.
2264 */
2265 public function getBookingDetails($bid)
2266 {
2267 $bid = (int)$bid;
2268
2269 $dbo = JFactory::getDbo();
2270
2271 $q = "SELECT `o`.*, `co`.`idcustomer`, CONCAT_WS(' ', `c`.`first_name`, `c`.`last_name`) AS `customer_fullname`, `c`.`country` AS `customer_country`, `c`.`pic`
2272 FROM `#__vikbooking_orders` AS `o`
2273 LEFT JOIN `#__vikbooking_customers_orders` AS `co` ON `co`.`idorder`=`o`.`id`
2274 LEFT JOIN `#__vikbooking_customers` AS `c` ON `c`.`id`=`co`.`idcustomer`
2275 WHERE `o`.`id`={$bid}";
2276 $dbo->setQuery($q, 0, 1);
2277 $row = $dbo->loadAssoc();
2278 if ($row) {
2279 return $row;
2280 }
2281
2282 return [];
2283 }
2284
2285 /**
2286 * Validates if the room allows the given number of nights of stay by checking if a
2287 * tariff is defined for the given length of stay. Useful for particular rate tables.
2288 *
2289 * @param int $room_id the ID of the VBO room.
2290 * @param int $nights the number of nights of stay.
2291 *
2292 * @return bool true if a tariff is found between min and max nights.
2293 *
2294 * @since 1.16.3 (J) - 1.6.3 (WP)
2295 */
2296 public function roomNightsAllowed($room_id, $nights)
2297 {
2298 if (!isset($this->min_max_los_tariffs_map[$room_id])) {
2299 $dbo = JFactory::getDbo();
2300
2301 $q = $dbo->getQuery(true)
2302 ->select('MIN(' . $dbo->qn('t.days') . ') AS ' . $dbo->qn('min_nights'))
2303 ->select('MAX(' . $dbo->qn('t.days') . ') AS ' . $dbo->qn('max_nights'))
2304 ->from($dbo->qn('#__vikbooking_dispcost', 't'))
2305 ->where($dbo->qn('t.idroom') . ' = ' . (int)$room_id);
2306
2307 $dbo->setQuery($q, 0, 1);
2308 $tariffs = $dbo->loadObject();
2309
2310 if (!$tariffs || !$tariffs->min_nights || !$tariffs->max_nights) {
2311 return false;
2312 }
2313
2314 // set values
2315 $this->min_max_los_tariffs_map[$room_id] = [$tariffs->min_nights, $tariffs->max_nights];
2316 }
2317
2318 // check if the number of nights of stay is within the range of tariffs los map
2319 return ($nights >= min($this->min_max_los_tariffs_map[$room_id]) && $nights <= max($this->min_max_los_tariffs_map[$room_id]));
2320 }
2321
2322 /**
2323 * Sets the stay dates, check-in and check-out date timestamps.
2324 *
2325 * @param string $from check-in date string in Y-m-d or VBO format.
2326 * @param string $to check-out date string in Y-m-d or VBO format.
2327 *
2328 * @return self
2329 */
2330 public function setStayDates($from, $to)
2331 {
2332 if (empty($from) || empty($to)) {
2333 return $this;
2334 }
2335
2336 $checkinh = 0;
2337 $checkinm = 0;
2338 $checkouth = 0;
2339 $checkoutm = 0;
2340 $timeopst = VikBooking::getTimeOpenStore();
2341 if (is_array($timeopst)) {
2342 if ($timeopst[0] < $timeopst[1]) {
2343 // check-in not allowed on a day where there is already a check out (no arrivals/depatures on the same day)
2344 $this->inonout_allowed = false;
2345 }
2346 $opent = VikBooking::getHoursMinutes($timeopst[0]);
2347 $closet = VikBooking::getHoursMinutes($timeopst[1]);
2348 $checkinh = $opent[0];
2349 $checkinm = $opent[1];
2350 $checkouth = $closet[0];
2351 $checkoutm = $closet[1];
2352 }
2353 $from_ts = VikBooking::getDateTimestamp($from, $checkinh, $checkinm);
2354 $to_ts = VikBooking::getDateTimestamp($to, $checkouth, $checkoutm);
2355
2356 // set stay dates and timestamps
2357 $this->stay_dates = [date('Y-m-d', $from_ts), date('Y-m-d', $to_ts)];
2358 $this->stay_ts = [$from_ts, $to_ts];
2359
2360 return $this;
2361 }
2362
2363 /**
2364 * Returns the current stay dates or timestamps.
2365 *
2366 * @param bool $ts whether to get the date timestamps.
2367 *
2368 * @return array the current stay dates or timestamps.
2369 */
2370 public function getStayDates($ts = false)
2371 {
2372 return $ts ? $this->stay_ts : $this->stay_dates;
2373 }
2374
2375 /**
2376 * Sets a room party with adults and children, by optionally replacing the others.
2377 *
2378 * @param int $adults the number of adults for this room party.
2379 * @param int $children the number of children for this room party.
2380 * @param bool $replace if true, any previously set room party will be replaced.
2381 *
2382 * @return self
2383 */
2384 public function setRoomParty($adults, $children = 0, $replace = false)
2385 {
2386 $room_party = [
2387 'adults' => $adults,
2388 'children' => $children,
2389 ];
2390
2391 if ($replace) {
2392 $this->room_parties = [$room_party];
2393 } else {
2394 array_push($this->room_parties, $room_party);
2395 }
2396
2397 return $this;
2398 }
2399
2400 /**
2401 * Returns the current room parties array.
2402 *
2403 * @return array the current room parties.
2404 */
2405 public function getRoomParties()
2406 {
2407 return $this->room_parties;
2408 }
2409
2410 /**
2411 * Sets and returns the flag to ignore the restrictions.
2412 *
2413 * @param bool $set the boolean status to set.
2414 *
2415 * @return bool the current ignore status.
2416 */
2417 public function ignoreRestrictions($set = null)
2418 {
2419 if (is_bool($set)) {
2420 $this->ignore_restrictions = $set;
2421 }
2422
2423 return $this->ignore_restrictions;
2424 }
2425
2426 /**
2427 * Sets and returns the flag to ignore the rooms availability.
2428 *
2429 * @param bool $set the boolean status to set.
2430 *
2431 * @return bool the current ignore status.
2432 *
2433 * @since 1.16.10 (J) - 1.6.10 (WP)
2434 */
2435 public function ignoreAvailability($set = null)
2436 {
2437 if (is_bool($set)) {
2438 $this->ignore_availability = $set;
2439 }
2440
2441 return $this->ignore_availability;
2442 }
2443
2444 /**
2445 * Takes the first payment ID "string", if available.
2446 *
2447 * @return string|null
2448 */
2449 protected function getDefaultPaymentId()
2450 {
2451 $dbo = JFactory::getDbo();
2452
2453 $q = "SELECT `id`, `name` FROM `#__vikbooking_gpayments` WHERE `published`=1 ORDER BY `ordering` ASC";
2454 $dbo->setQuery($q, 0, 1);
2455 $data = $dbo->loadAssoc();
2456
2457 if ($data) {
2458 return $data['id'] . '=' . $data['name'];
2459 }
2460
2461 return null;
2462 }
2463
2464 /**
2465 * Returns the current nights/transfers ratio for split stays.
2466 *
2467 * @return int the nights transfers ratio for the percent calculation.
2468 *
2469 * @since 1.16.0 (J) - 1.6.0 (WP)
2470 */
2471 public function getNightsTransfersRatio()
2472 {
2473 return $this->nights_transfers_ratio;
2474 }
2475
2476 /**
2477 * Returns the default nights/transfers ratio for split stays.
2478 *
2479 * @return int the nights transfers ratio defined in the configuration.
2480 *
2481 * @since 1.16.0 (J) - 1.6.0 (WP)
2482 */
2483 public function getDefaultNightsTransfersRatio()
2484 {
2485 $config = VBOFactory::getConfig();
2486
2487 return (int) $config->get('split_stay_ratio', 50);
2488 }
2489
2490 /**
2491 * Sets the nights/transfers ratio for split stays. Use it to start applying limits.
2492 *
2493 * @param ?int $ratio Optional ratio integer value, or default value will apply.
2494 *
2495 * @return self
2496 *
2497 * @since 1.16.0 (J) - 1.6.0 (WP)
2498 */
2499 public function setNightsTransfersRatio(?int $ratio = null)
2500 {
2501 if (is_int($ratio)) {
2502 $this->nights_transfers_ratio = $ratio;
2503 } else {
2504 $this->nights_transfers_ratio = $this->getDefaultNightsTransfersRatio();
2505 }
2506
2507 return $this;
2508 }
2509
2510 /**
2511 * Tells whether we need to behave for the front-end booking process.
2512 *
2513 * @return bool true if we are in the front-end booking process or false.
2514 *
2515 * @since 1.16.0 (J) - 1.6.0 (WP)
2516 */
2517 public function isFrontBooking()
2518 {
2519 return (bool) $this->is_front_booking;
2520 }
2521
2522 /**
2523 * Toggles the flag to behave for the front-end booking process.
2524 *
2525 * @return self
2526 *
2527 * @since 1.16.0 (J) - 1.6.0 (WP)
2528 */
2529 public function setIsFrontBooking($is_front = true)
2530 {
2531 $this->is_front_booking = (bool)$is_front;
2532
2533 return $this;
2534 }
2535
2536 /**
2537 * This helper method aims to collect the stay dates of each room in
2538 * a booking with split stay. Records will be loaded by ID ascending,
2539 * so in the same exact way as the rooms get stored. For this reason,
2540 * it is then possible to match the stay dates of a room by array-key,
2541 * even if bookings with split stay should always have different room IDs.
2542 *
2543 * @param int $bid the website reservation ID.
2544 *
2545 * @return array the list of busy records with stay dates information.
2546 *
2547 * @since 1.16.0 (J) - 1.6.0 (WP)
2548 */
2549 public function loadSplitStayBusyRecords($bid)
2550 {
2551 $dbo = JFactory::getDbo();
2552
2553 $bid = (int)$bid;
2554
2555 /**
2556 * It is fundamental to keep the ID column as the busy record ID
2557 * to allow the room switching in case of booking modification.
2558 */
2559 $q = "SELECT `ob`.`idorder`, `b`.`id`, `b`.`idroom`, `b`.`checkin`, `b`.`checkout`, `b`.`sharedcal`
2560 FROM `#__vikbooking_ordersbusy` AS `ob`
2561 LEFT JOIN `#__vikbooking_busy` AS `b` ON `ob`.`idbusy`=`b`.`id`
2562 WHERE `ob`.`idorder`={$bid}
2563 ORDER BY `b`.`sharedcal` ASC, `b`.`id` ASC;";
2564 $dbo->setQuery($q);
2565 $records = $dbo->loadAssocList();
2566
2567 if (!$records) {
2568 // the booking may no longer exist, or maybe it was cancelled
2569 return [];
2570 }
2571
2572 return $records;
2573 }
2574
2575 /**
2576 * Returns a list of the tax rate records.
2577 *
2578 * @return array the list of tax rates.
2579 *
2580 * @since 1.16.0 (J) - 1.6.0 (WP)
2581 */
2582 public function getTaxRates()
2583 {
2584 $dbo = JFactory::getDbo();
2585 $dbo->setQuery("SELECT * FROM `#__vikbooking_iva`;");
2586
2587 return $dbo->loadAssocList();
2588 }
2589
2590 /**
2591 * Sets the IDs of the rooms to filter or use.
2592 *
2593 * @param mixed $room_ids the list of room IDs to filter or use, or int room ID.
2594 *
2595 * @return self
2596 */
2597 public function setRoomIds($room_ids = [])
2598 {
2599 if (is_scalar($room_ids)) {
2600 // single room ID integer
2601 $room_ids = [$room_ids];
2602 }
2603
2604 $this->room_ids = $room_ids;
2605
2606 return $this;
2607 }
2608
2609 /**
2610 * Returns the current room ids to filter or use.
2611 *
2612 * @return array the current room ids.
2613 */
2614 public function getRoomIds()
2615 {
2616 return $this->room_ids;
2617 }
2618
2619 /**
2620 * In case of no availability, overrides the default number of days to check
2621 * prior and after the originally requested check-in and check-out dates.
2622 *
2623 * @param int $days the total number of days to use back and forth.
2624 *
2625 * @return self
2626 */
2627 public function setBackForthDays($days)
2628 {
2629 if (is_int($days) && $days >= 0) {
2630 $this->back_and_forth = $days;
2631 }
2632
2633 return $this;
2634 }
2635
2636 /**
2637 * Returns the current number of back and forth days.
2638 *
2639 * @return int the current number of days.
2640 */
2641 public function getBackForthDays()
2642 {
2643 return $this->back_and_forth;
2644 }
2645
2646 /**
2647 * Sets warning messages by concatenating the existing ones.
2648 *
2649 * @param string $str
2650 *
2651 * @return self
2652 */
2653 protected function setWarning($str)
2654 {
2655 $this->warning .= $str . "\n";
2656
2657 return $this;
2658 }
2659
2660 /**
2661 * Gets the current warning string.
2662 *
2663 * @return string
2664 */
2665 public function getWarning()
2666 {
2667 return rtrim($this->warning, "\n");
2668 }
2669
2670 /**
2671 * Sets errors by concatenating the existing ones.
2672 *
2673 * @param string $str
2674 *
2675 * @return self
2676 */
2677 protected function setError($str)
2678 {
2679 $this->error .= $str . "\n";
2680
2681 return $this;
2682 }
2683
2684 /**
2685 * Gets the current error string.
2686 *
2687 * @return string
2688 */
2689 public function getError()
2690 {
2691 return rtrim($this->error, "\n");
2692 }
2693
2694 /**
2695 * Gets the current error code.
2696 *
2697 * @return int
2698 */
2699 public function getErrorCode()
2700 {
2701 return $this->errorCode;
2702 }
2703 }
2704