PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.16
VikBooking Hotel Booking Engine & PMS v1.8.16
1.8.16 1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 All 37 releases
← All changes | admin/helpers/availability.php +723 -47 1.6.5 → 1.8.16 View file →
@@ -67,8 +67,15 @@
67 67 */
68 68 protected $ignore_restrictions = false;
69 69
70 70 /**
71 + * Whether to ignore rooms availability.
72 + *
73 + * @var bool
74 + */
75 + protected $ignore_availability = false;
76 +
77 + /**
71 78 * Whether check-ins on check-outs are allowed.
72 79 *
73 80 * @var bool
74 81 */
@@ -147,20 +154,28 @@
147 154 // load dependencies
148 155 if (!class_exists('TACVBO')) {
149 156 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'tac.vikbooking.php';
150 157 }
158 +
159 + /**
160 + * Set default nights transfer ratio.
161 + *
162 + * @since 1.18.5 (J) - 1.8.5 (WP)
163 + */
164 + $this->setNightsTransfersRatio();
151 165 }
152 166
153 167 /**
154 - * Returns the global object, either
155 - * a new instance or the existing instance
156 - * if the class was already instantiated.
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.
157 172 *
158 - * @return self A new instance of the class.
173 + * @return VikBookingAvailability
159 174 */
160 - public static function getInstance()
175 + public static function getInstance($anew = false)
161 176 {
162 - if (is_null(static::$instance)) {
177 + if (is_null(static::$instance) || $anew) {
163 178 static::$instance = new static();
164 179 }
165 180
166 181 return static::$instance;
@@ -246,9 +261,10 @@
246 261 return 'Missing or invalid request options.';
247 262 case 2:
248 263 return 'Invalid request authentication.';
249 264 case 3:
250 - return JText::translate('VBO_AV_ECODE_3');
265 + $expl = JText::translate('VBO_AV_ECODE_3');
266 + return $expl != 'VBO_AV_ECODE_3' ? $expl : 'No rooms found for the given party.';
251 267 case 4:
252 268 return 'Not compliant with the booking restrictions.';
253 269 case 5:
254 270 return 'Not compliant with the global closing dates.';
@@ -254,25 +270,105 @@
254 270 return 'Not compliant with the global closing dates.';
255 271 case 6:
256 272 return 'No rates defined for the given length of stay.';
257 273 case 7:
258 - return JText::translate('VBO_AV_ECODE_7');
274 + $expl = JText::translate('VBO_AV_ECODE_7');
275 + return $expl != 'VBO_AV_ECODE_7' ? $expl : 'No availability for the dates requested.';
259 276 case 8:
260 277 return 'No rooms available due to room or rate plan restrictions.';
261 278 default:
262 279 return 'Unknown error code.';
263 280 }
264 -
265 281 }
266 282
267 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 + /**
268 356 * Loads all rooms in VBO and maps them into an associative array.
269 357 *
270 - * @return array the associative list of rooms.
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.
271 367 */
272 - public function loadRooms()
368 + public function loadRooms(array $ids = [], $max = 0, $anew = false)
273 369 {
274 - if ($this->all_rooms) {
370 + if ($this->all_rooms && !$anew) {
275 371 // return previously cached array if available
276 372 return $this->all_rooms;
277 373 }
278 374
@@ -277,16 +373,56 @@
277 373 }
278 374
279 375 $dbo = JFactory::getDbo();
280 376
281 - $q = "SELECT `id`, `name`, `img`, `idcat`, `avail`, `units`, `fromadult`, `toadult`,
282 - `fromchild`, `tochild`, `totpeople`, `mintotpeople` FROM `#__vikbooking_rooms`
283 - ORDER BY `avail` DESC, `name` ASC;";
284 - $dbo->setQuery($q);
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);
285 421 $room_rows = $dbo->loadAssocList();
286 422
287 423 if (!$room_rows) {
288 - return $this->all_rooms;
424 + return $anew ? [] : $this->all_rooms;
289 425 }
290 426
291 427 if ($this->isFrontBooking()) {
292 428 // apply translations on rooms
@@ -293,13 +429,19 @@
293 429 $vbo_tn = VikBooking::getTranslator();
294 430 $vbo_tn->translateContents($room_rows, '#__vikbooking_rooms');
295 431 }
296 432
433 + $assoc_rooms = [];
297 434 foreach ($room_rows as $room) {
298 - $this->all_rooms[$room['id']] = $room;
435 + $assoc_rooms[$room['id']] = $room;
299 436 }
300 437
301 - return $this->all_rooms;
438 + if (!$anew) {
439 + // cache room records
440 + $this->all_rooms = $assoc_rooms;
441 + }
442 +
443 + return $anew ? $assoc_rooms : $this->all_rooms;
302 444 }
303 445
304 446 /**
305 447 * Sets the current rooms as an associative array of information. The
@@ -325,9 +467,9 @@
325 467 {
326 468 $rooms = $this->loadRooms();
327 469
328 470 foreach ($rooms as $k => $room) {
329 - if (!$room['avail']) {
471 + if (!($room['avail'] ?? 1)) {
330 472 unset($rooms[$k]);
331 473 }
332 474 }
333 475
@@ -334,15 +476,56 @@
334 476 return $rooms;
335 477 }
336 478
337 479 /**
480 + * Finds a room (or multiple rooms) by name.
481 + *
482 + * @param string $name The room name to look for.
483 + * @param bool $all True to return a list of matches.
484 + *
485 + * @return array
486 + *
487 + * @since 1.16.10 (J) - 1.6.10 (WP)
488 + * @since 1.18.17 (J) - 1.8.17 (WP) added argument $all.
489 + */
490 + public function getRoomByName(string $name, bool $all = false)
491 + {
492 + $dbo = JFactory::getDbo();
493 +
494 + $q = $dbo->getQuery(true)
495 + ->select('*')
496 + ->from($dbo->qn('#__vikbooking_rooms'))
497 + ->order($dbo->qn('avail') . ' DESC')
498 + ->order($dbo->qn('name') . ' ASC');
499 +
500 + foreach (array_filter(preg_split("/[\s\-_.,]+/", $name)) as $nm_part) {
501 + $q->where($dbo->qn('name') . ' LIKE ' . $dbo->q("%{$nm_part}%"));
502 + }
503 +
504 + if ($all) {
505 + // match all records
506 + $dbo->setQuery($q);
507 + return $dbo->loadAssocList();
508 + }
509 +
510 + $dbo->setQuery($q, 0, 1);
511 + $record = $dbo->loadAssoc();
512 +
513 + return $record ?? [];
514 + }
515 +
516 + /**
338 517 * Loads all rate plans in VBO and maps them into an associative array.
339 518 *
519 + * @param bool $no_cache True to avoid internal caching.
520 + *
340 521 * @return array the associative list of rate plans.
522 + *
523 + * @since 1.17.6 (J) - 1.7.6 (WP) added $no_cache argument.
341 524 */
342 - public function loadRatePlans()
525 + public function loadRatePlans($no_cache = false)
343 526 {
344 - if (count($this->all_rplans)) {
527 + if (!$no_cache && $this->all_rplans) {
345 528 // return previously cached array if available
346 529 return $this->all_rplans;
347 530 }
348 531
@@ -348,27 +531,452 @@
348 531
349 532 $dbo = JFactory::getDbo();
350 533
351 534 $rate_plans = [];
535 + $derived_rplans = [];
352 536
353 537 $q = "SELECT * FROM `#__vikbooking_prices` ORDER BY `name` ASC;";
354 538 $dbo->setQuery($q);
355 539 $rplan_rows = $dbo->loadAssocList();
356 540
357 - if ($rplan_rows) {
358 - foreach ($rplan_rows as $rplan) {
359 - $rate_plans[$rplan['id']] = $rplan;
541 + foreach ($rplan_rows as $rplan) {
542 + if (!empty($rplan['derived_id']) && !empty($rplan['derived_data'])) {
543 + // decode the derived data information
544 + $rplan['derived_data'] = json_decode($rplan['derived_data'], true);
545 + // add rate plan ID
546 + $derived_rplans[] = $rplan['id'];
360 547 }
361 - // sort rate plans
362 - $rate_plans = VikBooking::sortRatePlans($rate_plans, true);
548 + $rate_plans[$rplan['id']] = $rplan;
363 549 }
364 550
365 - $this->all_rplans = $rate_plans;
551 + // add the information about the parent rate plans, if any
552 + foreach ($rate_plans as $rplan_id => $rplan_data) {
553 + if (in_array($rplan_id, $derived_rplans) && isset($rate_plans[$rplan_data['derived_id']])) {
554 + // set parent rate details
555 + $rate_plans[$rplan_id]['parent_rate_id'] = $rate_plans[$rplan_data['derived_id']]['id'];
556 + $rate_plans[$rplan_id]['parent_rate_name'] = $rate_plans[$rplan_data['derived_id']]['name'];
557 + }
558 + }
366 559
560 + // sort rate plans
561 + $rate_plans = VikBooking::sortRatePlans($rate_plans, true);
562 +
563 + if (!$no_cache) {
564 + $this->all_rplans = $rate_plans;
565 + }
566 +
367 567 return $rate_plans;
368 568 }
369 569
370 570 /**
571 + * Returns a list of rate plans derived from the given parent rate plan ID.
572 + *
573 + * @param int $parent_rate_id The parent rate plan ID.
574 + * @param ?array $all_rate_plans Optional cached rate plans list.
575 + *
576 + * @return array
577 + *
578 + * @since 1.16.10 (J) - 1.6.10 (WP)
579 + * @since 1.18.5 (J) - 1.8.5 (WP) added argument $all_rate_plans.
580 + */
581 + public function getDerivedRatePlans(int $parent_rate_id, ?array $all_rate_plans = null)
582 + {
583 + $derived_rplans = [];
584 +
585 + foreach (($all_rate_plans ?: $this->loadRatePlans()) as $rp_id => $rplan) {
586 + if ($rplan['derived_id'] && $rplan['derived_id'] == $parent_rate_id && $rplan['derived_data']) {
587 + // this rate plan is derived from the given parent rate plan
588 + $derived_rplans[] = $rplan;
589 + }
590 + }
591 +
592 + return $derived_rplans;
593 + }
594 +
595 + /**
596 + * Returns the orphan dates for the given or set rooms and dates.
597 + *
598 + * @param array $options List of fetching options.
599 + *
600 + * @return array Associative list of rooms orphan dates.
601 + *
602 + * @since 1.17.7 (J) - 1.7.7 (WP)
603 + */
604 + public function getOrphanDates(array $options = [])
605 + {
606 + // flag to indicate whether room-level restrictions were loaded
607 + $use_room_level_restr = false;
608 +
609 + // load and set rooms and restrictions
610 + if ($options['room_ids'] ?? []) {
611 + // force the requested rooms to be loaded
612 + $this->loadRooms((array) $options['room_ids']);
613 +
614 + // load restrictions with room filters
615 + $all_restrictions = VikBooking::loadRestrictions(true, (array) $options['room_ids']);
616 +
617 + // turn flag on for room-level restrictions being used
618 + $use_room_level_restr = true;
619 + } else {
620 + // load restrictions with no filters
621 + $all_restrictions = VikBooking::loadRestrictions(false);
622 + }
623 +
624 + // default minimum stay
625 + $def_min_stay = VikBooking::getDefaultNightsCalendar();
626 +
627 + // figure out dates range
628 + if (($current_stay_dates = $this->getStayDates()) && !($options['from_date'] ?? null)) {
629 + // populate current stay dates
630 + $options['from_date'] = $current_stay_dates[0];
631 + $options['to_date'] = $current_stay_dates[1];
632 + } else {
633 + // check given fetching options
634 + if (!($options['from_date'] ?? null)) {
635 + // default to today's date
636 + $options['from_date'] = date('Y-m-d');
637 + }
638 +
639 + if (!($options['to_date'] ?? null)) {
640 + // default to next week's date
641 + $options['to_date'] = date('Y-m-d', strtotime('+1 week'));
642 + }
643 + }
644 +
645 + // calculate the highest minimum stay for a better accuracy
646 + $all_min_los = [($def_min_stay > 1 ? $def_min_stay : 0)];
647 + foreach ($all_restrictions as $index => $restrs) {
648 + $all_min_los = array_merge($all_min_los, array_column($restrs, 'minlos'));
649 + }
650 + $max_min_los = max(array_map('intval', $all_min_los));
651 +
652 + if (!$max_min_los) {
653 + // no minimum stay restrictions to apply, hence no orphan dates
654 + return [];
655 + }
656 +
657 + // calculate past limit timestamp for today at midnight
658 + $lim_past_ts = strtotime(date('Y-m-d'));
659 +
660 + // always fetch a wider availability window for better accuracy
661 + $fetch_from_date = date('Y-m-d', strtotime("-{$max_min_los} days", strtotime($options['from_date'])));
662 + $fetch_to_date = date('Y-m-d', strtotime("+{$max_min_los} days", strtotime($options['to_date'])));
663 + if (strtotime($fetch_from_date) < $lim_past_ts) {
664 + // start fetching the inventory from today's date (past dates not accepted)
665 + $fetch_from_date = date('Y-m-d');
666 + }
667 +
668 + // always set stay dates
669 + $this->setStayDates($fetch_from_date, $fetch_to_date);
670 +
671 + // obtain the availability inventory by ignoring restrictions
672 + $ari = $this->getInventory(false);
673 +
674 + if (!$ari) {
675 + return [];
676 + }
677 +
678 + // build orphan dates pool
679 + $orphans_pool = [];
680 +
681 + // UTC timezone
682 + $utc_tz = new DateTimezone('UTC');
683 +
684 + // get date bounds
685 + $from_bound = new DateTime($options['from_date'], $utc_tz);
686 + $to_bound = new DateTime($options['to_date'], $utc_tz);
687 +
688 + // build iterable dates interval (period)
689 + $date_range = new DatePeriod(
690 + // start date included by default in the result set
691 + $from_bound,
692 + // interval between recurrences within the period
693 + new DateInterval('P1D'),
694 + // end date excluded by default from the result set
695 + $to_bound->modify('+1 day')
696 + );
697 +
698 + // scan rooms availability inventory
699 + foreach ($ari as $idroom => $room_ari) {
700 + // load proper room-level restrictions if not done already
701 + $all_restrictions = $use_room_level_restr ? $all_restrictions : VikBooking::loadRestrictions(true, [$idroom]);
702 +
703 + // iterate the dates interval
704 + foreach ($date_range as $dt) {
705 + // calculate date values
706 + $day_key = $dt->format('Y-m-d');
707 + $day_now_ts = strtotime($day_key);
708 + $day_after_ts = strtotime('+1 day', $day_now_ts);
709 +
710 + // parse room restrictions for the current inventory day and room to get the minimum stay
711 + $restr = VikBooking::parseSeasonRestrictions($day_now_ts, $day_after_ts, 1, $all_restrictions);
712 + $minimum_stay = (int) ($restr['minlos'] ?? $def_min_stay);
713 +
714 + if ($minimum_stay < 2) {
715 + // no real minimum stay restriction detected for this day
716 + continue;
717 + }
718 +
719 + // scan rooms availability
720 + foreach ($room_ari['inventory'] as $keypoint => $inventory) {
721 + if ($inventory['day'] != $day_key) {
722 + // not the date-key point we are looking for
723 + continue;
724 + }
725 +
726 + // tell whether the listing is available on the current date-key point
727 + $is_available = (bool) ($inventory['units_to_sell'] ?? $inventory['available'] ?? 0);
728 + if (!$is_available) {
729 + // this day is fully booked, hence no orphan dates
730 + continue 2;
731 + }
732 +
733 + // scan the availability for the next minimum stay days from the current day-key point
734 + for ($d = 0; $d < $minimum_stay; $d++) {
735 + // build next date-key point value (start from current day-key point as it counts as one available night of stay)
736 + $next_keypoint = $keypoint + $d;
737 +
738 + if (!isset($room_ari['inventory'][$next_keypoint])) {
739 + // no more data available
740 + break;
741 + }
742 +
743 + // tell whether the listing will be available on this future date-key point (0th day will always be available)
744 + $will_be_available = (bool) ($room_ari['inventory'][$next_keypoint]['units_to_sell'] ?? $room_ari['inventory'][$next_keypoint]['available'] ?? 0);
745 +
746 + if (!$will_be_available) {
747 + /**
748 + * Probable orphan date found for bookings on days ahead. Ensure this
749 + * is truly an orphan date by checking the days before. If staying
750 + * on this (available) night is allowed, then this should not be
751 + * considered as an orphan date, even though arriving is not allowed.
752 + */
753 + if (!($options['orphans_checkin'] ?? false)) {
754 + // check previous dates before saying it's an orphan date
755 + for ($backd = ($keypoint - 1), $distance = 1; $backd >= 0; $backd--, $distance++) {
756 + if (!isset($room_ari['inventory'][$backd])) {
757 + // nothing to do, let it be an orphan date
758 + break;
759 + }
760 +
761 + // tell whether the listing was available on this previous date-key point
762 + $was_available = (bool) ($room_ari['inventory'][$backd]['units_to_sell'] ?? $room_ari['inventory'][$backd]['available'] ?? 0);
763 +
764 + if (!$was_available) {
765 + // nothing to do, let it be an orphan date
766 + break;
767 + }
768 +
769 + // calculate past date values
770 + $past_day_now_ts = strtotime($room_ari['inventory'][$backd]['day']);
771 + $past_day_after_ts = strtotime('+1 day', $past_day_now_ts);
772 +
773 + // parse room restrictions for the current past day and room to get the minimum stay
774 + $restr = VikBooking::parseSeasonRestrictions($past_day_now_ts, $past_day_after_ts, 1, $all_restrictions);
775 + $minimum_stay = (int) ($restr['minlos'] ?? $def_min_stay);
776 +
777 + if ($minimum_stay <= $distance) {
778 + // free day with a minimum stay that allows to stay on the presumed orphan date
779 + // break the cycles for the presumed orphan date-key point, because it's not truly an orphan
780 + break 3;
781 + }
782 + }
783 + }
784 +
785 + // orphan date found
786 + if (!isset($orphans_pool[$idroom])) {
787 + // start container
788 + $orphans_pool[$idroom] = [
789 + 'room_name' => $room_ari['room_name'],
790 + 'orphans' => [],
791 + ];
792 + }
793 +
794 + // push orphan date information
795 + $orphans_pool[$idroom]['orphans'][] = [
796 + 'day' => $day_key,
797 + 'current_min_stay_nights' => $minimum_stay,
798 + 'max_min_stay_nights_allowed' => $d,
799 + ];
800 +
801 + // break the cycles for this date-key point
802 + break 2;
803 + }
804 + }
805 + }
806 + }
807 + }
808 +
809 + // return the associative list of room orphan dates found, if any
810 + return $orphans_pool;
811 + }
812 +
813 + /**
814 + * Returns the inventory for the rooms and dates set.
815 + *
816 + * @param bool $restrictions Whether to include booking restrictions data.
817 + *
818 + * @return array Associative list of rooms availability inventory.
819 + *
820 + * @since 1.16.10 (J) - 1.6.10 (WP)
821 + */
822 + public function getInventory($restrictions = false)
823 + {
824 + $stay_ts = $this->getStayDates(true);
825 +
826 + if (!$stay_ts) {
827 + $this->setError('No dates provided');
828 + return [];
829 + }
830 +
831 + if ($stay_ts[0] > $stay_ts[1]) {
832 + $this->setError('Invalid dates provided');
833 + return [];
834 + }
835 +
836 + $info_from = getdate($stay_ts[0]);
837 +
838 + $room_ids = array_column($this->loadRooms(), 'id');
839 + $room_names = array_column($this->loadRooms(), 'name');
840 + $room_units = array_column($this->loadRooms(), 'units');
841 +
842 + if (!$room_ids) {
843 + $this->setError('No rooms provided');
844 + return [];
845 + }
846 +
847 + // load busy records
848 + $busy_records = VikBooking::loadBusyRecords($room_ids, $stay_ts[0], $stay_ts[1]);
849 +
850 + // room restrictions associative list
851 + $room_restrictions = [];
852 +
853 + // global minimum stay restriction
854 + $glob_minlos = VikBooking::getDefaultNightsCalendar();
855 + $glob_minlos = $glob_minlos < 1 ? 1 : (int) $glob_minlos;
856 +
857 + // inventory pool
858 + $ari = [];
859 + foreach ($room_ids as $room_index => $room_id) {
860 + $ari[$room_id] = [
861 + 'room_name' => $room_names[$room_index],
862 + 'inventory' => [],
863 + ];
864 + }
865 +
866 + // loop through all dates in the range
867 + while ($info_from[0] <= $stay_ts[1]) {
868 + // build day after timestamp
869 + $day_after_ts = mktime(0, 0, 0, $info_from['mon'], $info_from['mday'] + 1, $info_from['year']);
870 +
871 + // scan the units booked for each requested room
872 + foreach ($room_ids as $room_index => $room_id) {
873 + $units_booked = 0;
874 + // scan all the occupied records of the current room
875 + foreach (($busy_records[$room_id] ?? []) as $busy) {
876 + $info_in = getdate($busy['checkin']);
877 + $info_out = getdate($busy['checkout']);
878 + $in_ts = mktime(0, 0, 0, $info_in['mon'], $info_in['mday'], $info_in['year']);
879 + $out_ts = mktime(0, 0, 0, $info_out['mon'], $info_out['mday'], $info_out['year']);
880 + if ($info_from[0] >= $in_ts && $info_from[0] < $out_ts) {
881 + // increase room units booked
882 + $units_booked++;
883 + }
884 + }
885 +
886 + // count units left
887 + $units_left = $units_booked >= $room_units[$room_index] ? 0 : ($room_units[$room_index] - $units_booked);
888 +
889 + // set room inventory date
890 + $inventory_day = [
891 + 'day' => date('Y-m-d', $info_from[0]),
892 + ];
893 +
894 + if ($room_units[$room_index] == 1) {
895 + // single-unit listing inventory structure
896 + $inventory_day['available'] = (bool) $units_left;
897 + } else {
898 + // multi-unit room-type inventory structure
899 + $inventory_day['units_booked'] = $units_booked;
900 + $inventory_day['units_to_sell'] = $units_left;
901 + }
902 +
903 + // check for room-level restrictions
904 + if ($restrictions) {
905 + if (!isset($room_restrictions[$room_id])) {
906 + // load room restrictions only once
907 + $room_restrictions[$room_id] = VikBooking::loadRestrictions(true, [$room_id]);
908 + }
909 +
910 + // parse room restrictions for the current inventory day
911 + $restr = VikBooking::parseSeasonRestrictions($info_from[0], $day_after_ts, 1, $room_restrictions[$room_id]);
912 + if ($restr) {
913 + $inventory_day['restrictions'] = [
914 + 'min_los' => (int) $restr['minlos'],
915 + ];
916 + if (($restr['maxlos'] ?? 0)) {
917 + $inventory_day['restrictions']['max_los'] = (int) $restr['maxlos'];
918 + }
919 + if (!empty($restr['cta'])) {
920 + $inventory_day['restrictions']['closed_to_arrival'] = array_map(function($wday) {
921 + switch ($wday) {
922 + case '1':
923 + return 'Monday';
924 + case '2':
925 + return 'Tuesday';
926 + case '3':
927 + return 'Wednesday';
928 + case '4':
929 + return 'Thursday';
930 + case '5':
931 + return 'Friday';
932 + case '6':
933 + return 'Saturday';
934 + default:
935 + return 'Sunday';
936 + }
937 + }, $restr['cta']);
938 + }
939 + if (!empty($restr['ctd'])) {
940 + $inventory_day['restrictions']['closed_to_departure'] = array_map(function($wday) {
941 + switch ($wday) {
942 + case '1':
943 + return 'Monday';
944 + case '2':
945 + return 'Tuesday';
946 + case '3':
947 + return 'Wednesday';
948 + case '4':
949 + return 'Thursday';
950 + case '5':
951 + return 'Friday';
952 + case '6':
953 + return 'Saturday';
954 + default:
955 + return 'Sunday';
956 + }
957 + }, $restr['ctd']);
958 + }
959 + } else {
960 + // set the global minimum stay restriction
961 + $inventory_day['restrictions'] = [
962 + 'min_los' => $glob_minlos,
963 + ];
964 + }
965 + }
966 +
967 + // push room-day inventory
968 + $ari[$room_id]['inventory'][] = $inventory_day;
969 + }
970 +
971 + // go to next date
972 + $info_from = getdate($day_after_ts);
973 + }
974 +
975 + return $ari;
976 + }
977 +
978 + /**
371 979 * Gets the available room rates for the specified dates, party and rooms.
372 980 *
373 981 * @param array $params optional list of options to be forced for TACVBO.
374 982 *
@@ -375,23 +983,29 @@
375 983 * @return mixed boolean false in case of errors or array result of TACVBO class.
376 984 */
377 985 public function getRates($params = [])
378 986 {
379 - if (!count($this->stay_dates)) {
987 + // reset errors to their initial values
988 + $this->error = '';
989 + $this->errorCode = 0;
990 +
991 + if (!$this->stay_dates) {
380 992 $this->setError('No dates provided');
381 993 return false;
382 994 }
383 995
384 - if (!count($this->room_parties)) {
996 + if (!$this->room_parties) {
385 997 $this->setError('No room party provided');
386 998 return false;
387 999 }
388 1000
389 1001 // count injected rooms, if any
390 - $tot_rooms = count($this->room_ids);
1002 + $tot_rooms = (int) (($params['rooms_count'] ?? 0) ?: count($this->room_ids));
391 1003
392 1004 // build options array for TACVBO
393 1005 $options = [
1006 + 'hash' => md5('vbo.e4j.vbo'),
1007 + 'req_type' => 'hotel_availability',
394 1008 'start_date' => $this->stay_dates[0],
395 1009 'end_date' => $this->stay_dates[1],
396 1010 'nights' => $this->countNightsOfStay(),
397 1011 'num_rooms' => ($tot_rooms > 0 ? $tot_rooms : 1),
@@ -397,8 +1011,9 @@
397 1011 'num_rooms' => ($tot_rooms > 0 ? $tot_rooms : 1),
398 1012 'adults' => [$this->getPartyGuests('adults', 0)],
399 1013 'children' => [$this->getPartyGuests('children', 0)],
400 1014 'only_rates' => 1,
1015 + 'wtax' => $params['wtax'] ?? null,
401 1016 ];
402 1017
403 1018 if ($tot_rooms > 1 && count($this->room_parties) == $tot_rooms) {
404 1019 // re-build list of adults and children
@@ -409,8 +1024,29 @@
409 1024 $options['children'][] = $this->getPartyGuests('children', $i);
410 1025 }
411 1026 }
412 1027
1028 + // check for implicit settings
1029 + if (!empty($params['max_rooms_limit'])) {
1030 + // set rooms maximum limit
1031 + TACVBO::$maxRoomsLimit = (int) $params['max_rooms_limit'];
1032 + // unset implicit setting
1033 + unset($params['max_rooms_limit']);
1034 + } else {
1035 + // reset to initial value for subsequent calls
1036 + TACVBO::$maxRoomsLimit = 0;
1037 + }
1038 +
1039 + if (is_array(($params['forced_room_ids'] ?? null))) {
1040 + // set allowed room IDs
1041 + TACVBO::$forcedRoomIds = $params['forced_room_ids'];
1042 + // unset implicit setting
1043 + unset($params['forced_room_ids']);
1044 + } else {
1045 + // reset to initial value for subsequent calls
1046 + TACVBO::$forcedRoomIds = [];
1047 + }
1048 +
413 1049 // merge default options with params, if any
414 1050 $options = array_merge($options, $params);
415 1051
416 1052 // invoke TACVBO class by injecting the options
@@ -415,8 +1051,9 @@
415 1051
416 1052 // invoke TACVBO class by injecting the options
417 1053 TACVBO::$getArray = true;
418 1054 TACVBO::$ignoreRestrictions = $this->ignore_restrictions;
1055 + TACVBO::$ignoreAvailability = $this->ignore_availability;
419 1056 $website_rates = TACVBO::tac_av_l($options);
420 1057
421 1058 // store the error code occurred (if any)
422 1059 $this->errorCode = TACVBO::getErrorCode();
@@ -440,9 +1077,9 @@
440 1077 }
441 1078
442 1079 // optional filter by room IDs will be applied on this flow
443 1080 $found_rids = array_keys($website_rates);
444 - $unwanted_rids = $tot_rooms ? array_diff($found_rids, $this->room_ids) : [];
1081 + $unwanted_rids = $this->room_ids ? array_diff($found_rids, $this->room_ids) : [];
445 1082 foreach ($unwanted_rids as $rid) {
446 1083 unset($website_rates[$rid]);
447 1084 }
448 1085
@@ -449,8 +1086,20 @@
449 1086 return $website_rates;
450 1087 }
451 1088
452 1089 /**
1090 + * Returns the fully booked linear array of room IDs.
1091 + *
1092 + * @return array
1093 + *
1094 + * @since 1.18.8 (J) - 1.8.8 (WP)
1095 + */
1096 + public function getFullyBooked()
1097 + {
1098 + return (array) $this->fully_booked;
1099 + }
1100 +
1101 + /**
453 1102 * Finds the available suggestions in case of no availability previously occurred
454 1103 * while getting the rates. This method should be called after getRates() so that
455 1104 * a valid errorCode to be analized will be available, unless code is forced.
456 1105 * Suggestions can include closest booking dates and/or different room-guest parties.
@@ -509,15 +1158,15 @@
509 1158 * we build a list of available solutions for booking split stays on the same dates. The
510 1159 * visibility should be public so that other Views could use just this method.
511 1160 *
512 1161 * @param array $room_ids list of unavailable, yet compatible, room IDs.
513 - * @param array $busy_list optional list of busy records for the involved dates.
1162 + * @param ?array $busy_list optional list of busy records for the involved dates.
514 1163 *
515 1164 * @return array associative list of available split stays, or empty array.
516 1165 *
517 1166 * @since 1.16.0 (J) - 1.6.0 (WP)
518 1167 */
519 - public function findSplitStays(array $room_ids = [], array $busy_list = null)
1168 + public function findSplitStays(array $room_ids = [], ?array $busy_list = null)
520 1169 {
521 1170 if (!$room_ids) {
522 1171 return [];
523 1172 }
@@ -618,15 +1267,15 @@
618 1267 }
619 1268 }
620 1269 }
621 1270
622 - if (!count($avroom_nights)) {
1271 + if (!$avroom_nights) {
623 1272 // no rooms available at all, there's no way to do anything
624 1273 return [];
625 1274 }
626 1275
627 1276 // make sure all nights requested can be satisfied by at least one room
628 - foreach ($groupdays as $gday) {
1277 + foreach ($groupdays as $gday_index => $gday) {
629 1278 $day_key = date('Y-m-d', $gday);
630 1279 $day_av = false;
631 1280 foreach ($avroom_nights as $rid => $av_nights) {
632 1281 if (in_array($day_key, $av_nights)) {
@@ -634,10 +1283,11 @@
634 1283 $day_av = true;
635 1284 break;
636 1285 }
637 1286 }
638 - if (!$day_av) {
639 - // this night is not available in any room, unable to proceed
1287 + // ensure there's availability for this day, or permit if it's the check-out day
1288 + if (!$day_av && !($gday_index === (count($groupdays) - 1) && $this->inonout_allowed)) {
1289 + // this night of stay is not available in any room, unable to proceed
640 1290 return [];
641 1291 }
642 1292 }
643 1293
@@ -835,9 +1485,9 @@
835 1485 }
836 1486
837 1487 // apply nights/transfers ratio limit (unless disabled)
838 1488 $nights_transfers_ratio = $this->getNightsTransfersRatio();
839 - if ($nights_transfers_ratio > 0 && $nights_transfers_ratio < 100) {
1489 + if ($nights_transfers_ratio > 0 && $nights_transfers_ratio <= 100) {
840 1490 // count and apply limits
841 1491 foreach ($split_stay_sols as $k => $split_stay_sol) {
842 1492 // count nights and transfers
843 1493 $split_stay_transfers = count($split_stay_sol) - 1;
@@ -852,11 +1502,14 @@
852 1502 unset($split_stay_sols[$k]);
853 1503 $altered_sols = true;
854 1504 }
855 1505 }
1506 + } else {
1507 + // split stays have been disabled
1508 + $split_stay_sols = [];
856 1509 }
857 1510
858 - if ($altered_sols && count($split_stay_sols)) {
1511 + if ($altered_sols && $split_stay_sols) {
859 1512 // restore the array keys
860 1513 $split_stay_sols = array_values($split_stay_sols);
861 1514 }
862 1515
@@ -1727,13 +2380,13 @@
1727 2380 return $this;
1728 2381 }
1729 2382
1730 2383 /**
1731 - * Returns the current stay date or timestamps.
2384 + * Returns the current stay dates or timestamps.
1732 2385 *
1733 2386 * @param bool $ts whether to get the date timestamps.
1734 2387 *
1735 - * @return array the current stay date timestamps.
2388 + * @return array the current stay dates or timestamps.
1736 2389 */
1737 2390 public function getStayDates($ts = false)
1738 2391 {
1739 2392 return $ts ? $this->stay_ts : $this->stay_dates;
@@ -1790,8 +2443,26 @@
1790 2443 return $this->ignore_restrictions;
1791 2444 }
1792 2445
1793 2446 /**
2447 + * Sets and returns the flag to ignore the rooms availability.
2448 + *
2449 + * @param bool $set the boolean status to set.
2450 + *
2451 + * @return bool the current ignore status.
2452 + *
2453 + * @since 1.16.10 (J) - 1.6.10 (WP)
2454 + */
2455 + public function ignoreAvailability($set = null)
2456 + {
2457 + if (is_bool($set)) {
2458 + $this->ignore_availability = $set;
2459 + }
2460 +
2461 + return $this->ignore_availability;
2462 + }
2463 +
2464 + /**
1794 2465 * Takes the first payment ID "string", if available.
1795 2466 *
1796 2467 * @return string|null
1797 2468 */
@@ -1830,23 +2501,28 @@
1830 2501 * @since 1.16.0 (J) - 1.6.0 (WP)
1831 2502 */
1832 2503 public function getDefaultNightsTransfersRatio()
1833 2504 {
1834 - $config = VBOFactory::getConfig();
2505 + static $transferRatio = null;
1835 2506
1836 - return (int)$config->get('split_stay_ratio', 50);
2507 + if ($transferRatio === null) {
2508 + $config = VBOFactory::getConfig();
2509 + $transferRatio = (int) $config->get('split_stay_ratio', 50);
2510 + }
2511 +
2512 + return $transferRatio;
1837 2513 }
1838 2514
1839 2515 /**
1840 2516 * Sets the nights/transfers ratio for split stays. Use it to start applying limits.
1841 2517 *
1842 - * @param mixed $ratio integer value, or the default config setting will be applied.
2518 + * @param ?int $ratio Optional ratio integer value, or default value will apply.
1843 2519 *
1844 2520 * @return self
1845 2521 *
1846 2522 * @since 1.16.0 (J) - 1.6.0 (WP)
1847 2523 */
1848 - public function setNightsTransfersRatio($ratio = null)
2524 + public function setNightsTransfersRatio(?int $ratio = null)
1849 2525 {
1850 2526 if (is_int($ratio)) {
1851 2527 $this->nights_transfers_ratio = $ratio;
1852 2528 } else {
@@ -1864,9 +2540,9 @@
1864 2540 * @since 1.16.0 (J) - 1.6.0 (WP)
1865 2541 */
1866 2542 public function isFrontBooking()
1867 2543 {
1868 - return (bool)$this->is_front_booking;
2544 + return (bool) $this->is_front_booking;
1869 2545 }
1870 2546
1871 2547 /**
1872 2548 * Toggles the flag to behave for the front-end booking process.