PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.7
VikBooking Hotel Booking Engine & PMS v1.8.7
1.8.16 1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 All 37 releases
vikbooking / admin / helpers / availability.php

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

2,717 lines 81.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage 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 (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 /**
517 * Loads all rate plans in VBO and maps them into an associative array.
518 *
519 * @param bool $no_cache True to avoid internal caching.
520 *
521 * @return array the associative list of rate plans.
522 *
523 * @since 1.17.6 (J) - 1.7.6 (WP) added $no_cache argument.
524 */
525 public function loadRatePlans($no_cache = false)
526 {
527 if (!$no_cache && $this->all_rplans) {
528 // return previously cached array if available
529 return $this->all_rplans;
530 }
531
532 $dbo = JFactory::getDbo();
533
534 $rate_plans = [];
535 $derived_rplans = [];
536
537 $q = "SELECT * FROM `#__vikbooking_prices` ORDER BY `name` ASC;";
538 $dbo->setQuery($q);
539 $rplan_rows = $dbo->loadAssocList();
540
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'];
547 }
548 $rate_plans[$rplan['id']] = $rplan;
549 }
550
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 }
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
567 return $rate_plans;
568 }
569
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 /**
979 * Gets the available room rates for the specified dates, party and rooms.
980 *
981 * @param array $params optional list of options to be forced for TACVBO.
982 *
983 * @return mixed boolean false in case of errors or array result of TACVBO class.
984 */
985 public function getRates($params = [])
986 {
987 // reset errors to their initial values
988 $this->error = '';
989 $this->errorCode = 0;
990
991 if (!$this->stay_dates) {
992 $this->setError('No dates provided');
993 return false;
994 }
995
996 if (!$this->room_parties) {
997 $this->setError('No room party provided');
998 return false;
999 }
1000
1001 // count injected rooms, if any
1002 $tot_rooms = count($this->room_ids);
1003
1004 // build options array for TACVBO
1005 $options = [
1006 'hash' => md5('vbo.e4j.vbo'),
1007 'req_type' => 'hotel_availability',
1008 'start_date' => $this->stay_dates[0],
1009 'end_date' => $this->stay_dates[1],
1010 'nights' => $this->countNightsOfStay(),
1011 'num_rooms' => ($tot_rooms > 0 ? $tot_rooms : 1),
1012 'adults' => [$this->getPartyGuests('adults', 0)],
1013 'children' => [$this->getPartyGuests('children', 0)],
1014 'only_rates' => 1,
1015 'wtax' => $params['wtax'] ?? null,
1016 ];
1017
1018 if ($tot_rooms > 1 && count($this->room_parties) == $tot_rooms) {
1019 // re-build list of adults and children
1020 $options['adults'] = [];
1021 $options['children'] = [];
1022 for ($i = 0; $i < $tot_rooms; $i++) {
1023 $options['adults'][] = $this->getPartyGuests('adults', $i);
1024 $options['children'][] = $this->getPartyGuests('children', $i);
1025 }
1026 }
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
1049 // merge default options with params, if any
1050 $options = array_merge($options, $params);
1051
1052 // invoke TACVBO class by injecting the options
1053 TACVBO::$getArray = true;
1054 TACVBO::$ignoreRestrictions = $this->ignore_restrictions;
1055 TACVBO::$ignoreAvailability = $this->ignore_availability;
1056 $website_rates = TACVBO::tac_av_l($options);
1057
1058 // store the error code occurred (if any)
1059 $this->errorCode = TACVBO::getErrorCode();
1060
1061 if (!is_array($website_rates)) {
1062 // critical error
1063 $this->setError(str_replace('e4j.error.', '', $website_rates));
1064 return false;
1065 }
1066
1067 if (isset($website_rates['e4j.error'])) {
1068 // calculation/availability error
1069 $this->setError($website_rates['e4j.error']);
1070 // check if reserved keys like "fullybooked" are present
1071 if (isset($website_rates['fullybooked']) && is_array($website_rates['fullybooked'])) {
1072 // store fully booked rooms array
1073 $this->fully_booked = $website_rates['fullybooked'];
1074 }
1075 // always return false
1076 return false;
1077 }
1078
1079 // optional filter by room IDs will be applied on this flow
1080 $found_rids = array_keys($website_rates);
1081 $unwanted_rids = $tot_rooms ? array_diff($found_rids, $this->room_ids) : [];
1082 foreach ($unwanted_rids as $rid) {
1083 unset($website_rates[$rid]);
1084 }
1085
1086 return $website_rates;
1087 }
1088
1089 /**
1090 * Finds the available suggestions in case of no availability previously occurred
1091 * while getting the rates. This method should be called after getRates() so that
1092 * a valid errorCode to be analized will be available, unless code is forced.
1093 * Suggestions can include closest booking dates and/or different room-guest parties.
1094 *
1095 * @param int $force_code the error code to force (no availability or party unsatisfied).
1096 * @param array $force_rooms an optional list of room IDs to consider for the suggestions.
1097 *
1098 * @return array array of alternative dates, alternative room-parties and split stays.
1099 *
1100 * @since 1.16.0 (J) - 1.6.0 (WP) list of split stays available is also returned.
1101 */
1102 public function findSuggestions($force_code = 0, $force_rooms = [])
1103 {
1104 // reset error and warning strings to start a new calculation
1105 $this->error = '';
1106 $this->warning = '';
1107
1108 // build containers for the two types of suggestions
1109 $alternative_dates = [];
1110 $alternative_parties = [];
1111 $split_stay_sols = [];
1112
1113 // the error code to parse
1114 $use_ecode = $force_code ? $force_code : $this->errorCode;
1115
1116 if (empty($use_ecode)) {
1117 // do not continue if no valid errors previously occurred or forced
1118 $this->setError('Empty error code');
1119 return [$alternative_dates, $alternative_parties, $split_stay_sols];
1120 }
1121
1122 if ($use_ecode == 7 && (count($this->fully_booked) || count($force_rooms))) {
1123 // get the closest booking dates for the compatible, yet unavailable, rooms
1124 $use_rooms = count($force_rooms) ? $force_rooms : $this->fully_booked;
1125 $alternative_dates = $this->findClosestRoomDateSolutions($use_rooms);
1126 // calculate the split stay solutions available for the compatible rooms
1127 $split_stay_sols = $this->findSplitStays($use_rooms);
1128 }
1129
1130 if ($use_ecode == 3) {
1131 // no rooms found for the given party, suggest alternative parties
1132 $active_rooms = count($force_rooms) ? $force_rooms : array_keys($this->filterPublishedRooms());
1133 // match all the available rooms in the requested or near dates
1134 $all_solutions = $this->findClosestRoomDateSolutions($active_rooms);
1135 // sort solutions by bigger rooms
1136 $all_solutions = $this->sortBiggerRoomSolutions($all_solutions);
1137 // find matching solutions for the requested party
1138 $alternative_parties = $this->matchSolutionsParty($all_solutions);
1139 }
1140
1141 return [$alternative_dates, $alternative_parties, $split_stay_sols];
1142 }
1143
1144 /**
1145 * Given a list of unavailable room IDs, yet compatible with the party and LOS requested,
1146 * we build a list of available solutions for booking split stays on the same dates. The
1147 * visibility should be public so that other Views could use just this method.
1148 *
1149 * @param array $room_ids list of unavailable, yet compatible, room IDs.
1150 * @param ?array $busy_list optional list of busy records for the involved dates.
1151 *
1152 * @return array associative list of available split stays, or empty array.
1153 *
1154 * @since 1.16.0 (J) - 1.6.0 (WP)
1155 */
1156 public function findSplitStays(array $room_ids = [], ?array $busy_list = null)
1157 {
1158 if (!$room_ids) {
1159 return [];
1160 }
1161
1162 // get all website rooms
1163 $all_rooms = $this->loadRooms();
1164
1165 // validate max occupancy for the given rooms
1166 if (count($this->room_parties) === 1) {
1167 // we only use the first party for the occupancy validation
1168 $party_adults = $this->getPartyGuests('adults', $party = 0);
1169 $party_children = $this->getPartyGuests('children', $party = 0);
1170 foreach ($room_ids as $rindex => $rid) {
1171 if (!isset($all_rooms[$rid])) {
1172 unset($room_ids[$rindex]);
1173 continue;
1174 }
1175 if ($party_adults > $all_rooms[$rid]['toadult'] || $party_children > $all_rooms[$rid]['tochild'] || ($party_adults + $party_children) > $all_rooms[$rid]['totpeople']) {
1176 // max occupancy not met
1177 unset($room_ids[$rindex]);
1178 continue;
1179 }
1180 }
1181
1182 if (!$room_ids) {
1183 return [];
1184 }
1185
1186 // reset array keys
1187 $room_ids = array_values($room_ids);
1188 }
1189
1190 // get original check-in and check-out timestamps
1191 list($orig_checkin_ts, $orig_checkout_ts) = $this->getStayDates(true);
1192 $info_from = getdate($orig_checkin_ts);
1193 $info_to = getdate($orig_checkout_ts);
1194
1195 // the final check-out date
1196 $final_checkout_ymd = date('Y-m-d', $orig_checkout_ts);
1197
1198 // count original length of stay and nights involved
1199 $tot_nights = $this->countNightsOfStay();
1200 $groupdays = VikBooking::getGroupDays($orig_checkin_ts, $orig_checkout_ts, $tot_nights);
1201
1202 if ($tot_nights < 2) {
1203 // useless to waste time on finding a split stay if not at least 2 nights
1204 return [];
1205 }
1206
1207 // load the occupied records for these dates and rooms
1208 $busy_records = !is_null($busy_list) ? $busy_list : VikBooking::loadBusyRecords($room_ids, $orig_checkin_ts, strtotime('+1 day', $orig_checkout_ts));
1209
1210 // calculate available rooms for each night
1211 $avroom_nights = [];
1212 foreach ($room_ids as $rid) {
1213 if (!isset($all_rooms[$rid])) {
1214 continue;
1215 }
1216 $room = $all_rooms[$rid];
1217 foreach ($groupdays as $gday) {
1218 $day_key = date('Y-m-d', $gday);
1219 $bfound = 0;
1220 if (!isset($busy_records[$rid])) {
1221 $busy_records[$rid] = [];
1222 }
1223 foreach ($busy_records[$rid] as $bu) {
1224 $busy_info_in = getdate($bu['checkin']);
1225 $busy_info_out = getdate($bu['checkout']);
1226 $busy_in_ts = mktime(0, 0, 0, $busy_info_in['mon'], $busy_info_in['mday'], $busy_info_in['year']);
1227 $busy_out_ts = mktime(0, 0, 0, $busy_info_out['mon'], $busy_info_out['mday'], $busy_info_out['year']);
1228 if ($gday >= $busy_in_ts && $gday == $busy_out_ts && !$this->inonout_allowed && $room['units'] < 2) {
1229 // check-ins on check-outs not allowed
1230 $bfound++;
1231 if ($bfound >= $room['units']) {
1232 break;
1233 }
1234 }
1235 if ($gday >= $busy_in_ts && $gday < $busy_out_ts) {
1236 $bfound++;
1237 if ($bfound >= $room['units']) {
1238 break;
1239 }
1240 }
1241 }
1242 if ($bfound < $room['units']) {
1243 // push this night as available for this room
1244 if (!isset($avroom_nights[$rid])) {
1245 $avroom_nights[$rid] = [];
1246 }
1247 $avroom_nights[$rid][] = $day_key;
1248 } else {
1249 // room not available on this night, make sure to unset any previous value
1250 if (isset($avroom_nights[$rid]) && in_array($day_key, $avroom_nights[$rid])) {
1251 $unav_key = array_search($day_key, $avroom_nights[$rid]);
1252 unset($avroom_nights[$rid][$unav_key]);
1253 $avroom_nights[$rid] = array_values($avroom_nights[$rid]);
1254 }
1255 }
1256 }
1257 }
1258
1259 if (!count($avroom_nights)) {
1260 // no rooms available at all, there's no way to do anything
1261 return [];
1262 }
1263
1264 // make sure all nights requested can be satisfied by at least one room
1265 foreach ($groupdays as $gday_index => $gday) {
1266 $day_key = date('Y-m-d', $gday);
1267 $day_av = false;
1268 foreach ($avroom_nights as $rid => $av_nights) {
1269 if (in_array($day_key, $av_nights)) {
1270 // night was found
1271 $day_av = true;
1272 break;
1273 }
1274 }
1275 // ensure there's availability for this day, or permit if it's the check-out day
1276 if (!$day_av && !($gday_index === (count($groupdays) - 1) && $this->inonout_allowed)) {
1277 // this night of stay is not available in any room, unable to proceed
1278 return [];
1279 }
1280 }
1281
1282 // count the number of consecutive nights per room
1283 $cons_room_nights = [];
1284 $tot_gdays = count($groupdays);
1285 foreach ($groupdays as $k => $gday) {
1286 $day_key = date('Y-m-d', $gday);
1287 if (!isset($cons_room_nights[$day_key])) {
1288 $cons_room_nights[$day_key] = [];
1289 }
1290 foreach ($avroom_nights as $rid => $av_nights) {
1291 if (in_array($day_key, $av_nights)) {
1292 if (!isset($cons_room_nights[$day_key][$rid])) {
1293 $cons_room_nights[$day_key][$rid] = [];
1294 }
1295 // count the next consecutive nights of stay
1296 $cons_room_nights[$day_key][$rid][] = $day_key;
1297 for ($j = ($k + 1); $j < $tot_gdays; $j++) {
1298 $next_day_key = date('Y-m-d', $groupdays[$j]);
1299 if (in_array($next_day_key, $av_nights)) {
1300 $cons_room_nights[$day_key][$rid][] = $next_day_key;
1301 } else {
1302 break;
1303 }
1304 }
1305 }
1306 }
1307 }
1308
1309 // sort the solutions with the highest number of consecutive nights to reduce the splits
1310 $cons_room_nights_sorted = [];
1311 foreach ($cons_room_nights as $day_key => $cons_nights) {
1312 $cons_room_nights_cnt = [];
1313 foreach ($cons_nights as $rid => $cons_dates) {
1314 $cons_room_nights_cnt[$rid] = count($cons_dates);
1315 }
1316 // sort the array in a descending order
1317 arsort($cons_room_nights_cnt);
1318 // restore sorted values in cloned array
1319 $cons_room_nights_sorted[$day_key] = [];
1320 foreach ($cons_room_nights_cnt as $rid => $tot_cons_nights) {
1321 $cons_room_nights_sorted[$day_key][$rid] = $cons_room_nights[$day_key][$rid];
1322 }
1323 }
1324 $cons_room_nights = $cons_room_nights_sorted;
1325
1326 // validate the data just built
1327 $first_day_key = date('Y-m-d', $groupdays[0]);
1328 if (!isset($cons_room_nights[$first_day_key]) || !count($cons_room_nights[$first_day_key]) || count($cons_room_nights) != count($groupdays)) {
1329 // unable to proceed
1330 return [];
1331 }
1332
1333 // remove the consecutive nights from the check-out date as this won't be a night of stay
1334 unset($cons_room_nights[$final_checkout_ymd]);
1335
1336 // build the split stay solutions
1337 $split_stay_sols = [];
1338
1339 // the number of rooms available on the first night should determine the number of split stay solutions
1340 foreach ($cons_room_nights[$first_day_key] as $start_rid => $cons_nights) {
1341 // start container of the various splits for this stay
1342 $split_stay_sol = [];
1343
1344 // calculate last consecutive night available
1345 $leave_date = end($cons_nights);
1346 $leave_date_info = getdate(strtotime($leave_date));
1347
1348 // set the check-out date to the day after the last night
1349 $checkout_ymd = date('Y-m-d', mktime(0, 0, 0, $leave_date_info['mon'], ($leave_date_info['mday'] + 1), $leave_date_info['year']));
1350
1351 // define the first stay
1352 $split_stay = [
1353 'idroom' => $start_rid,
1354 'room_name' => $all_rooms[$start_rid]['name'],
1355 'checkin' => $cons_nights[0],
1356 'checkout' => $checkout_ymd,
1357 'nights' => $this->countNightsOfStay(strtotime($cons_nights[0]), strtotime($checkout_ymd)),
1358 ];
1359
1360 // make sure this room has got a tariff defined for this number of nights of stay
1361 if (!$this->roomNightsAllowed($start_rid, $split_stay['nights'])) {
1362 // no tariffs found for this los
1363 continue;
1364 }
1365
1366 // push first stay
1367 $split_stay_sol[] = $split_stay;
1368
1369 // loop through the next stays
1370 while (isset($cons_room_nights[$checkout_ymd])) {
1371 /**
1372 * For the next splits, we use just the first available rooms, which is
1373 * the one with the highest number of consecutive nights available.
1374 */
1375 foreach ($cons_room_nights[$checkout_ymd] as $rid => $split_cons_nights) {
1376 // calculate last consecutive night available
1377 $leave_date = end($split_cons_nights);
1378 $leave_date_info = getdate(strtotime($leave_date));
1379
1380 // set the check-out date to the day after the last night
1381 $checkout_ymd = date('Y-m-d', mktime(0, 0, 0, $leave_date_info['mon'], ($leave_date_info['mday'] + 1), $leave_date_info['year']));
1382 if ($leave_date == $final_checkout_ymd) {
1383 // check-out date reached
1384 $checkout_ymd = $final_checkout_ymd;
1385 }
1386
1387 // count nights of stay
1388 $split_nights = $this->countNightsOfStay(strtotime($split_cons_nights[0]), strtotime($checkout_ymd));
1389
1390 // make sure this room has got a tariff defined for this number of nights of stay
1391 if (!$this->roomNightsAllowed($rid, $split_nights)) {
1392 // no tariffs found for this los, abort solution
1393 $split_stay_sol = [];
1394 break 2;
1395 }
1396
1397 // push split stay
1398 $split_stay_sol[] = [
1399 'idroom' => $rid,
1400 'room_name' => $all_rooms[$rid]['name'],
1401 'checkin' => $split_cons_nights[0],
1402 'checkout' => $checkout_ymd,
1403 'nights' => $split_nights,
1404 ];
1405
1406 // we try to reduce the number of splits by considering just the first room
1407 break;
1408 }
1409 }
1410
1411 if (count($split_stay_sol) < 2) {
1412 // not a split stay, but rather a fully available room
1413 continue;
1414 }
1415
1416 // push split stay solution
1417 $split_stay_sols[] = $split_stay_sol;
1418 }
1419
1420 /**
1421 * Load rooms involved in all split stays in order to validate
1422 * global/room-level restrictions and closing dates on the stay.
1423 */
1424 $rooms_involved = [];
1425 foreach ($split_stay_sols as $split_stay_sol) {
1426 foreach ($split_stay_sol as $split_stay) {
1427 if (!in_array($split_stay['idroom'], $rooms_involved)) {
1428 $rooms_involved[] = $split_stay['idroom'];
1429 }
1430 }
1431 }
1432
1433 // load restrictions for all rooms involved
1434 $all_restrictions = VikBooking::loadRestrictions(true, $rooms_involved);
1435 $glob_restrictions = VikBooking::globalRestrictions($all_restrictions);
1436 $invalid_room_restr = [];
1437
1438 // validate global restrictions
1439 if (VikBooking::validateRoomRestriction($glob_restrictions, $info_from, $info_to, $tot_nights)) {
1440 // global restrictions apply over this stay
1441 return [];
1442 }
1443
1444 // validate closing dates
1445 if (VikBooking::validateClosingDates($orig_checkin_ts, $orig_checkout_ts)) {
1446 // global closing dates apply over this stay
1447 return [];
1448 }
1449
1450 // validate restrictions at room level
1451 foreach ($rooms_involved as $rid) {
1452 // load restrictions at room level
1453 $room_level_restr = VikBooking::roomRestrictions($rid, $all_restrictions);
1454 if (VikBooking::validateRoomRestriction($room_level_restr, $info_from, $info_to, $tot_nights)) {
1455 // room-level restrictions apply over this stay
1456 $invalid_room_restr[] = $rid;
1457 }
1458 }
1459
1460 // unset the split stays with the restricted rooms (if any)
1461 $altered_sols = false;
1462 foreach ($invalid_room_restr as $rid) {
1463 foreach ($split_stay_sols as $k => $split_stay_sol) {
1464 foreach ($split_stay_sol as $split_stay) {
1465 if ($rid == $split_stay['idroom']) {
1466 // this booking split stay cannot be suggested because of this room
1467 unset($split_stay_sols[$k]);
1468 $altered_sols = true;
1469 continue 2;
1470 }
1471 }
1472 }
1473 }
1474
1475 // apply nights/transfers ratio limit (unless disabled)
1476 $nights_transfers_ratio = $this->getNightsTransfersRatio();
1477 if ($nights_transfers_ratio > 0 && $nights_transfers_ratio < 100) {
1478 // count and apply limits
1479 foreach ($split_stay_sols as $k => $split_stay_sol) {
1480 // count nights and transfers
1481 $split_stay_transfers = count($split_stay_sol) - 1;
1482 $split_stay_nights = 0;
1483 foreach ($split_stay_sol as $split_stay_room) {
1484 $split_stay_nights += $split_stay_room['nights'];
1485 }
1486 // max allowed transfers
1487 $max_transfers = round($split_stay_nights * $nights_transfers_ratio / 100, 0);
1488 if (!$split_stay_transfers || $split_stay_transfers > $max_transfers) {
1489 // unset solution
1490 unset($split_stay_sols[$k]);
1491 $altered_sols = true;
1492 }
1493 }
1494 } else {
1495 // split stays have been disabled
1496 $split_stay_sols = [];
1497 }
1498
1499 if ($altered_sols && count($split_stay_sols)) {
1500 // restore the array keys
1501 $split_stay_sols = array_values($split_stay_sols);
1502 }
1503
1504 // return the available booking split stay solutions (if any)
1505 return $split_stay_sols;
1506 }
1507
1508 /**
1509 * Returns the number of guests requested from the given room-party index.
1510 *
1511 * @param string $guest either "adults", "children" or "guests".
1512 * @param int $party the party index number, 0 by default.
1513 *
1514 * @return int the total number of guests requested in the party.
1515 */
1516 protected function getPartyGuests($guest = 'adults', $party = 0)
1517 {
1518 if (!isset($this->room_parties[$party])) {
1519 return 0;
1520 }
1521
1522 if (!strcasecmp($guest, 'adults')) {
1523 // adults
1524 return $this->room_parties[$party]['adults'];
1525 }
1526
1527 if (!strcasecmp($guest, 'children')) {
1528 // children
1529 return $this->room_parties[$party]['children'];
1530 }
1531
1532 // total guests
1533 $tot_guests = 0;
1534 foreach ($this->room_parties as $rparty) {
1535 $tot_guests += $rparty['adults'];
1536 $tot_guests += $rparty['children'];
1537 }
1538
1539 return $tot_guests;
1540 }
1541
1542 /**
1543 * Given a list of unavailable room IDs, yet compatible with the party and LOS requested,
1544 * we build a list of available dates when such rooms could be booked for the same LOS.
1545 *
1546 * @param array $room_ids list of unavailable, yet compatible, room IDs.
1547 *
1548 * @return array associative list of available room-dates, or empty array.
1549 */
1550 protected function findClosestRoomDateSolutions($room_ids = [])
1551 {
1552 if (!$room_ids) {
1553 return [];
1554 }
1555
1556 // get all website rooms
1557 $all_rooms = $this->loadRooms();
1558
1559 // get original check-in and check-out timestamps
1560 list($orig_checkin_ts, $orig_checkout_ts) = $this->getStayDates(true);
1561 $info_from = getdate($orig_checkin_ts);
1562 $info_to = getdate($orig_checkout_ts);
1563
1564 // count original length of stay
1565 $tot_nights = $this->countNightsOfStay();
1566
1567 // earliest checkin timestamp allowed
1568 $lim_past_ts = mktime(0, 0, 0, date('n'), ((int)date('j') + VikBooking::getMinDaysAdvance()), date('Y'));
1569
1570 // suggested range of dates (+/- "back and forth" days from original dates)
1571 $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']);
1572 if ($sug_from_ts < $lim_past_ts) {
1573 $sug_from_ts = $lim_past_ts;
1574 // since we are close to the requested check-in, double up the "back and forth" for the max date
1575 $this->setBackForthDays($this->getBackForthDays() * 2);
1576 }
1577 $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']);
1578 $sug_to_ts = $sug_to_ts < $sug_from_ts ? $sug_from_ts : $sug_to_ts;
1579
1580 // get days timestamps for suggestions
1581 $groupdays = [];
1582 $sug_start_info = getdate($sug_from_ts);
1583 $sug_from_midnight = mktime(0, 0, 0, $sug_start_info['mon'], $sug_start_info['mday'], $sug_start_info['year']);
1584 $sug_start_info = getdate($sug_from_midnight);
1585 while ($sug_start_info[0] <= $sug_to_ts) {
1586 array_push($groupdays, $sug_start_info[0]);
1587 $sug_start_info = getdate(mktime(0, 0, 0, $sug_start_info['mon'], ($sug_start_info['mday'] + 1), $sug_start_info['year']));
1588 }
1589
1590 // build suggestions array of dates with some availability for the given rooms
1591 $suggestions = [];
1592 $busy_records = VikBooking::loadBusyRecords($room_ids, $sug_from_ts, strtotime('+1 day', $sug_to_ts));
1593 foreach ($room_ids as $rid) {
1594 if (!isset($all_rooms[$rid])) {
1595 continue;
1596 }
1597 $room = $all_rooms[$rid];
1598 foreach ($groupdays as $gday) {
1599 $day_key = date('Y-m-d', $gday);
1600 $bfound = 0;
1601 if (!isset($busy_records[$rid])) {
1602 $busy_records[$rid] = [];
1603 }
1604 foreach ($busy_records[$rid] as $bu) {
1605 $busy_info_in = getdate($bu['checkin']);
1606 $busy_info_out = getdate($bu['checkout']);
1607 $busy_in_ts = mktime(0, 0, 0, $busy_info_in['mon'], $busy_info_in['mday'], $busy_info_in['year']);
1608 $busy_out_ts = mktime(0, 0, 0, $busy_info_out['mon'], $busy_info_out['mday'], $busy_info_out['year']);
1609 if ($gday >= $busy_in_ts && $gday == $busy_out_ts && !$this->inonout_allowed && $room['units'] < 2) {
1610 // check-ins on check-outs not allowed
1611 $bfound++;
1612 if ($bfound >= $room['units']) {
1613 break;
1614 }
1615 }
1616 if ($gday >= $busy_in_ts && $gday < $busy_out_ts) {
1617 $bfound++;
1618 if ($bfound >= $room['units']) {
1619 break;
1620 }
1621 }
1622 }
1623 if ($bfound < $room['units']) {
1624 if (!isset($suggestions[$day_key])) {
1625 $suggestions[$day_key] = [];
1626 }
1627 $room_day = $room;
1628 $room_day['units_left'] = $room['units'] - $bfound;
1629 $suggestions[$day_key] = $suggestions[$day_key] + [$rid => $room_day];
1630 }
1631 }
1632 }
1633
1634 if (!$suggestions) {
1635 // no available nights found for the prior and next "back and forth" days for the given rooms
1636 return [];
1637 }
1638
1639 // build the solutions array with keys=checkin, values=all rooms suited for the requested number of nights
1640 $solutions = [];
1641 // get all rooms available for the number of nights requested in the suggestions array of dates
1642 foreach ($suggestions as $kday => $rooms) {
1643 $day_ts_info = getdate(strtotime($kday));
1644 foreach ($rooms as $rid => $room) {
1645 $suitable = true;
1646 $room_days_av_left = [$kday => $room['units_left']];
1647 for ($i = 1; $i < $tot_nights; $i++) {
1648 $next_night = mktime(0, 0, 0, $day_ts_info['mon'], ($day_ts_info['mday'] + $i), $day_ts_info['year']);
1649 $next_night_dt = date('Y-m-d', $next_night);
1650 if (!isset($suggestions[$next_night_dt]) || !isset($suggestions[$next_night_dt][$rid])) {
1651 $suitable = false;
1652 break;
1653 }
1654 $room_days_av_left[$next_night_dt] = $suggestions[$next_night_dt][$rid]['units_left'];
1655 }
1656 if ($suitable === true) {
1657 if (!isset($solutions[$kday])) {
1658 $solutions[$kday] = [];
1659 }
1660 unset($room['units_left']);
1661 $room['days_av_left'] = $room_days_av_left;
1662 $solutions[$kday] = $solutions[$kday] + [$rid => $room];
1663 }
1664 }
1665 }
1666
1667 if (!count($solutions)) {
1668 // the requested length of stay could not be satisfied for any available night
1669 return [];
1670 }
1671
1672 // sort the solutions by the closest checkin date to the one requested
1673 $sortmap = [];
1674 $orig_checkin_ymd = date('Y-m-d', $orig_checkin_ts);
1675 foreach ($solutions as $kday => $solution) {
1676 $kdayts = strtotime($kday);
1677 $sortmap[$kdayts] = $kdayts > $orig_checkin_ts ? ($kdayts - $orig_checkin_ts) : ($orig_checkin_ts - $kdayts);
1678 if ($orig_checkin_ymd == $kday) {
1679 // the original check-in day is available, so we want it to be first, regardless of the check-in time
1680 $sortmap[$kdayts] = 1;
1681 }
1682 }
1683 asort($sortmap);
1684 $sorted = [];
1685 foreach ($sortmap as $kdayts => $v) {
1686 $kday = date('Y-m-d', $kdayts);
1687 $sorted[$kday] = $solutions[$kday];
1688 }
1689 $solutions = $sorted;
1690 unset($sorted);
1691
1692 /**
1693 * Load rooms involved in the final alternative solutions in order
1694 * to validate global/room-level restrictions and closing dates.
1695 *
1696 * @since 1.15.4 (J) - 1.5.10 (WP)
1697 */
1698 $rooms_involved = [];
1699 foreach ($solutions as $arrive_ymd => $roomsol) {
1700 foreach (array_keys($roomsol) as $rid) {
1701 if (!in_array($rid, $rooms_involved)) {
1702 $rooms_involved[] = $rid;
1703 }
1704 }
1705 }
1706
1707 // load restrictions for all rooms involved
1708 $all_restrictions = VikBooking::loadRestrictions(true, $rooms_involved);
1709 $glob_restrictions = VikBooking::globalRestrictions($all_restrictions);
1710 $room_level_restr = [];
1711
1712 foreach ($solutions as $arrive_ymd => $roomsol) {
1713 // build suggested stay dates
1714 $sug_in = getdate(strtotime($arrive_ymd));
1715 $sug_out = getdate(mktime(0, 0, 0, $sug_in['mon'], ($sug_in['mday'] + $tot_nights), $sug_in['year']));
1716 // validate global restrictions
1717 if (VikBooking::validateRoomRestriction($glob_restrictions, $sug_in, $sug_out, $tot_nights)) {
1718 // global restrictions apply over this stay
1719 unset($solutions[$arrive_ymd]);
1720 continue;
1721 }
1722 // validate closing dates
1723 if (VikBooking::validateClosingDates($sug_in[0], $sug_out[0])) {
1724 // global closing dates apply over this stay
1725 unset($solutions[$arrive_ymd]);
1726 continue;
1727 }
1728 // validate restrictions at room level
1729 foreach ($roomsol as $rid => $rdata) {
1730 if (!isset($room_level_restr[$rid])) {
1731 // load restrictions at room level
1732 $room_level_restr[$rid] = VikBooking::roomRestrictions($rid, $all_restrictions);
1733 }
1734 if (VikBooking::validateRoomRestriction($room_level_restr[$rid], $sug_in, $sug_out, $tot_nights)) {
1735 // room-level restrictions apply over this stay
1736 unset($solutions[$arrive_ymd][$rid]);
1737 if (!count($solutions[$arrive_ymd])) {
1738 // unset the entire suggested date
1739 unset($solutions[$arrive_ymd]);
1740 break;
1741 }
1742 continue;
1743 }
1744 }
1745 }
1746
1747 if (!count($solutions)) {
1748 // the calculated suggestions do not meet the restrictions or the closing dates
1749 return [];
1750 }
1751
1752 // return the solution alternative dates for all rooms available
1753 return $solutions;
1754 }
1755
1756 /**
1757 * Sorts an associative array of room-solutions by the bigger rooms.
1758 *
1759 * @param array $solutions the date solutions obtained for some rooms.
1760 *
1761 * @return array the same array sorted by bigger rooms on top.
1762 */
1763 protected function sortBiggerRoomSolutions($solutions)
1764 {
1765 if (!is_array($solutions) || !$solutions) {
1766 return $solutions;
1767 }
1768
1769 // sort rooms-solutions by max-adults, 'max-guests', 'max-children', in a descending order
1770 foreach ($solutions as $kday => $solution) {
1771 // with this sorting, we will have the bigger rooms on top to quickly fit the party requested
1772 uasort($solutions[$kday], function($a, $b) {
1773 if ($a['toadult'] == $b['toadult']) {
1774 if ($a['totpeople'] == $b['totpeople']) {
1775 return $a['tochild'] > $b['tochild'] ? -1 : 1;
1776 }
1777 return $a['totpeople'] > $b['totpeople'] ? -1 : 1;
1778 }
1779 return $a['toadult'] > $b['toadult'] ? -1 : 1;
1780 });
1781 }
1782
1783 return $solutions;
1784 }
1785
1786 /**
1787 * Given a list of available dates and rooms (solutions), attempts
1788 * to match a party of rooms that fits the party requested.
1789 *
1790 * @param array $solutions the list of available dates and related rooms.
1791 *
1792 * @return array list of alternative party solutions, if any.
1793 */
1794 protected function matchSolutionsParty($solutions)
1795 {
1796 if (!is_array($solutions) || !$solutions) {
1797 return [];
1798 }
1799
1800 // build the list of alternative parties
1801 $alternative_parties = [];
1802
1803 // build list of party guests
1804 $party_guests = [
1805 'adults' => 0,
1806 'children' => 0,
1807 ];
1808 foreach ($this->room_parties as $rparty) {
1809 $party_guests['adults'] += $rparty['adults'];
1810 $party_guests['children'] += $rparty['children'];
1811 }
1812
1813 // check if the rooms of each solution can fit the number of guests requested, unset the solution otherwise
1814 foreach ($solutions as $kday => $solution) {
1815 $solution_guests = [
1816 'adults' => 0,
1817 'children' => 0,
1818 ];
1819 foreach ($solution as $rid => $roomsol) {
1820 // count minimum units left for this room
1821 $room_min_uleft = min($roomsol['days_av_left']);
1822 // check if this solution of rooms can allocate all the guests requested
1823 if ($roomsol['totpeople'] < ($roomsol['toadult'] + $roomsol['tochild']) && !$party_guests['children']) {
1824 // in case of no children requested, we ignore them to avoid adjusting the room capacity
1825 $roomsol['tochild'] = 0;
1826 }
1827 if ($roomsol['totpeople'] < ($roomsol['toadult'] + $roomsol['tochild'])) {
1828 /**
1829 * The sum of the max_adults and max_children exceeds the max_guests: lower the adults
1830 * we can take first (if party children > 0), then the children, until sum=max_guests
1831 */
1832 while (($roomsol['toadult'] > 0 || $roomsol['tochild'] > 0)) {
1833 if (!$party_guests['children'] && $roomsol['totpeople'] == $roomsol['toadult']) {
1834 /**
1835 * When no children requested in the party, we cannot under-utilize rooms.
1836 * Break the loop without lowering the 'toadult'.
1837 */
1838 $roomsol['tochild'] = 0;
1839 break;
1840 }
1841 if ($party_guests['children'] && $solution_guests['children'] >= $party_guests['children']) {
1842 /**
1843 * If all the children requested were allocated in other solutions,
1844 * we should not under-utilize rooms by reducing the number of adults.
1845 */
1846 break;
1847 }
1848 if ($roomsol['toadult'] > 0 && $party_guests['children'] > 0 && !($roomsol['tochild'] > $party_guests['children'])) {
1849 /**
1850 * We lower first the adults that we put in this room, only if there are children in the party
1851 * and if the children in the party are more than the 'max_children' of this room.
1852 */
1853 $roomsol['toadult']--;
1854 if ($roomsol['totpeople'] >= ($roomsol['toadult'] + $roomsol['tochild'])) {
1855 break;
1856 }
1857 }
1858 if ($roomsol['tochild'] > 0) {
1859 // if the max_guests is still greater than the sum of adults+children we take, take out one child
1860 $roomsol['tochild']--;
1861 if ($roomsol['totpeople'] >= ($roomsol['toadult'] + $roomsol['tochild'])) {
1862 break;
1863 }
1864 }
1865 if ($roomsol['toadult'] > 0) {
1866 // 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
1867 $roomsol['toadult']--;
1868 if ($roomsol['totpeople'] >= ($roomsol['toadult'] + $roomsol['tochild'])) {
1869 break;
1870 }
1871 }
1872 }
1873 }
1874 $solution_guests['adults'] += $roomsol['toadult'] * $room_min_uleft;
1875 $solution_guests['children'] += $roomsol['tochild'] * $room_min_uleft;
1876 // update 'max_adults' and 'max_children' for this solution (for later guests allocation)
1877 $solution[$rid]['toadult'] = $roomsol['toadult'];
1878 $solution[$rid]['tochild'] = $roomsol['tochild'];
1879 }
1880
1881 $solutions[$kday] = $solution;
1882 if ($solution_guests['adults'] < $party_guests['adults'] || $solution_guests['children'] < $party_guests['children']) {
1883 // the guests we can allocate with the solution of this day are not enough: unset the solution
1884 unset($solutions[$kday]);
1885 continue;
1886 }
1887
1888 // if we get to this point we can suggest a booking solution for the party requested, but in different rooms
1889 if (!isset($alternative_parties[$kday])) {
1890 $alternative_parties[$kday] = [];
1891 }
1892
1893 // re-loop over the rooms in this solution to build the booking solution for this day
1894 $guests_allocated = [
1895 'adults' => 0,
1896 'children' => 0
1897 ];
1898
1899 /**
1900 * The rooms available for an alternative booking solutions have been sorted by capacity
1901 * in a descending order to quickly fit the guest party requested. However, if a smaller
1902 * and cheaper room was capable of fitting all guests, we should opt for this solution.
1903 *
1904 * @since 1.15.4 (J) - 1.5.9 (WP)
1905 */
1906 $smaller_fit_found = false;
1907 $smaller_solutions = array_reverse($solution, true);
1908 foreach ($smaller_solutions as $rid => $roomsol) {
1909 if ($party_guests['adults'] > 0 && $party_guests['adults'] > $roomsol['toadult']) {
1910 // too many adults requested for this small room
1911 continue;
1912 }
1913 if ($party_guests['children'] > 0 && $party_guests['children'] > $roomsol['tochild']) {
1914 // too many children requested for this small room
1915 continue;
1916 }
1917 if (($party_guests['adults'] + $party_guests['children']) > $roomsol['totpeople']) {
1918 // too many guests requested for this small room
1919 continue;
1920 }
1921 // we've got a fitting room which could be smaller
1922 $roomsol['guests_allocation'] = [
1923 'adults' => $party_guests['adults'],
1924 'children' => $party_guests['children'],
1925 ];
1926 array_push($alternative_parties[$kday], $roomsol);
1927 // turn flag on and break the loop
1928 $smaller_fit_found = true;
1929 break;
1930 }
1931 if ($smaller_fit_found) {
1932 // no need to parse the rooms from the largest to the smallest
1933 continue;
1934 }
1935
1936 foreach ($solution as $rid => $roomsol) {
1937 // count minimum units left for this room
1938 $room_min_uleft = min($roomsol['days_av_left']);
1939 // fullfil all the units of this room
1940 for ($units_taken = 0; $units_taken < $room_min_uleft; $units_taken++) {
1941 $current_allocation = [
1942 'adults' => 0,
1943 'children' => 0
1944 ];
1945 if ($guests_allocated['adults'] < $party_guests['adults']) {
1946 $humans_taken = $roomsol['toadult'];
1947 $missing_humans = $party_guests['adults'] - $guests_allocated['adults'];
1948 $humans_taken = $humans_taken > $missing_humans ? $missing_humans : $humans_taken;
1949
1950 $current_allocation['adults'] = $humans_taken;
1951 $guests_allocated['adults'] += $humans_taken;
1952 }
1953 if ($guests_allocated['children'] < $party_guests['children']) {
1954 $humans_taken = $roomsol['tochild'];
1955 $missing_humans = $party_guests['children'] - $guests_allocated['children'];
1956 $humans_taken = $humans_taken > $missing_humans ? $missing_humans : $humans_taken;
1957
1958 $current_allocation['children'] = $humans_taken;
1959 $guests_allocated['children'] += $humans_taken;
1960 }
1961 $roomsol['guests_allocation'] = $current_allocation;
1962 array_push($alternative_parties[$kday], $roomsol);
1963 if ($guests_allocated['adults'] >= $party_guests['adults'] && $guests_allocated['children'] >= $party_guests['children']) {
1964 // we have allocated all guests, exit the for-loop
1965 break;
1966 }
1967 }
1968 if ($guests_allocated['adults'] >= $party_guests['adults'] && $guests_allocated['children'] >= $party_guests['children']) {
1969 //we have allocated all guests with this solution, no need to loop over other rooms available in this day.
1970 break;
1971 }
1972 }
1973 }
1974
1975 // return the alternative parties found, if any
1976 return $alternative_parties;
1977 }
1978
1979 /**
1980 * Given a list of alternative dates obtained from an inquiry/request information,
1981 * composes a valid room-rate array to store the inquiry reservation. By calling this
1982 * method, the original stay dates will be overwritten.
1983 *
1984 * @param array $alt_dates the list of alternative dates found for the stay.
1985 * @param object $customer a stdClass object with the basic customer details.
1986 *
1987 * @return int the ID of the newly created inquiry reservation.
1988 */
1989 public function allocateAltDatesInquiry($alt_dates, $customer)
1990 {
1991 if (!is_array($alt_dates) || !$alt_dates) {
1992 return 0;
1993 }
1994
1995 foreach ($alt_dates as $ymd => $rooms) {
1996 // we expect just one room-type for the party, and we use the first suggestion
1997 foreach ($rooms as $rid => $alt_stay) {
1998 if (empty($alt_stay['days_av_left']) || !is_array($alt_stay['days_av_left'])) {
1999 // invalid structure
2000 continue;
2001 }
2002 // compose the new stay dates
2003 $sugg_checkin_dt = null;
2004 $sugg_checkout_dt = null;
2005 foreach ($alt_stay['days_av_left'] as $dayk => $uleft) {
2006 if (empty($sugg_checkin_dt)) {
2007 // grab the first date
2008 $sugg_checkin_dt = $dayk;
2009 }
2010 // always overwrite until last date
2011 $sugg_checkout_dt = $dayk;
2012 }
2013 // increase check-out date by one day (day after last night of stay)
2014 $sugg_out_info = getdate(strtotime($sugg_checkout_dt));
2015 $sugg_checkout_dt = date('Y-m-d', mktime(0, 0, 0, $sugg_out_info['mon'], ($sugg_out_info['mday'] + 1), $sugg_out_info['year']));
2016
2017 // set the new stay dates
2018 $this->setStayDates($sugg_checkin_dt, $sugg_checkout_dt);
2019
2020 // build the room rate plan array without any rate plan information
2021 $room_rplan = [
2022 'idroom' => $alt_stay['id'],
2023 ];
2024
2025 // create the inquiry reservation for the closest alternative dates
2026 return $this->createInquiryReservation($room_rplan, $customer);
2027 }
2028 }
2029
2030 return 0;
2031 }
2032
2033 /**
2034 * Given a list of alternative parties obtained from an inquiry/request information,
2035 * composes valid room-rate arrays to store the inquiry reservation. By calling this
2036 * method, the original stay dates and room party will be overwritten.
2037 *
2038 * @param array $alt_parties the list of alternative parties found for the stay.
2039 * @param object $customer a stdClass object with the basic customer details.
2040 *
2041 * @return int the ID of the newly created inquiry reservation.
2042 */
2043 public function allocateAltPartyInquiry($alt_parties, $customer)
2044 {
2045 if (!is_array($alt_parties) || !$alt_parties) {
2046 return 0;
2047 }
2048
2049 // build list of rooms to assign to the inquiry reservation
2050 $room_rates = [];
2051
2052 // start party counter
2053 $party_counter = 0;
2054
2055 foreach ($alt_parties as $ymd => $alt_rooms) {
2056 // we expect to have more than one room-type for the large party suggestion
2057 foreach ($alt_rooms as $alt_room) {
2058 if (empty($alt_room['guests_allocation']) || !is_array($alt_room['guests_allocation'])) {
2059 // invalid structure
2060 continue;
2061 }
2062 if (empty($alt_room['days_av_left']) || !is_array($alt_room['days_av_left'])) {
2063 // invalid structure
2064 continue;
2065 }
2066 // compose the new stay dates
2067 $sugg_checkin_dt = null;
2068 $sugg_checkout_dt = null;
2069 foreach ($alt_room['days_av_left'] as $dayk => $uleft) {
2070 if (empty($sugg_checkin_dt)) {
2071 // grab the first date
2072 $sugg_checkin_dt = $dayk;
2073 }
2074 // always overwrite until last date
2075 $sugg_checkout_dt = $dayk;
2076 }
2077 // increase check-out date by one day (day after last night of stay)
2078 $sugg_out_info = getdate(strtotime($sugg_checkout_dt));
2079 $sugg_checkout_dt = date('Y-m-d', mktime(0, 0, 0, $sugg_out_info['mon'], ($sugg_out_info['mday'] + 1), $sugg_out_info['year']));
2080
2081 // set the new stay dates
2082 $this->setStayDates($sugg_checkin_dt, $sugg_checkout_dt);
2083
2084 // set the current guests party (the first will replace the previous party, others will be pushed)
2085 $this->setRoomParty($alt_room['guests_allocation']['adults'], $alt_room['guests_allocation']['children'], ($party_counter === 0));
2086
2087 // increase party counter
2088 $party_counter++;
2089
2090 // push current room with no rate plan information
2091 array_push($room_rates, [
2092 'idroom' => $alt_room['id'],
2093 ]);
2094 }
2095
2096 if (count($room_rates)) {
2097 // we use the closest dates in the first suggestion party array
2098 break;
2099 }
2100 }
2101
2102 // count total rooms in the party
2103 $tot_room_party = count($room_rates);
2104
2105 if (!$tot_room_party) {
2106 // something went wrong
2107 return 0;
2108 }
2109
2110 // grab the main/first room reservation
2111 $room_rplan = $room_rates[0];
2112
2113 // build extra rooms
2114 $extra_rooms = [];
2115 if ($tot_room_party > 1) {
2116 // grab the remaining rooms
2117 unset($room_rates[0]);
2118 $extra_rooms = array_values($room_rates);
2119 }
2120
2121 // create the inquiry reservation for the closest alternative dates and rooms party
2122 return $this->createInquiryReservation($room_rplan, $customer, $extra_rooms);
2123 }
2124
2125 /**
2126 * Creates a new pending reservation from the inquiry/request information.
2127 * Requires a valid room-rate array to be available, or in case suggestions should
2128 * be applied, the room-rate array should be adjusted to comply with this method.
2129 *
2130 * @param array $room_rplan a room-rate array to allocate the booking.
2131 * @param object $customer a stdClass object with the basic customer details.
2132 * @param array $extra_rooms optional list of additional room-rate arrays to store
2133 * in case of alternative parties suggested.
2134 *
2135 * @return int the ID of the newly created inquiry reservation.
2136 */
2137 public function createInquiryReservation($room_rplan, $customer, $extra_rooms = array())
2138 {
2139 if (!is_array($room_rplan) || empty($room_rplan['idroom'])) {
2140 return 0;
2141 }
2142
2143 if (empty($this->stay_ts) || empty($this->room_parties)) {
2144 // no stay dates or room party set
2145 return 0;
2146 }
2147
2148 $dbo = JFactory::getDbo();
2149
2150 // build reservation object
2151 $res_obj = new stdClass;
2152 $res_obj->custdata = $customer->custdata;
2153 $res_obj->ts = time();
2154 $res_obj->status = 'standby';
2155 $res_obj->days = $this->countNightsOfStay();
2156 $res_obj->checkin = $this->stay_ts[0];
2157 $res_obj->checkout = $this->stay_ts[1];
2158 $res_obj->custmail = $customer->email;
2159 $res_obj->sid = VikBooking::getSecretLink();
2160 $res_obj->idpayment = $this->getDefaultPaymentId();
2161 $res_obj->roomsnum = count($this->room_parties);
2162 if (!empty($room_rplan['cost'])) {
2163 $res_obj->total = (float)$room_rplan['cost'];
2164 }
2165 $res_obj->adminnotes = $customer->adminnotes;
2166 $res_obj->lang = $customer->lang;
2167 $res_obj->country = $customer->country;
2168 if (!empty($room_rplan['taxes'])) {
2169 $res_obj->tot_taxes = (float)$room_rplan['taxes'];
2170 }
2171 if (!empty($room_rplan['city_taxes'])) {
2172 $res_obj->tot_city_taxes = (float)$room_rplan['city_taxes'];
2173 }
2174 $res_obj->phone = $customer->phone;
2175 $res_obj->type = 'inquiry';
2176
2177 // store record
2178 if (!$dbo->insertObject('#__vikbooking_orders', $res_obj, 'id')) {
2179 // could not store the booking record
2180 return 0;
2181 }
2182
2183 // get the ID of the newly created reservation
2184 $res_id = $res_obj->id;
2185
2186 // check if mandatory options should be assigned (not in case of suggestions)
2187 $room_options = null;
2188 if (!empty($room_rplan['idprice']) && isset($res_obj->tot_city_taxes)) {
2189 $mand_taxes = VikBooking::getMandatoryTaxesFees([$room_rplan['idroom']], $this->getPartyGuests('adults', 0), $res_obj->days);
2190 if (is_array($mand_taxes) && !empty($mand_taxes['options'])) {
2191 $room_options = implode(';', $mand_taxes['options']);
2192 }
2193 }
2194
2195 // build room-reservation object
2196 $room_res_obj = new stdClass;
2197 $room_res_obj->idorder = $res_id;
2198 $room_res_obj->idroom = $room_rplan['idroom'];
2199 $room_res_obj->adults = $this->getPartyGuests('adults', 0);
2200 $room_res_obj->children = $this->getPartyGuests('children', 0);
2201 if (!empty($room_rplan['idprice'])) {
2202 $room_res_obj->idtar = $this->getTariffId($room_rplan['idroom'], $room_rplan['idprice'], $res_obj->days);
2203 }
2204 $room_res_obj->optionals = $room_options;
2205 $room_res_obj->t_first_name = $customer->name;
2206 $room_res_obj->t_last_name = $customer->lname;
2207
2208 // store record
2209 if (!$dbo->insertObject('#__vikbooking_ordersrooms', $room_res_obj, 'id')) {
2210 // could not store the room-reservation record
2211 return $res_id;
2212 }
2213
2214 // in case of suggestions for alternative room parties, parse the extra rooms
2215 $party_index = 1;
2216 foreach ($extra_rooms as $extra_room) {
2217 if (!is_array($extra_room) || empty($extra_room['idroom'])) {
2218 continue;
2219 }
2220 // build additional room-reservation object
2221 $room_res_obj = new stdClass;
2222 $room_res_obj->idorder = $res_id;
2223 $room_res_obj->idroom = $extra_room['idroom'];
2224 $room_res_obj->adults = $this->getPartyGuests('adults', $party_index);
2225 $room_res_obj->children = $this->getPartyGuests('children', $party_index);
2226
2227 // store record
2228 $dbo->insertObject('#__vikbooking_ordersrooms', $room_res_obj, 'id');
2229
2230 // increase room party index
2231 $party_index++;
2232 }
2233
2234 // return the newly created reservation ID
2235 return $res_id;
2236 }
2237
2238 /**
2239 * Attempts to get the tariff ID for the given room, rate plan and nights.
2240 *
2241 * @param int $room_id the ID of the VBO room.
2242 * @param int $rplan_id the rate plan ID in VBO.
2243 * @param int $nights the number of nights of stay.
2244 *
2245 * @return int|null the tariff ID or null.
2246 */
2247 public function getTariffId($room_id, $rplan_id, $nights)
2248 {
2249 if (empty($room_id) || empty($rplan_id) || $nights < 1) {
2250 return null;
2251 }
2252
2253 $dbo = JFactory::getDbo();
2254
2255 $q = "SELECT `id` FROM `#__vikbooking_dispcost` WHERE `idroom`={$room_id} AND `days`={$nights} AND `idprice`={$rplan_id}";
2256 $dbo->setQuery($q, 0, 1);
2257 $res = $dbo->loadResult();
2258
2259 if ($res) {
2260 return (int)$res;
2261 }
2262
2263 return null;
2264 }
2265
2266 /**
2267 * Grabs the details of a given booking id.
2268 *
2269 * @param int $bid the booking ID to look for.
2270 *
2271 * @return array empty array or booking record details.
2272 */
2273 public function getBookingDetails($bid)
2274 {
2275 $bid = (int)$bid;
2276
2277 $dbo = JFactory::getDbo();
2278
2279 $q = "SELECT `o`.*, `co`.`idcustomer`, CONCAT_WS(' ', `c`.`first_name`, `c`.`last_name`) AS `customer_fullname`, `c`.`country` AS `customer_country`, `c`.`pic`
2280 FROM `#__vikbooking_orders` AS `o`
2281 LEFT JOIN `#__vikbooking_customers_orders` AS `co` ON `co`.`idorder`=`o`.`id`
2282 LEFT JOIN `#__vikbooking_customers` AS `c` ON `c`.`id`=`co`.`idcustomer`
2283 WHERE `o`.`id`={$bid}";
2284 $dbo->setQuery($q, 0, 1);
2285 $row = $dbo->loadAssoc();
2286 if ($row) {
2287 return $row;
2288 }
2289
2290 return [];
2291 }
2292
2293 /**
2294 * Validates if the room allows the given number of nights of stay by checking if a
2295 * tariff is defined for the given length of stay. Useful for particular rate tables.
2296 *
2297 * @param int $room_id the ID of the VBO room.
2298 * @param int $nights the number of nights of stay.
2299 *
2300 * @return bool true if a tariff is found between min and max nights.
2301 *
2302 * @since 1.16.3 (J) - 1.6.3 (WP)
2303 */
2304 public function roomNightsAllowed($room_id, $nights)
2305 {
2306 if (!isset($this->min_max_los_tariffs_map[$room_id])) {
2307 $dbo = JFactory::getDbo();
2308
2309 $q = $dbo->getQuery(true)
2310 ->select('MIN(' . $dbo->qn('t.days') . ') AS ' . $dbo->qn('min_nights'))
2311 ->select('MAX(' . $dbo->qn('t.days') . ') AS ' . $dbo->qn('max_nights'))
2312 ->from($dbo->qn('#__vikbooking_dispcost', 't'))
2313 ->where($dbo->qn('t.idroom') . ' = ' . (int)$room_id);
2314
2315 $dbo->setQuery($q, 0, 1);
2316 $tariffs = $dbo->loadObject();
2317
2318 if (!$tariffs || !$tariffs->min_nights || !$tariffs->max_nights) {
2319 return false;
2320 }
2321
2322 // set values
2323 $this->min_max_los_tariffs_map[$room_id] = [$tariffs->min_nights, $tariffs->max_nights];
2324 }
2325
2326 // check if the number of nights of stay is within the range of tariffs los map
2327 return ($nights >= min($this->min_max_los_tariffs_map[$room_id]) && $nights <= max($this->min_max_los_tariffs_map[$room_id]));
2328 }
2329
2330 /**
2331 * Sets the stay dates, check-in and check-out date timestamps.
2332 *
2333 * @param string $from check-in date string in Y-m-d or VBO format.
2334 * @param string $to check-out date string in Y-m-d or VBO format.
2335 *
2336 * @return self
2337 */
2338 public function setStayDates($from, $to)
2339 {
2340 if (empty($from) || empty($to)) {
2341 return $this;
2342 }
2343
2344 $checkinh = 0;
2345 $checkinm = 0;
2346 $checkouth = 0;
2347 $checkoutm = 0;
2348 $timeopst = VikBooking::getTimeOpenStore();
2349 if (is_array($timeopst)) {
2350 if ($timeopst[0] < $timeopst[1]) {
2351 // check-in not allowed on a day where there is already a check out (no arrivals/depatures on the same day)
2352 $this->inonout_allowed = false;
2353 }
2354 $opent = VikBooking::getHoursMinutes($timeopst[0]);
2355 $closet = VikBooking::getHoursMinutes($timeopst[1]);
2356 $checkinh = $opent[0];
2357 $checkinm = $opent[1];
2358 $checkouth = $closet[0];
2359 $checkoutm = $closet[1];
2360 }
2361 $from_ts = VikBooking::getDateTimestamp($from, $checkinh, $checkinm);
2362 $to_ts = VikBooking::getDateTimestamp($to, $checkouth, $checkoutm);
2363
2364 // set stay dates and timestamps
2365 $this->stay_dates = [date('Y-m-d', $from_ts), date('Y-m-d', $to_ts)];
2366 $this->stay_ts = [$from_ts, $to_ts];
2367
2368 return $this;
2369 }
2370
2371 /**
2372 * Returns the current stay dates or timestamps.
2373 *
2374 * @param bool $ts whether to get the date timestamps.
2375 *
2376 * @return array the current stay dates or timestamps.
2377 */
2378 public function getStayDates($ts = false)
2379 {
2380 return $ts ? $this->stay_ts : $this->stay_dates;
2381 }
2382
2383 /**
2384 * Sets a room party with adults and children, by optionally replacing the others.
2385 *
2386 * @param int $adults the number of adults for this room party.
2387 * @param int $children the number of children for this room party.
2388 * @param bool $replace if true, any previously set room party will be replaced.
2389 *
2390 * @return self
2391 */
2392 public function setRoomParty($adults, $children = 0, $replace = false)
2393 {
2394 $room_party = [
2395 'adults' => $adults,
2396 'children' => $children,
2397 ];
2398
2399 if ($replace) {
2400 $this->room_parties = [$room_party];
2401 } else {
2402 array_push($this->room_parties, $room_party);
2403 }
2404
2405 return $this;
2406 }
2407
2408 /**
2409 * Returns the current room parties array.
2410 *
2411 * @return array the current room parties.
2412 */
2413 public function getRoomParties()
2414 {
2415 return $this->room_parties;
2416 }
2417
2418 /**
2419 * Sets and returns the flag to ignore the restrictions.
2420 *
2421 * @param bool $set the boolean status to set.
2422 *
2423 * @return bool the current ignore status.
2424 */
2425 public function ignoreRestrictions($set = null)
2426 {
2427 if (is_bool($set)) {
2428 $this->ignore_restrictions = $set;
2429 }
2430
2431 return $this->ignore_restrictions;
2432 }
2433
2434 /**
2435 * Sets and returns the flag to ignore the rooms availability.
2436 *
2437 * @param bool $set the boolean status to set.
2438 *
2439 * @return bool the current ignore status.
2440 *
2441 * @since 1.16.10 (J) - 1.6.10 (WP)
2442 */
2443 public function ignoreAvailability($set = null)
2444 {
2445 if (is_bool($set)) {
2446 $this->ignore_availability = $set;
2447 }
2448
2449 return $this->ignore_availability;
2450 }
2451
2452 /**
2453 * Takes the first payment ID "string", if available.
2454 *
2455 * @return string|null
2456 */
2457 protected function getDefaultPaymentId()
2458 {
2459 $dbo = JFactory::getDbo();
2460
2461 $q = "SELECT `id`, `name` FROM `#__vikbooking_gpayments` WHERE `published`=1 ORDER BY `ordering` ASC";
2462 $dbo->setQuery($q, 0, 1);
2463 $data = $dbo->loadAssoc();
2464
2465 if ($data) {
2466 return $data['id'] . '=' . $data['name'];
2467 }
2468
2469 return null;
2470 }
2471
2472 /**
2473 * Returns the current nights/transfers ratio for split stays.
2474 *
2475 * @return int the nights transfers ratio for the percent calculation.
2476 *
2477 * @since 1.16.0 (J) - 1.6.0 (WP)
2478 */
2479 public function getNightsTransfersRatio()
2480 {
2481 return $this->nights_transfers_ratio;
2482 }
2483
2484 /**
2485 * Returns the default nights/transfers ratio for split stays.
2486 *
2487 * @return int the nights transfers ratio defined in the configuration.
2488 *
2489 * @since 1.16.0 (J) - 1.6.0 (WP)
2490 */
2491 public function getDefaultNightsTransfersRatio()
2492 {
2493 static $transferRatio = null;
2494
2495 if ($transferRatio === null) {
2496 $config = VBOFactory::getConfig();
2497 $transferRatio = (int) $config->get('split_stay_ratio', 50);
2498 }
2499
2500 return $transferRatio;
2501 }
2502
2503 /**
2504 * Sets the nights/transfers ratio for split stays. Use it to start applying limits.
2505 *
2506 * @param ?int $ratio Optional ratio integer value, or default value will apply.
2507 *
2508 * @return self
2509 *
2510 * @since 1.16.0 (J) - 1.6.0 (WP)
2511 */
2512 public function setNightsTransfersRatio(?int $ratio = null)
2513 {
2514 if (is_int($ratio)) {
2515 $this->nights_transfers_ratio = $ratio;
2516 } else {
2517 $this->nights_transfers_ratio = $this->getDefaultNightsTransfersRatio();
2518 }
2519
2520 return $this;
2521 }
2522
2523 /**
2524 * Tells whether we need to behave for the front-end booking process.
2525 *
2526 * @return bool true if we are in the front-end booking process or false.
2527 *
2528 * @since 1.16.0 (J) - 1.6.0 (WP)
2529 */
2530 public function isFrontBooking()
2531 {
2532 return (bool) $this->is_front_booking;
2533 }
2534
2535 /**
2536 * Toggles the flag to behave for the front-end booking process.
2537 *
2538 * @return self
2539 *
2540 * @since 1.16.0 (J) - 1.6.0 (WP)
2541 */
2542 public function setIsFrontBooking($is_front = true)
2543 {
2544 $this->is_front_booking = (bool)$is_front;
2545
2546 return $this;
2547 }
2548
2549 /**
2550 * This helper method aims to collect the stay dates of each room in
2551 * a booking with split stay. Records will be loaded by ID ascending,
2552 * so in the same exact way as the rooms get stored. For this reason,
2553 * it is then possible to match the stay dates of a room by array-key,
2554 * even if bookings with split stay should always have different room IDs.
2555 *
2556 * @param int $bid the website reservation ID.
2557 *
2558 * @return array the list of busy records with stay dates information.
2559 *
2560 * @since 1.16.0 (J) - 1.6.0 (WP)
2561 */
2562 public function loadSplitStayBusyRecords($bid)
2563 {
2564 $dbo = JFactory::getDbo();
2565
2566 $bid = (int)$bid;
2567
2568 /**
2569 * It is fundamental to keep the ID column as the busy record ID
2570 * to allow the room switching in case of booking modification.
2571 */
2572 $q = "SELECT `ob`.`idorder`, `b`.`id`, `b`.`idroom`, `b`.`checkin`, `b`.`checkout`, `b`.`sharedcal`
2573 FROM `#__vikbooking_ordersbusy` AS `ob`
2574 LEFT JOIN `#__vikbooking_busy` AS `b` ON `ob`.`idbusy`=`b`.`id`
2575 WHERE `ob`.`idorder`={$bid}
2576 ORDER BY `b`.`sharedcal` ASC, `b`.`id` ASC;";
2577 $dbo->setQuery($q);
2578 $records = $dbo->loadAssocList();
2579
2580 if (!$records) {
2581 // the booking may no longer exist, or maybe it was cancelled
2582 return [];
2583 }
2584
2585 return $records;
2586 }
2587
2588 /**
2589 * Returns a list of the tax rate records.
2590 *
2591 * @return array the list of tax rates.
2592 *
2593 * @since 1.16.0 (J) - 1.6.0 (WP)
2594 */
2595 public function getTaxRates()
2596 {
2597 $dbo = JFactory::getDbo();
2598 $dbo->setQuery("SELECT * FROM `#__vikbooking_iva`;");
2599
2600 return $dbo->loadAssocList();
2601 }
2602
2603 /**
2604 * Sets the IDs of the rooms to filter or use.
2605 *
2606 * @param mixed $room_ids the list of room IDs to filter or use, or int room ID.
2607 *
2608 * @return self
2609 */
2610 public function setRoomIds($room_ids = [])
2611 {
2612 if (is_scalar($room_ids)) {
2613 // single room ID integer
2614 $room_ids = [$room_ids];
2615 }
2616
2617 $this->room_ids = $room_ids;
2618
2619 return $this;
2620 }
2621
2622 /**
2623 * Returns the current room ids to filter or use.
2624 *
2625 * @return array the current room ids.
2626 */
2627 public function getRoomIds()
2628 {
2629 return $this->room_ids;
2630 }
2631
2632 /**
2633 * In case of no availability, overrides the default number of days to check
2634 * prior and after the originally requested check-in and check-out dates.
2635 *
2636 * @param int $days the total number of days to use back and forth.
2637 *
2638 * @return self
2639 */
2640 public function setBackForthDays($days)
2641 {
2642 if (is_int($days) && $days >= 0) {
2643 $this->back_and_forth = $days;
2644 }
2645
2646 return $this;
2647 }
2648
2649 /**
2650 * Returns the current number of back and forth days.
2651 *
2652 * @return int the current number of days.
2653 */
2654 public function getBackForthDays()
2655 {
2656 return $this->back_and_forth;
2657 }
2658
2659 /**
2660 * Sets warning messages by concatenating the existing ones.
2661 *
2662 * @param string $str
2663 *
2664 * @return self
2665 */
2666 protected function setWarning($str)
2667 {
2668 $this->warning .= $str . "\n";
2669
2670 return $this;
2671 }
2672
2673 /**
2674 * Gets the current warning string.
2675 *
2676 * @return string
2677 */
2678 public function getWarning()
2679 {
2680 return rtrim($this->warning, "\n");
2681 }
2682
2683 /**
2684 * Sets errors by concatenating the existing ones.
2685 *
2686 * @param string $str
2687 *
2688 * @return self
2689 */
2690 protected function setError($str)
2691 {
2692 $this->error .= $str . "\n";
2693
2694 return $this;
2695 }
2696
2697 /**
2698 * Gets the current error string.
2699 *
2700 * @return string
2701 */
2702 public function getError()
2703 {
2704 return rtrim($this->error, "\n");
2705 }
2706
2707 /**
2708 * Gets the current error code.
2709 *
2710 * @return int
2711 */
2712 public function getErrorCode()
2713 {
2714 return $this->errorCode;
2715 }
2716 }
2717