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

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

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