PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.16
VikBooking Hotel Booking Engine & PMS v1.8.16
1.8.16 1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 All 37 releases
← All changes | admin/helpers/availability.php +2728 -0 1.6.1 → 1.8.16 View file →
@@ -1,0 +1,2728 @@
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 +}