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

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

2,053 lines 61.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage com_vikbooking
5 * @author Alessio Gaggii - e4j - Extensionsforjoomla.com
6 * @copyright Copyright (C) 2022 e4j - Extensionsforjoomla.com. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 * @link https://vikwp.com
9 */
10
11 defined('ABSPATH') or die('No script kiddies please!');
12
13 /**
14 * Availability handler class for Vik Booking.
15 * Also used to handle website inquiry reservations.
16 *
17 * @since 1.15.0 (J) - 1.5.0 (WP)
18 */
19 class VikBookingAvailability
20 {
21 /**
22 * The singleton instance of the class.
23 *
24 * @var VikBookingAvailability
25 */
26 protected static $instance = null;
27
28 /**
29 * An array containing the stay dates.
30 *
31 * @var array
32 */
33 protected $stay_dates = [];
34
35 /**
36 * An array containing the stay date timestamps.
37 *
38 * @var array
39 */
40 protected $stay_ts = [];
41
42 /**
43 * An array containing the room parties.
44 *
45 * @var array
46 */
47 protected $room_parties = [];
48
49 /**
50 * The total number of days to go "back and forth".
51 *
52 * @var int
53 */
54 protected $back_and_forth = 14;
55
56 /**
57 * A list of the room ids to be checked.
58 *
59 * @var array
60 */
61 protected $room_ids = [];
62
63 /**
64 * Whether to ignore restrictions.
65 *
66 * @var bool
67 */
68 protected $ignore_restrictions = false;
69
70 /**
71 * Whether check-ins on check-outs are allowed.
72 *
73 * @var bool
74 */
75 protected $inonout_allowed = true;
76
77 /**
78 * The percent ratio for nights/transfers in split stays.
79 *
80 * @var int
81 */
82 protected $nights_transfers_ratio = 100;
83
84 /**
85 * Whether we need to behave for the front-end booking process.
86 *
87 * @var bool
88 */
89 protected $is_front_booking = false;
90
91 /**
92 * The warning string occurred.
93 *
94 * @var string
95 */
96 protected $warning = '';
97
98 /**
99 * The error string occurred.
100 *
101 * @var string
102 */
103 protected $error = '';
104
105 /**
106 * The last error code occurred in TACVBO.
107 *
108 * @var int
109 */
110 protected $errorCode = 0;
111
112 /**
113 * A list of fully booked room ids.
114 *
115 * @var array
116 */
117 protected $fully_booked = [];
118
119 /**
120 * Associative list of all rooms.
121 *
122 * @var array
123 */
124 protected $all_rooms = [];
125
126 /**
127 * Associative list of all rate plans.
128 *
129 * @var array
130 */
131 protected $all_rplans = [];
132
133 /**
134 * Map of min/max LOS tariffs defined per room.
135 *
136 * @var array
137 */
138 protected $min_max_los_tariffs_map = [];
139
140 /**
141 * Class constructor is protected.
142 *
143 * @see getInstance()
144 */
145 protected function __construct()
146 {
147 // load dependencies
148 if (!class_exists('TACVBO')) {
149 require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'tac.vikbooking.php';
150 }
151 }
152
153 /**
154 * Returns the global object, either
155 * a new instance or the existing instance
156 * if the class was already instantiated.
157 *
158 * @return self A new instance of the class.
159 */
160 public static function getInstance()
161 {
162 if (is_null(static::$instance)) {
163 static::$instance = new static();
164 }
165
166 return static::$instance;
167 }
168
169 /**
170 * Counts the total number of nights of stay according to the stay dates.
171 *
172 * @param int $from_ts optional check-in timestamp.
173 * @param int $to_ts optional check-out timestamp.
174 *
175 * @return int the total number of nights of stay.
176 *
177 * @since 1.16.0 (J) - 1.6.0 (WP) added args to make this an helper method.
178 */
179 public function countNightsOfStay($from_ts = null, $to_ts = null)
180 {
181 if (!count($this->stay_ts) && (empty($from_ts) || empty($to_ts))) {
182 return 1;
183 }
184
185 if (!empty($from_ts) && !empty($to_ts)) {
186 $use_from = $from_ts;
187 $use_to = $to_ts;
188 } else {
189 $use_from = $this->stay_ts[0];
190 $use_to = $this->stay_ts[1];
191 }
192
193 $secdiff = $use_to - $use_from;
194 $daysdiff = $secdiff / 86400;
195 if (is_int($daysdiff)) {
196 $daysdiff = $daysdiff < 1 ? 1 : $daysdiff;
197 } else {
198 if ($daysdiff < 1) {
199 $daysdiff = 1;
200 } else {
201 $sum = floor($daysdiff) * 86400;
202 $newdiff = $secdiff - $sum;
203 $maxhmore = VikBooking::getHoursMoreRb() * 3600;
204 if ($maxhmore >= $newdiff) {
205 $daysdiff = floor($daysdiff);
206 } else {
207 $daysdiff = ceil($daysdiff);
208 }
209 }
210 }
211
212 return $daysdiff;
213 }
214
215 /**
216 * Explains the error code occurred or passed by using translation strings.
217 *
218 * @param int $force_code optional error code to explain.
219 *
220 * @return string the explanation of the error, or an empty string.
221 */
222 public function explainErrorCode($force_code = 0)
223 {
224 // the error code to parse
225 $use_ecode = $force_code ? $force_code : $this->errorCode;
226
227 if (empty($use_ecode)) {
228 return '';
229 }
230
231 /**
232 * Error code identifier:
233 *
234 * 1 = missing/invalid request options.
235 * 2 = invalid authentication.
236 * 3 = no rooms found for the given party.
237 * 4 = not compliant with booking restrictions.
238 * 5 = not compliant with global closing dates.
239 * 6 = no rates defined for the given length of stay.
240 * 7 = no availability for the dates requested.
241 * 8 = no rooms available due to restrictions at room or rate plan level.
242 */
243
244 switch ($use_ecode) {
245 case 1:
246 return 'Missing or invalid request options.';
247 case 2:
248 return 'Invalid request authentication.';
249 case 3:
250 return JText::translate('VBO_AV_ECODE_3');
251 case 4:
252 return 'Not compliant with the booking restrictions.';
253 case 5:
254 return 'Not compliant with the global closing dates.';
255 case 6:
256 return 'No rates defined for the given length of stay.';
257 case 7:
258 return JText::translate('VBO_AV_ECODE_7');
259 case 8:
260 return 'No rooms available due to room or rate plan restrictions.';
261 default:
262 return 'Unknown error code.';
263 }
264
265 }
266
267 /**
268 * Loads all rooms in VBO and maps them into an associative array.
269 *
270 * @return array the associative list of rooms.
271 */
272 public function loadRooms()
273 {
274 if ($this->all_rooms) {
275 // return previously cached array if available
276 return $this->all_rooms;
277 }
278
279 $dbo = JFactory::getDbo();
280
281 $q = "SELECT `id`, `name`, `img`, `idcat`, `avail`, `units`, `fromadult`, `toadult`,
282 `fromchild`, `tochild`, `totpeople`, `mintotpeople` FROM `#__vikbooking_rooms`
283 ORDER BY `avail` DESC, `name` ASC;";
284 $dbo->setQuery($q);
285 $room_rows = $dbo->loadAssocList();
286
287 if (!$room_rows) {
288 return $this->all_rooms;
289 }
290
291 if ($this->isFrontBooking()) {
292 // apply translations on rooms
293 $vbo_tn = VikBooking::getTranslator();
294 $vbo_tn->translateContents($room_rows, '#__vikbooking_rooms');
295 }
296
297 foreach ($room_rows as $room) {
298 $this->all_rooms[$room['id']] = $room;
299 }
300
301 return $this->all_rooms;
302 }
303
304 /**
305 * Sets the current rooms as an associative array of information. The
306 * array keys represent the room IDs as an associative array of details.
307 *
308 * @param array $rooms the associatve list of rooms.
309 *
310 * @return self
311 */
312 public function setRooms(array $rooms = [])
313 {
314 $this->all_rooms = $rooms;
315
316 return $this;
317 }
318
319 /**
320 * Filters all rooms by keeping just the ones published/available.
321 *
322 * @return array associative array of published (available) rooms.
323 */
324 public function filterPublishedRooms()
325 {
326 $rooms = $this->loadRooms();
327
328 foreach ($rooms as $k => $room) {
329 if (!$room['avail']) {
330 unset($rooms[$k]);
331 }
332 }
333
334 return $rooms;
335 }
336
337 /**
338 * Loads all rate plans in VBO and maps them into an associative array.
339 *
340 * @return array the associative list of rate plans.
341 */
342 public function loadRatePlans()
343 {
344 if (count($this->all_rplans)) {
345 // return previously cached array if available
346 return $this->all_rplans;
347 }
348
349 $dbo = JFactory::getDbo();
350
351 $rate_plans = [];
352
353 $q = "SELECT * FROM `#__vikbooking_prices` ORDER BY `name` ASC;";
354 $dbo->setQuery($q);
355 $rplan_rows = $dbo->loadAssocList();
356
357 if ($rplan_rows) {
358 foreach ($rplan_rows as $rplan) {
359 $rate_plans[$rplan['id']] = $rplan;
360 }
361 // sort rate plans
362 $rate_plans = VikBooking::sortRatePlans($rate_plans, true);
363 }
364
365 $this->all_rplans = $rate_plans;
366
367 return $rate_plans;
368 }
369
370 /**
371 * Gets the available room rates for the specified dates, party and rooms.
372 *
373 * @param array $params optional list of options to be forced for TACVBO.
374 *
375 * @return mixed boolean false in case of errors or array result of TACVBO class.
376 */
377 public function getRates($params = [])
378 {
379 if (!count($this->stay_dates)) {
380 $this->setError('No dates provided');
381 return false;
382 }
383
384 if (!count($this->room_parties)) {
385 $this->setError('No room party provided');
386 return false;
387 }
388
389 // count injected rooms, if any
390 $tot_rooms = count($this->room_ids);
391
392 // build options array for TACVBO
393 $options = [
394 'start_date' => $this->stay_dates[0],
395 'end_date' => $this->stay_dates[1],
396 'nights' => $this->countNightsOfStay(),
397 'num_rooms' => ($tot_rooms > 0 ? $tot_rooms : 1),
398 'adults' => [$this->getPartyGuests('adults', 0)],
399 'children' => [$this->getPartyGuests('children', 0)],
400 'only_rates' => 1,
401 ];
402
403 if ($tot_rooms > 1 && count($this->room_parties) == $tot_rooms) {
404 // re-build list of adults and children
405 $options['adults'] = [];
406 $options['children'] = [];
407 for ($i = 0; $i < $tot_rooms; $i++) {
408 $options['adults'][] = $this->getPartyGuests('adults', $i);
409 $options['children'][] = $this->getPartyGuests('children', $i);
410 }
411 }
412
413 // merge default options with params, if any
414 $options = array_merge($options, $params);
415
416 // invoke TACVBO class by injecting the options
417 TACVBO::$getArray = true;
418 TACVBO::$ignoreRestrictions = $this->ignore_restrictions;
419 $website_rates = TACVBO::tac_av_l($options);
420
421 // store the error code occurred (if any)
422 $this->errorCode = TACVBO::getErrorCode();
423
424 if (!is_array($website_rates)) {
425 // critical error
426 $this->setError(str_replace('e4j.error.', '', $website_rates));
427 return false;
428 }
429
430 if (isset($website_rates['e4j.error'])) {
431 // calculation/availability error
432 $this->setError($website_rates['e4j.error']);
433 // check if reserved keys like "fullybooked" are present
434 if (isset($website_rates['fullybooked']) && is_array($website_rates['fullybooked'])) {
435 // store fully booked rooms array
436 $this->fully_booked = $website_rates['fullybooked'];
437 }
438 // always return false
439 return false;
440 }
441
442 // optional filter by room IDs will be applied on this flow
443 $found_rids = array_keys($website_rates);
444 $unwanted_rids = $tot_rooms ? array_diff($found_rids, $this->room_ids) : [];
445 foreach ($unwanted_rids as $rid) {
446 unset($website_rates[$rid]);
447 }
448
449 return $website_rates;
450 }
451
452 /**
453 * Finds the available suggestions in case of no availability previously occurred
454 * while getting the rates. This method should be called after getRates() so that
455 * a valid errorCode to be analized will be available, unless code is forced.
456 * Suggestions can include closest booking dates and/or different room-guest parties.
457 *
458 * @param int $force_code the error code to force (no availability or party unsatisfied).
459 * @param array $force_rooms an optional list of room IDs to consider for the suggestions.
460 *
461 * @return array array of alternative dates, alternative room-parties and split stays.
462 *
463 * @since 1.16.0 (J) - 1.6.0 (WP) list of split stays available is also returned.
464 */
465 public function findSuggestions($force_code = 0, $force_rooms = [])
466 {
467 // reset error and warning strings to start a new calculation
468 $this->error = '';
469 $this->warning = '';
470
471 // build containers for the two types of suggestions
472 $alternative_dates = [];
473 $alternative_parties = [];
474 $split_stay_sols = [];
475
476 // the error code to parse
477 $use_ecode = $force_code ? $force_code : $this->errorCode;
478
479 if (empty($use_ecode)) {
480 // do not continue if no valid errors previously occurred or forced
481 $this->setError('Empty error code');
482 return [$alternative_dates, $alternative_parties, $split_stay_sols];
483 }
484
485 if ($use_ecode == 7 && (count($this->fully_booked) || count($force_rooms))) {
486 // get the closest booking dates for the compatible, yet unavailable, rooms
487 $use_rooms = count($force_rooms) ? $force_rooms : $this->fully_booked;
488 $alternative_dates = $this->findClosestRoomDateSolutions($use_rooms);
489 // calculate the split stay solutions available for the compatible rooms
490 $split_stay_sols = $this->findSplitStays($use_rooms);
491 }
492
493 if ($use_ecode == 3) {
494 // no rooms found for the given party, suggest alternative parties
495 $active_rooms = count($force_rooms) ? $force_rooms : array_keys($this->filterPublishedRooms());
496 // match all the available rooms in the requested or near dates
497 $all_solutions = $this->findClosestRoomDateSolutions($active_rooms);
498 // sort solutions by bigger rooms
499 $all_solutions = $this->sortBiggerRoomSolutions($all_solutions);
500 // find matching solutions for the requested party
501 $alternative_parties = $this->matchSolutionsParty($all_solutions);
502 }
503
504 return [$alternative_dates, $alternative_parties, $split_stay_sols];
505 }
506
507 /**
508 * Given a list of unavailable room IDs, yet compatible with the party and LOS requested,
509 * we build a list of available solutions for booking split stays on the same dates. The
510 * visibility should be public so that other Views could use just this method.
511 *
512 * @param array $room_ids list of unavailable, yet compatible, room IDs.
513 * @param array $busy_list optional list of busy records for the involved dates.
514 *
515 * @return array associative list of available split stays, or empty array.
516 *
517 * @since 1.16.0 (J) - 1.6.0 (WP)
518 */
519 public function findSplitStays(array $room_ids = [], array $busy_list = null)
520 {
521 if (!$room_ids) {
522 return [];
523 }
524
525 // get all website rooms
526 $all_rooms = $this->loadRooms();
527
528 // validate max occupancy for the given rooms
529 if (count($this->room_parties) === 1) {
530 // we only use the first party for the occupancy validation
531 $party_adults = $this->getPartyGuests('adults', $party = 0);
532 $party_children = $this->getPartyGuests('children', $party = 0);
533 foreach ($room_ids as $rindex => $rid) {
534 if (!isset($all_rooms[$rid])) {
535 unset($room_ids[$rindex]);
536 continue;
537 }
538 if ($party_adults > $all_rooms[$rid]['toadult'] || $party_children > $all_rooms[$rid]['tochild'] || ($party_adults + $party_children) > $all_rooms[$rid]['totpeople']) {
539 // max occupancy not met
540 unset($room_ids[$rindex]);
541 continue;
542 }
543 }
544
545 if (!$room_ids) {
546 return [];
547 }
548
549 // reset array keys
550 $room_ids = array_values($room_ids);
551 }
552
553 // get original check-in and check-out timestamps
554 list($orig_checkin_ts, $orig_checkout_ts) = $this->getStayDates(true);
555 $info_from = getdate($orig_checkin_ts);
556 $info_to = getdate($orig_checkout_ts);
557
558 // the final check-out date
559 $final_checkout_ymd = date('Y-m-d', $orig_checkout_ts);
560
561 // count original length of stay and nights involved
562 $tot_nights = $this->countNightsOfStay();
563 $groupdays = VikBooking::getGroupDays($orig_checkin_ts, $orig_checkout_ts, $tot_nights);
564
565 if ($tot_nights < 2) {
566 // useless to waste time on finding a split stay if not at least 2 nights
567 return [];
568 }
569
570 // load the occupied records for these dates and rooms
571 $busy_records = !is_null($busy_list) ? $busy_list : VikBooking::loadBusyRecords($room_ids, $orig_checkin_ts, strtotime('+1 day', $orig_checkout_ts));
572
573 // calculate available rooms for each night
574 $avroom_nights = [];
575 foreach ($room_ids as $rid) {
576 if (!isset($all_rooms[$rid])) {
577 continue;
578 }
579 $room = $all_rooms[$rid];
580 foreach ($groupdays as $gday) {
581 $day_key = date('Y-m-d', $gday);
582 $bfound = 0;
583 if (!isset($busy_records[$rid])) {
584 $busy_records[$rid] = [];
585 }
586 foreach ($busy_records[$rid] as $bu) {
587 $busy_info_in = getdate($bu['checkin']);
588 $busy_info_out = getdate($bu['checkout']);
589 $busy_in_ts = mktime(0, 0, 0, $busy_info_in['mon'], $busy_info_in['mday'], $busy_info_in['year']);
590 $busy_out_ts = mktime(0, 0, 0, $busy_info_out['mon'], $busy_info_out['mday'], $busy_info_out['year']);
591 if ($gday >= $busy_in_ts && $gday == $busy_out_ts && !$this->inonout_allowed && $room['units'] < 2) {
592 // check-ins on check-outs not allowed
593 $bfound++;
594 if ($bfound >= $room['units']) {
595 break;
596 }
597 }
598 if ($gday >= $busy_in_ts && $gday < $busy_out_ts) {
599 $bfound++;
600 if ($bfound >= $room['units']) {
601 break;
602 }
603 }
604 }
605 if ($bfound < $room['units']) {
606 // push this night as available for this room
607 if (!isset($avroom_nights[$rid])) {
608 $avroom_nights[$rid] = [];
609 }
610 $avroom_nights[$rid][] = $day_key;
611 } else {
612 // room not available on this night, make sure to unset any previous value
613 if (isset($avroom_nights[$rid]) && in_array($day_key, $avroom_nights[$rid])) {
614 $unav_key = array_search($day_key, $avroom_nights[$rid]);
615 unset($avroom_nights[$rid][$unav_key]);
616 $avroom_nights[$rid] = array_values($avroom_nights[$rid]);
617 }
618 }
619 }
620 }
621
622 if (!count($avroom_nights)) {
623 // no rooms available at all, there's no way to do anything
624 return [];
625 }
626
627 // make sure all nights requested can be satisfied by at least one room
628 foreach ($groupdays as $gday) {
629 $day_key = date('Y-m-d', $gday);
630 $day_av = false;
631 foreach ($avroom_nights as $rid => $av_nights) {
632 if (in_array($day_key, $av_nights)) {
633 // night was found
634 $day_av = true;
635 break;
636 }
637 }
638 if (!$day_av) {
639 // this night is not available in any room, unable to proceed
640 return [];
641 }
642 }
643
644 // count the number of consecutive nights per room
645 $cons_room_nights = [];
646 $tot_gdays = count($groupdays);
647 foreach ($groupdays as $k => $gday) {
648 $day_key = date('Y-m-d', $gday);
649 if (!isset($cons_room_nights[$day_key])) {
650 $cons_room_nights[$day_key] = [];
651 }
652 foreach ($avroom_nights as $rid => $av_nights) {
653 if (in_array($day_key, $av_nights)) {
654 if (!isset($cons_room_nights[$day_key][$rid])) {
655 $cons_room_nights[$day_key][$rid] = [];
656 }
657 // count the next consecutive nights of stay
658 $cons_room_nights[$day_key][$rid][] = $day_key;
659 for ($j = ($k + 1); $j < $tot_gdays; $j++) {
660 $next_day_key = date('Y-m-d', $groupdays[$j]);
661 if (in_array($next_day_key, $av_nights)) {
662 $cons_room_nights[$day_key][$rid][] = $next_day_key;
663 } else {
664 break;
665 }
666 }
667 }
668 }
669 }
670
671 // sort the solutions with the highest number of consecutive nights to reduce the splits
672 $cons_room_nights_sorted = [];
673 foreach ($cons_room_nights as $day_key => $cons_nights) {
674 $cons_room_nights_cnt = [];
675 foreach ($cons_nights as $rid => $cons_dates) {
676 $cons_room_nights_cnt[$rid] = count($cons_dates);
677 }
678 // sort the array in a descending order
679 arsort($cons_room_nights_cnt);
680 // restore sorted values in cloned array
681 $cons_room_nights_sorted[$day_key] = [];
682 foreach ($cons_room_nights_cnt as $rid => $tot_cons_nights) {
683 $cons_room_nights_sorted[$day_key][$rid] = $cons_room_nights[$day_key][$rid];
684 }
685 }
686 $cons_room_nights = $cons_room_nights_sorted;
687
688 // validate the data just built
689 $first_day_key = date('Y-m-d', $groupdays[0]);
690 if (!isset($cons_room_nights[$first_day_key]) || !count($cons_room_nights[$first_day_key]) || count($cons_room_nights) != count($groupdays)) {
691 // unable to proceed
692 return [];
693 }
694
695 // remove the consecutive nights from the check-out date as this won't be a night of stay
696 unset($cons_room_nights[$final_checkout_ymd]);
697
698 // build the split stay solutions
699 $split_stay_sols = [];
700
701 // the number of rooms available on the first night should determine the number of split stay solutions
702 foreach ($cons_room_nights[$first_day_key] as $start_rid => $cons_nights) {
703 // start container of the various splits for this stay
704 $split_stay_sol = [];
705
706 // calculate last consecutive night available
707 $leave_date = end($cons_nights);
708 $leave_date_info = getdate(strtotime($leave_date));
709
710 // set the check-out date to the day after the last night
711 $checkout_ymd = date('Y-m-d', mktime(0, 0, 0, $leave_date_info['mon'], ($leave_date_info['mday'] + 1), $leave_date_info['year']));
712
713 // define the first stay
714 $split_stay = [
715 'idroom' => $start_rid,
716 'room_name' => $all_rooms[$start_rid]['name'],
717 'checkin' => $cons_nights[0],
718 'checkout' => $checkout_ymd,
719 'nights' => $this->countNightsOfStay(strtotime($cons_nights[0]), strtotime($checkout_ymd)),
720 ];
721
722 // make sure this room has got a tariff defined for this number of nights of stay
723 if (!$this->roomNightsAllowed($start_rid, $split_stay['nights'])) {
724 // no tariffs found for this los
725 continue;
726 }
727
728 // push first stay
729 $split_stay_sol[] = $split_stay;
730
731 // loop through the next stays
732 while (isset($cons_room_nights[$checkout_ymd])) {
733 /**
734 * For the next splits, we use just the first available rooms, which is
735 * the one with the highest number of consecutive nights available.
736 */
737 foreach ($cons_room_nights[$checkout_ymd] as $rid => $split_cons_nights) {
738 // calculate last consecutive night available
739 $leave_date = end($split_cons_nights);
740 $leave_date_info = getdate(strtotime($leave_date));
741
742 // set the check-out date to the day after the last night
743 $checkout_ymd = date('Y-m-d', mktime(0, 0, 0, $leave_date_info['mon'], ($leave_date_info['mday'] + 1), $leave_date_info['year']));
744 if ($leave_date == $final_checkout_ymd) {
745 // check-out date reached
746 $checkout_ymd = $final_checkout_ymd;
747 }
748
749 // count nights of stay
750 $split_nights = $this->countNightsOfStay(strtotime($split_cons_nights[0]), strtotime($checkout_ymd));
751
752 // make sure this room has got a tariff defined for this number of nights of stay
753 if (!$this->roomNightsAllowed($rid, $split_nights)) {
754 // no tariffs found for this los, abort solution
755 $split_stay_sol = [];
756 break 2;
757 }
758
759 // push split stay
760 $split_stay_sol[] = [
761 'idroom' => $rid,
762 'room_name' => $all_rooms[$rid]['name'],
763 'checkin' => $split_cons_nights[0],
764 'checkout' => $checkout_ymd,
765 'nights' => $split_nights,
766 ];
767
768 // we try to reduce the number of splits by considering just the first room
769 break;
770 }
771 }
772
773 if (count($split_stay_sol) < 2) {
774 // not a split stay, but rather a fully available room
775 continue;
776 }
777
778 // push split stay solution
779 $split_stay_sols[] = $split_stay_sol;
780 }
781
782 /**
783 * Load rooms involved in all split stays in order to validate
784 * global/room-level restrictions and closing dates on the stay.
785 */
786 $rooms_involved = [];
787 foreach ($split_stay_sols as $split_stay_sol) {
788 foreach ($split_stay_sol as $split_stay) {
789 if (!in_array($split_stay['idroom'], $rooms_involved)) {
790 $rooms_involved[] = $split_stay['idroom'];
791 }
792 }
793 }
794
795 // load restrictions for all rooms involved
796 $all_restrictions = VikBooking::loadRestrictions(true, $rooms_involved);
797 $glob_restrictions = VikBooking::globalRestrictions($all_restrictions);
798 $invalid_room_restr = [];
799
800 // validate global restrictions
801 if (VikBooking::validateRoomRestriction($glob_restrictions, $info_from, $info_to, $tot_nights)) {
802 // global restrictions apply over this stay
803 return [];
804 }
805
806 // validate closing dates
807 if (VikBooking::validateClosingDates($orig_checkin_ts, $orig_checkout_ts)) {
808 // global closing dates apply over this stay
809 return [];
810 }
811
812 // validate restrictions at room level
813 foreach ($rooms_involved as $rid) {
814 // load restrictions at room level
815 $room_level_restr = VikBooking::roomRestrictions($rid, $all_restrictions);
816 if (VikBooking::validateRoomRestriction($room_level_restr, $info_from, $info_to, $tot_nights)) {
817 // room-level restrictions apply over this stay
818 $invalid_room_restr[] = $rid;
819 }
820 }
821
822 // unset the split stays with the restricted rooms (if any)
823 $altered_sols = false;
824 foreach ($invalid_room_restr as $rid) {
825 foreach ($split_stay_sols as $k => $split_stay_sol) {
826 foreach ($split_stay_sol as $split_stay) {
827 if ($rid == $split_stay['idroom']) {
828 // this booking split stay cannot be suggested because of this room
829 unset($split_stay_sols[$k]);
830 $altered_sols = true;
831 continue 2;
832 }
833 }
834 }
835 }
836
837 // apply nights/transfers ratio limit (unless disabled)
838 $nights_transfers_ratio = $this->getNightsTransfersRatio();
839 if ($nights_transfers_ratio > 0 && $nights_transfers_ratio < 100) {
840 // count and apply limits
841 foreach ($split_stay_sols as $k => $split_stay_sol) {
842 // count nights and transfers
843 $split_stay_transfers = count($split_stay_sol) - 1;
844 $split_stay_nights = 0;
845 foreach ($split_stay_sol as $split_stay_room) {
846 $split_stay_nights += $split_stay_room['nights'];
847 }
848 // max allowed transfers
849 $max_transfers = round($split_stay_nights * $nights_transfers_ratio / 100, 0);
850 if (!$split_stay_transfers || $split_stay_transfers > $max_transfers) {
851 // unset solution
852 unset($split_stay_sols[$k]);
853 $altered_sols = true;
854 }
855 }
856 }
857
858 if ($altered_sols && count($split_stay_sols)) {
859 // restore the array keys
860 $split_stay_sols = array_values($split_stay_sols);
861 }
862
863 // return the available booking split stay solutions (if any)
864 return $split_stay_sols;
865 }
866
867 /**
868 * Returns the number of guests requested from the given room-party index.
869 *
870 * @param string $guest either "adults", "children" or "guests".
871 * @param int $party the party index number, 0 by default.
872 *
873 * @return int the total number of guests requested in the party.
874 */
875 protected function getPartyGuests($guest = 'adults', $party = 0)
876 {
877 if (!isset($this->room_parties[$party])) {
878 return 0;
879 }
880
881 if (!strcasecmp($guest, 'adults')) {
882 // adults
883 return $this->room_parties[$party]['adults'];
884 }
885
886 if (!strcasecmp($guest, 'children')) {
887 // children
888 return $this->room_parties[$party]['children'];
889 }
890
891 // total guests
892 $tot_guests = 0;
893 foreach ($this->room_parties as $rparty) {
894 $tot_guests += $rparty['adults'];
895 $tot_guests += $rparty['children'];
896 }
897
898 return $tot_guests;
899 }
900
901 /**
902 * Given a list of unavailable room IDs, yet compatible with the party and LOS requested,
903 * we build a list of available dates when such rooms could be booked for the same LOS.
904 *
905 * @param array $room_ids list of unavailable, yet compatible, room IDs.
906 *
907 * @return array associative list of available room-dates, or empty array.
908 */
909 protected function findClosestRoomDateSolutions($room_ids = [])
910 {
911 if (!$room_ids) {
912 return [];
913 }
914
915 // get all website rooms
916 $all_rooms = $this->loadRooms();
917
918 // get original check-in and check-out timestamps
919 list($orig_checkin_ts, $orig_checkout_ts) = $this->getStayDates(true);
920 $info_from = getdate($orig_checkin_ts);
921 $info_to = getdate($orig_checkout_ts);
922
923 // count original length of stay
924 $tot_nights = $this->countNightsOfStay();
925
926 // earliest checkin timestamp allowed
927 $lim_past_ts = mktime(0, 0, 0, date('n'), ((int)date('j') + VikBooking::getMinDaysAdvance()), date('Y'));
928
929 // suggested range of dates (+/- "back and forth" days from original dates)
930 $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']);
931 if ($sug_from_ts < $lim_past_ts) {
932 $sug_from_ts = $lim_past_ts;
933 // since we are close to the requested check-in, double up the "back and forth" for the max date
934 $this->setBackForthDays($this->getBackForthDays() * 2);
935 }
936 $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']);
937 $sug_to_ts = $sug_to_ts < $sug_from_ts ? $sug_from_ts : $sug_to_ts;
938
939 // get days timestamps for suggestions
940 $groupdays = [];
941 $sug_start_info = getdate($sug_from_ts);
942 $sug_from_midnight = mktime(0, 0, 0, $sug_start_info['mon'], $sug_start_info['mday'], $sug_start_info['year']);
943 $sug_start_info = getdate($sug_from_midnight);
944 while ($sug_start_info[0] <= $sug_to_ts) {
945 array_push($groupdays, $sug_start_info[0]);
946 $sug_start_info = getdate(mktime(0, 0, 0, $sug_start_info['mon'], ($sug_start_info['mday'] + 1), $sug_start_info['year']));
947 }
948
949 // build suggestions array of dates with some availability for the given rooms
950 $suggestions = [];
951 $busy_records = VikBooking::loadBusyRecords($room_ids, $sug_from_ts, strtotime('+1 day', $sug_to_ts));
952 foreach ($room_ids as $rid) {
953 if (!isset($all_rooms[$rid])) {
954 continue;
955 }
956 $room = $all_rooms[$rid];
957 foreach ($groupdays as $gday) {
958 $day_key = date('Y-m-d', $gday);
959 $bfound = 0;
960 if (!isset($busy_records[$rid])) {
961 $busy_records[$rid] = [];
962 }
963 foreach ($busy_records[$rid] as $bu) {
964 $busy_info_in = getdate($bu['checkin']);
965 $busy_info_out = getdate($bu['checkout']);
966 $busy_in_ts = mktime(0, 0, 0, $busy_info_in['mon'], $busy_info_in['mday'], $busy_info_in['year']);
967 $busy_out_ts = mktime(0, 0, 0, $busy_info_out['mon'], $busy_info_out['mday'], $busy_info_out['year']);
968 if ($gday >= $busy_in_ts && $gday == $busy_out_ts && !$this->inonout_allowed && $room['units'] < 2) {
969 // check-ins on check-outs not allowed
970 $bfound++;
971 if ($bfound >= $room['units']) {
972 break;
973 }
974 }
975 if ($gday >= $busy_in_ts && $gday < $busy_out_ts) {
976 $bfound++;
977 if ($bfound >= $room['units']) {
978 break;
979 }
980 }
981 }
982 if ($bfound < $room['units']) {
983 if (!isset($suggestions[$day_key])) {
984 $suggestions[$day_key] = [];
985 }
986 $room_day = $room;
987 $room_day['units_left'] = $room['units'] - $bfound;
988 $suggestions[$day_key] = $suggestions[$day_key] + [$rid => $room_day];
989 }
990 }
991 }
992
993 if (!$suggestions) {
994 // no available nights found for the prior and next "back and forth" days for the given rooms
995 return [];
996 }
997
998 // build the solutions array with keys=checkin, values=all rooms suited for the requested number of nights
999 $solutions = [];
1000 // get all rooms available for the number of nights requested in the suggestions array of dates
1001 foreach ($suggestions as $kday => $rooms) {
1002 $day_ts_info = getdate(strtotime($kday));
1003 foreach ($rooms as $rid => $room) {
1004 $suitable = true;
1005 $room_days_av_left = [$kday => $room['units_left']];
1006 for ($i = 1; $i < $tot_nights; $i++) {
1007 $next_night = mktime(0, 0, 0, $day_ts_info['mon'], ($day_ts_info['mday'] + $i), $day_ts_info['year']);
1008 $next_night_dt = date('Y-m-d', $next_night);
1009 if (!isset($suggestions[$next_night_dt]) || !isset($suggestions[$next_night_dt][$rid])) {
1010 $suitable = false;
1011 break;
1012 }
1013 $room_days_av_left[$next_night_dt] = $suggestions[$next_night_dt][$rid]['units_left'];
1014 }
1015 if ($suitable === true) {
1016 if (!isset($solutions[$kday])) {
1017 $solutions[$kday] = [];
1018 }
1019 unset($room['units_left']);
1020 $room['days_av_left'] = $room_days_av_left;
1021 $solutions[$kday] = $solutions[$kday] + [$rid => $room];
1022 }
1023 }
1024 }
1025
1026 if (!count($solutions)) {
1027 // the requested length of stay could not be satisfied for any available night
1028 return [];
1029 }
1030
1031 // sort the solutions by the closest checkin date to the one requested
1032 $sortmap = [];
1033 $orig_checkin_ymd = date('Y-m-d', $orig_checkin_ts);
1034 foreach ($solutions as $kday => $solution) {
1035 $kdayts = strtotime($kday);
1036 $sortmap[$kdayts] = $kdayts > $orig_checkin_ts ? ($kdayts - $orig_checkin_ts) : ($orig_checkin_ts - $kdayts);
1037 if ($orig_checkin_ymd == $kday) {
1038 // the original check-in day is available, so we want it to be first, regardless of the check-in time
1039 $sortmap[$kdayts] = 1;
1040 }
1041 }
1042 asort($sortmap);
1043 $sorted = [];
1044 foreach ($sortmap as $kdayts => $v) {
1045 $kday = date('Y-m-d', $kdayts);
1046 $sorted[$kday] = $solutions[$kday];
1047 }
1048 $solutions = $sorted;
1049 unset($sorted);
1050
1051 /**
1052 * Load rooms involved in the final alternative solutions in order
1053 * to validate global/room-level restrictions and closing dates.
1054 *
1055 * @since 1.15.4 (J) - 1.5.10 (WP)
1056 */
1057 $rooms_involved = [];
1058 foreach ($solutions as $arrive_ymd => $roomsol) {
1059 foreach (array_keys($roomsol) as $rid) {
1060 if (!in_array($rid, $rooms_involved)) {
1061 $rooms_involved[] = $rid;
1062 }
1063 }
1064 }
1065
1066 // load restrictions for all rooms involved
1067 $all_restrictions = VikBooking::loadRestrictions(true, $rooms_involved);
1068 $glob_restrictions = VikBooking::globalRestrictions($all_restrictions);
1069 $room_level_restr = [];
1070
1071 foreach ($solutions as $arrive_ymd => $roomsol) {
1072 // build suggested stay dates
1073 $sug_in = getdate(strtotime($arrive_ymd));
1074 $sug_out = getdate(mktime(0, 0, 0, $sug_in['mon'], ($sug_in['mday'] + $tot_nights), $sug_in['year']));
1075 // validate global restrictions
1076 if (VikBooking::validateRoomRestriction($glob_restrictions, $sug_in, $sug_out, $tot_nights)) {
1077 // global restrictions apply over this stay
1078 unset($solutions[$arrive_ymd]);
1079 continue;
1080 }
1081 // validate closing dates
1082 if (VikBooking::validateClosingDates($sug_in[0], $sug_out[0])) {
1083 // global closing dates apply over this stay
1084 unset($solutions[$arrive_ymd]);
1085 continue;
1086 }
1087 // validate restrictions at room level
1088 foreach ($roomsol as $rid => $rdata) {
1089 if (!isset($room_level_restr[$rid])) {
1090 // load restrictions at room level
1091 $room_level_restr[$rid] = VikBooking::roomRestrictions($rid, $all_restrictions);
1092 }
1093 if (VikBooking::validateRoomRestriction($room_level_restr[$rid], $sug_in, $sug_out, $tot_nights)) {
1094 // room-level restrictions apply over this stay
1095 unset($solutions[$arrive_ymd][$rid]);
1096 if (!count($solutions[$arrive_ymd])) {
1097 // unset the entire suggested date
1098 unset($solutions[$arrive_ymd]);
1099 break;
1100 }
1101 continue;
1102 }
1103 }
1104 }
1105
1106 if (!count($solutions)) {
1107 // the calculated suggestions do not meet the restrictions or the closing dates
1108 return [];
1109 }
1110
1111 // return the solution alternative dates for all rooms available
1112 return $solutions;
1113 }
1114
1115 /**
1116 * Sorts an associative array of room-solutions by the bigger rooms.
1117 *
1118 * @param array $solutions the date solutions obtained for some rooms.
1119 *
1120 * @return array the same array sorted by bigger rooms on top.
1121 */
1122 protected function sortBiggerRoomSolutions($solutions)
1123 {
1124 if (!is_array($solutions) || !$solutions) {
1125 return $solutions;
1126 }
1127
1128 // sort rooms-solutions by max-adults, 'max-guests', 'max-children', in a descending order
1129 foreach ($solutions as $kday => $solution) {
1130 // with this sorting, we will have the bigger rooms on top to quickly fit the party requested
1131 uasort($solutions[$kday], function($a, $b) {
1132 if ($a['toadult'] == $b['toadult']) {
1133 if ($a['totpeople'] == $b['totpeople']) {
1134 return $a['tochild'] > $b['tochild'] ? -1 : 1;
1135 }
1136 return $a['totpeople'] > $b['totpeople'] ? -1 : 1;
1137 }
1138 return $a['toadult'] > $b['toadult'] ? -1 : 1;
1139 });
1140 }
1141
1142 return $solutions;
1143 }
1144
1145 /**
1146 * Given a list of available dates and rooms (solutions), attempts
1147 * to match a party of rooms that fits the party requested.
1148 *
1149 * @param array $solutions the list of available dates and related rooms.
1150 *
1151 * @return array list of alternative party solutions, if any.
1152 */
1153 protected function matchSolutionsParty($solutions)
1154 {
1155 if (!is_array($solutions) || !$solutions) {
1156 return [];
1157 }
1158
1159 // build the list of alternative parties
1160 $alternative_parties = [];
1161
1162 // build list of party guests
1163 $party_guests = [
1164 'adults' => 0,
1165 'children' => 0,
1166 ];
1167 foreach ($this->room_parties as $rparty) {
1168 $party_guests['adults'] += $rparty['adults'];
1169 $party_guests['children'] += $rparty['children'];
1170 }
1171
1172 // check if the rooms of each solution can fit the number of guests requested, unset the solution otherwise
1173 foreach ($solutions as $kday => $solution) {
1174 $solution_guests = [
1175 'adults' => 0,
1176 'children' => 0,
1177 ];
1178 foreach ($solution as $rid => $roomsol) {
1179 // count minimum units left for this room
1180 $room_min_uleft = min($roomsol['days_av_left']);
1181 // check if this solution of rooms can allocate all the guests requested
1182 if ($roomsol['totpeople'] < ($roomsol['toadult'] + $roomsol['tochild']) && !$party_guests['children']) {
1183 // in case of no children requested, we ignore them to avoid adjusting the room capacity
1184 $roomsol['tochild'] = 0;
1185 }
1186 if ($roomsol['totpeople'] < ($roomsol['toadult'] + $roomsol['tochild'])) {
1187 /**
1188 * The sum of the max_adults and max_children exceeds the max_guests: lower the adults
1189 * we can take first (if party children > 0), then the children, until sum=max_guests
1190 */
1191 while (($roomsol['toadult'] > 0 || $roomsol['tochild'] > 0)) {
1192 if (!$party_guests['children'] && $roomsol['totpeople'] == $roomsol['toadult']) {
1193 /**
1194 * When no children requested in the party, we cannot under-utilize rooms.
1195 * Break the loop without lowering the 'toadult'.
1196 */
1197 $roomsol['tochild'] = 0;
1198 break;
1199 }
1200 if ($party_guests['children'] && $solution_guests['children'] >= $party_guests['children']) {
1201 /**
1202 * If all the children requested were allocated in other solutions,
1203 * we should not under-utilize rooms by reducing the number of adults.
1204 */
1205 break;
1206 }
1207 if ($roomsol['toadult'] > 0 && $party_guests['children'] > 0 && !($roomsol['tochild'] > $party_guests['children'])) {
1208 /**
1209 * We lower first the adults that we put in this room, only if there are children in the party
1210 * and if the children in the party are more than the 'max_children' of this room.
1211 */
1212 $roomsol['toadult']--;
1213 if ($roomsol['totpeople'] >= ($roomsol['toadult'] + $roomsol['tochild'])) {
1214 break;
1215 }
1216 }
1217 if ($roomsol['tochild'] > 0) {
1218 // if the max_guests is still greater than the sum of adults+children we take, take out one child
1219 $roomsol['tochild']--;
1220 if ($roomsol['totpeople'] >= ($roomsol['toadult'] + $roomsol['tochild'])) {
1221 break;
1222 }
1223 }
1224 if ($roomsol['toadult'] > 0) {
1225 // 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
1226 $roomsol['toadult']--;
1227 if ($roomsol['totpeople'] >= ($roomsol['toadult'] + $roomsol['tochild'])) {
1228 break;
1229 }
1230 }
1231 }
1232 }
1233 $solution_guests['adults'] += $roomsol['toadult'] * $room_min_uleft;
1234 $solution_guests['children'] += $roomsol['tochild'] * $room_min_uleft;
1235 // update 'max_adults' and 'max_children' for this solution (for later guests allocation)
1236 $solution[$rid]['toadult'] = $roomsol['toadult'];
1237 $solution[$rid]['tochild'] = $roomsol['tochild'];
1238 }
1239
1240 $solutions[$kday] = $solution;
1241 if ($solution_guests['adults'] < $party_guests['adults'] || $solution_guests['children'] < $party_guests['children']) {
1242 // the guests we can allocate with the solution of this day are not enough: unset the solution
1243 unset($solutions[$kday]);
1244 continue;
1245 }
1246
1247 // if we get to this point we can suggest a booking solution for the party requested, but in different rooms
1248 if (!isset($alternative_parties[$kday])) {
1249 $alternative_parties[$kday] = [];
1250 }
1251
1252 // re-loop over the rooms in this solution to build the booking solution for this day
1253 $guests_allocated = [
1254 'adults' => 0,
1255 'children' => 0
1256 ];
1257
1258 /**
1259 * The rooms available for an alternative booking solutions have been sorted by capacity
1260 * in a descending order to quickly fit the guest party requested. However, if a smaller
1261 * and cheaper room was capable of fitting all guests, we should opt for this solution.
1262 *
1263 * @since 1.15.4 (J) - 1.5.9 (WP)
1264 */
1265 $smaller_fit_found = false;
1266 $smaller_solutions = array_reverse($solution, true);
1267 foreach ($smaller_solutions as $rid => $roomsol) {
1268 if ($party_guests['adults'] > 0 && $party_guests['adults'] > $roomsol['toadult']) {
1269 // too many adults requested for this small room
1270 continue;
1271 }
1272 if ($party_guests['children'] > 0 && $party_guests['children'] > $roomsol['tochild']) {
1273 // too many children requested for this small room
1274 continue;
1275 }
1276 if (($party_guests['adults'] + $party_guests['children']) > $roomsol['totpeople']) {
1277 // too many guests requested for this small room
1278 continue;
1279 }
1280 // we've got a fitting room which could be smaller
1281 $roomsol['guests_allocation'] = [
1282 'adults' => $party_guests['adults'],
1283 'children' => $party_guests['children'],
1284 ];
1285 array_push($alternative_parties[$kday], $roomsol);
1286 // turn flag on and break the loop
1287 $smaller_fit_found = true;
1288 break;
1289 }
1290 if ($smaller_fit_found) {
1291 // no need to parse the rooms from the largest to the smallest
1292 continue;
1293 }
1294
1295 foreach ($solution as $rid => $roomsol) {
1296 // count minimum units left for this room
1297 $room_min_uleft = min($roomsol['days_av_left']);
1298 // fullfil all the units of this room
1299 for ($units_taken = 0; $units_taken < $room_min_uleft; $units_taken++) {
1300 $current_allocation = [
1301 'adults' => 0,
1302 'children' => 0
1303 ];
1304 if ($guests_allocated['adults'] < $party_guests['adults']) {
1305 $humans_taken = $roomsol['toadult'];
1306 $missing_humans = $party_guests['adults'] - $guests_allocated['adults'];
1307 $humans_taken = $humans_taken > $missing_humans ? $missing_humans : $humans_taken;
1308
1309 $current_allocation['adults'] = $humans_taken;
1310 $guests_allocated['adults'] += $humans_taken;
1311 }
1312 if ($guests_allocated['children'] < $party_guests['children']) {
1313 $humans_taken = $roomsol['tochild'];
1314 $missing_humans = $party_guests['children'] - $guests_allocated['children'];
1315 $humans_taken = $humans_taken > $missing_humans ? $missing_humans : $humans_taken;
1316
1317 $current_allocation['children'] = $humans_taken;
1318 $guests_allocated['children'] += $humans_taken;
1319 }
1320 $roomsol['guests_allocation'] = $current_allocation;
1321 array_push($alternative_parties[$kday], $roomsol);
1322 if ($guests_allocated['adults'] >= $party_guests['adults'] && $guests_allocated['children'] >= $party_guests['children']) {
1323 // we have allocated all guests, exit the for-loop
1324 break;
1325 }
1326 }
1327 if ($guests_allocated['adults'] >= $party_guests['adults'] && $guests_allocated['children'] >= $party_guests['children']) {
1328 //we have allocated all guests with this solution, no need to loop over other rooms available in this day.
1329 break;
1330 }
1331 }
1332 }
1333
1334 // return the alternative parties found, if any
1335 return $alternative_parties;
1336 }
1337
1338 /**
1339 * Given a list of alternative dates obtained from an inquiry/request information,
1340 * composes a valid room-rate array to store the inquiry reservation. By calling this
1341 * method, the original stay dates will be overwritten.
1342 *
1343 * @param array $alt_dates the list of alternative dates found for the stay.
1344 * @param object $customer a stdClass object with the basic customer details.
1345 *
1346 * @return int the ID of the newly created inquiry reservation.
1347 */
1348 public function allocateAltDatesInquiry($alt_dates, $customer)
1349 {
1350 if (!is_array($alt_dates) || !$alt_dates) {
1351 return 0;
1352 }
1353
1354 foreach ($alt_dates as $ymd => $rooms) {
1355 // we expect just one room-type for the party, and we use the first suggestion
1356 foreach ($rooms as $rid => $alt_stay) {
1357 if (empty($alt_stay['days_av_left']) || !is_array($alt_stay['days_av_left'])) {
1358 // invalid structure
1359 continue;
1360 }
1361 // compose the new stay dates
1362 $sugg_checkin_dt = null;
1363 $sugg_checkout_dt = null;
1364 foreach ($alt_stay['days_av_left'] as $dayk => $uleft) {
1365 if (empty($sugg_checkin_dt)) {
1366 // grab the first date
1367 $sugg_checkin_dt = $dayk;
1368 }
1369 // always overwrite until last date
1370 $sugg_checkout_dt = $dayk;
1371 }
1372 // increase check-out date by one day (day after last night of stay)
1373 $sugg_out_info = getdate(strtotime($sugg_checkout_dt));
1374 $sugg_checkout_dt = date('Y-m-d', mktime(0, 0, 0, $sugg_out_info['mon'], ($sugg_out_info['mday'] + 1), $sugg_out_info['year']));
1375
1376 // set the new stay dates
1377 $this->setStayDates($sugg_checkin_dt, $sugg_checkout_dt);
1378
1379 // build the room rate plan array without any rate plan information
1380 $room_rplan = [
1381 'idroom' => $alt_stay['id'],
1382 ];
1383
1384 // create the inquiry reservation for the closest alternative dates
1385 return $this->createInquiryReservation($room_rplan, $customer);
1386 }
1387 }
1388
1389 return 0;
1390 }
1391
1392 /**
1393 * Given a list of alternative parties obtained from an inquiry/request information,
1394 * composes valid room-rate arrays to store the inquiry reservation. By calling this
1395 * method, the original stay dates and room party will be overwritten.
1396 *
1397 * @param array $alt_parties the list of alternative parties found for the stay.
1398 * @param object $customer a stdClass object with the basic customer details.
1399 *
1400 * @return int the ID of the newly created inquiry reservation.
1401 */
1402 public function allocateAltPartyInquiry($alt_parties, $customer)
1403 {
1404 if (!is_array($alt_parties) || !$alt_parties) {
1405 return 0;
1406 }
1407
1408 // build list of rooms to assign to the inquiry reservation
1409 $room_rates = [];
1410
1411 // start party counter
1412 $party_counter = 0;
1413
1414 foreach ($alt_parties as $ymd => $alt_rooms) {
1415 // we expect to have more than one room-type for the large party suggestion
1416 foreach ($alt_rooms as $alt_room) {
1417 if (empty($alt_room['guests_allocation']) || !is_array($alt_room['guests_allocation'])) {
1418 // invalid structure
1419 continue;
1420 }
1421 if (empty($alt_room['days_av_left']) || !is_array($alt_room['days_av_left'])) {
1422 // invalid structure
1423 continue;
1424 }
1425 // compose the new stay dates
1426 $sugg_checkin_dt = null;
1427 $sugg_checkout_dt = null;
1428 foreach ($alt_room['days_av_left'] as $dayk => $uleft) {
1429 if (empty($sugg_checkin_dt)) {
1430 // grab the first date
1431 $sugg_checkin_dt = $dayk;
1432 }
1433 // always overwrite until last date
1434 $sugg_checkout_dt = $dayk;
1435 }
1436 // increase check-out date by one day (day after last night of stay)
1437 $sugg_out_info = getdate(strtotime($sugg_checkout_dt));
1438 $sugg_checkout_dt = date('Y-m-d', mktime(0, 0, 0, $sugg_out_info['mon'], ($sugg_out_info['mday'] + 1), $sugg_out_info['year']));
1439
1440 // set the new stay dates
1441 $this->setStayDates($sugg_checkin_dt, $sugg_checkout_dt);
1442
1443 // set the current guests party (the first will replace the previous party, others will be pushed)
1444 $this->setRoomParty($alt_room['guests_allocation']['adults'], $alt_room['guests_allocation']['children'], ($party_counter === 0));
1445
1446 // increase party counter
1447 $party_counter++;
1448
1449 // push current room with no rate plan information
1450 array_push($room_rates, [
1451 'idroom' => $alt_room['id'],
1452 ]);
1453 }
1454
1455 if (count($room_rates)) {
1456 // we use the closest dates in the first suggestion party array
1457 break;
1458 }
1459 }
1460
1461 // count total rooms in the party
1462 $tot_room_party = count($room_rates);
1463
1464 if (!$tot_room_party) {
1465 // something went wrong
1466 return 0;
1467 }
1468
1469 // grab the main/first room reservation
1470 $room_rplan = $room_rates[0];
1471
1472 // build extra rooms
1473 $extra_rooms = [];
1474 if ($tot_room_party > 1) {
1475 // grab the remaining rooms
1476 unset($room_rates[0]);
1477 $extra_rooms = array_values($room_rates);
1478 }
1479
1480 // create the inquiry reservation for the closest alternative dates and rooms party
1481 return $this->createInquiryReservation($room_rplan, $customer, $extra_rooms);
1482 }
1483
1484 /**
1485 * Creates a new pending reservation from the inquiry/request information.
1486 * Requires a valid room-rate array to be available, or in case suggestions should
1487 * be applied, the room-rate array should be adjusted to comply with this method.
1488 *
1489 * @param array $room_rplan a room-rate array to allocate the booking.
1490 * @param object $customer a stdClass object with the basic customer details.
1491 * @param array $extra_rooms optional list of additional room-rate arrays to store
1492 * in case of alternative parties suggested.
1493 *
1494 * @return int the ID of the newly created inquiry reservation.
1495 */
1496 public function createInquiryReservation($room_rplan, $customer, $extra_rooms = array())
1497 {
1498 if (!is_array($room_rplan) || empty($room_rplan['idroom'])) {
1499 return 0;
1500 }
1501
1502 if (empty($this->stay_ts) || empty($this->room_parties)) {
1503 // no stay dates or room party set
1504 return 0;
1505 }
1506
1507 $dbo = JFactory::getDbo();
1508
1509 // build reservation object
1510 $res_obj = new stdClass;
1511 $res_obj->custdata = $customer->custdata;
1512 $res_obj->ts = time();
1513 $res_obj->status = 'standby';
1514 $res_obj->days = $this->countNightsOfStay();
1515 $res_obj->checkin = $this->stay_ts[0];
1516 $res_obj->checkout = $this->stay_ts[1];
1517 $res_obj->custmail = $customer->email;
1518 $res_obj->sid = VikBooking::getSecretLink();
1519 $res_obj->idpayment = $this->getDefaultPaymentId();
1520 $res_obj->roomsnum = count($this->room_parties);
1521 if (!empty($room_rplan['cost'])) {
1522 $res_obj->total = (float)$room_rplan['cost'];
1523 }
1524 $res_obj->adminnotes = $customer->adminnotes;
1525 $res_obj->lang = $customer->lang;
1526 $res_obj->country = $customer->country;
1527 if (!empty($room_rplan['taxes'])) {
1528 $res_obj->tot_taxes = (float)$room_rplan['taxes'];
1529 }
1530 if (!empty($room_rplan['city_taxes'])) {
1531 $res_obj->tot_city_taxes = (float)$room_rplan['city_taxes'];
1532 }
1533 $res_obj->phone = $customer->phone;
1534 $res_obj->type = 'inquiry';
1535
1536 // store record
1537 if (!$dbo->insertObject('#__vikbooking_orders', $res_obj, 'id')) {
1538 // could not store the booking record
1539 return 0;
1540 }
1541
1542 // get the ID of the newly created reservation
1543 $res_id = $res_obj->id;
1544
1545 // check if mandatory options should be assigned (not in case of suggestions)
1546 $room_options = null;
1547 if (!empty($room_rplan['idprice']) && isset($res_obj->tot_city_taxes)) {
1548 $mand_taxes = VikBooking::getMandatoryTaxesFees([$room_rplan['idroom']], $this->getPartyGuests('adults', 0), $res_obj->days);
1549 if (is_array($mand_taxes) && !empty($mand_taxes['options'])) {
1550 $room_options = implode(';', $mand_taxes['options']);
1551 }
1552 }
1553
1554 // build room-reservation object
1555 $room_res_obj = new stdClass;
1556 $room_res_obj->idorder = $res_id;
1557 $room_res_obj->idroom = $room_rplan['idroom'];
1558 $room_res_obj->adults = $this->getPartyGuests('adults', 0);
1559 $room_res_obj->children = $this->getPartyGuests('children', 0);
1560 if (!empty($room_rplan['idprice'])) {
1561 $room_res_obj->idtar = $this->getTariffId($room_rplan['idroom'], $room_rplan['idprice'], $res_obj->days);
1562 }
1563 $room_res_obj->optionals = $room_options;
1564 $room_res_obj->t_first_name = $customer->name;
1565 $room_res_obj->t_last_name = $customer->lname;
1566
1567 // store record
1568 if (!$dbo->insertObject('#__vikbooking_ordersrooms', $room_res_obj, 'id')) {
1569 // could not store the room-reservation record
1570 return $res_id;
1571 }
1572
1573 // in case of suggestions for alternative room parties, parse the extra rooms
1574 $party_index = 1;
1575 foreach ($extra_rooms as $extra_room) {
1576 if (!is_array($extra_room) || empty($extra_room['idroom'])) {
1577 continue;
1578 }
1579 // build additional room-reservation object
1580 $room_res_obj = new stdClass;
1581 $room_res_obj->idorder = $res_id;
1582 $room_res_obj->idroom = $extra_room['idroom'];
1583 $room_res_obj->adults = $this->getPartyGuests('adults', $party_index);
1584 $room_res_obj->children = $this->getPartyGuests('children', $party_index);
1585
1586 // store record
1587 $dbo->insertObject('#__vikbooking_ordersrooms', $room_res_obj, 'id');
1588
1589 // increase room party index
1590 $party_index++;
1591 }
1592
1593 // return the newly created reservation ID
1594 return $res_id;
1595 }
1596
1597 /**
1598 * Attempts to get the tariff ID for the given room, rate plan and nights.
1599 *
1600 * @param int $room_id the ID of the VBO room.
1601 * @param int $rplan_id the rate plan ID in VBO.
1602 * @param int $nights the number of nights of stay.
1603 *
1604 * @return int|null the tariff ID or null.
1605 */
1606 public function getTariffId($room_id, $rplan_id, $nights)
1607 {
1608 if (empty($room_id) || empty($rplan_id) || $nights < 1) {
1609 return null;
1610 }
1611
1612 $dbo = JFactory::getDbo();
1613
1614 $q = "SELECT `id` FROM `#__vikbooking_dispcost` WHERE `idroom`={$room_id} AND `days`={$nights} AND `idprice`={$rplan_id}";
1615 $dbo->setQuery($q, 0, 1);
1616 $res = $dbo->loadResult();
1617
1618 if ($res) {
1619 return (int)$res;
1620 }
1621
1622 return null;
1623 }
1624
1625 /**
1626 * Grabs the details of a given booking id.
1627 *
1628 * @param int $bid the booking ID to look for.
1629 *
1630 * @return array empty array or booking record details.
1631 */
1632 public function getBookingDetails($bid)
1633 {
1634 $bid = (int)$bid;
1635
1636 $dbo = JFactory::getDbo();
1637
1638 $q = "SELECT `o`.*, `co`.`idcustomer`, CONCAT_WS(' ', `c`.`first_name`, `c`.`last_name`) AS `customer_fullname`, `c`.`country` AS `customer_country`, `c`.`pic`
1639 FROM `#__vikbooking_orders` AS `o`
1640 LEFT JOIN `#__vikbooking_customers_orders` AS `co` ON `co`.`idorder`=`o`.`id`
1641 LEFT JOIN `#__vikbooking_customers` AS `c` ON `c`.`id`=`co`.`idcustomer`
1642 WHERE `o`.`id`={$bid}";
1643 $dbo->setQuery($q, 0, 1);
1644 $row = $dbo->loadAssoc();
1645 if ($row) {
1646 return $row;
1647 }
1648
1649 return [];
1650 }
1651
1652 /**
1653 * Validates if the room allows the given number of nights of stay by checking if a
1654 * tariff is defined for the given length of stay. Useful for particular rate tables.
1655 *
1656 * @param int $room_id the ID of the VBO room.
1657 * @param int $nights the number of nights of stay.
1658 *
1659 * @return bool true if a tariff is found between min and max nights.
1660 *
1661 * @since 1.16.3 (J) - 1.6.3 (WP)
1662 */
1663 public function roomNightsAllowed($room_id, $nights)
1664 {
1665 if (!isset($this->min_max_los_tariffs_map[$room_id])) {
1666 $dbo = JFactory::getDbo();
1667
1668 $q = $dbo->getQuery(true)
1669 ->select('MIN(' . $dbo->qn('t.days') . ') AS ' . $dbo->qn('min_nights'))
1670 ->select('MAX(' . $dbo->qn('t.days') . ') AS ' . $dbo->qn('max_nights'))
1671 ->from($dbo->qn('#__vikbooking_dispcost', 't'))
1672 ->where($dbo->qn('t.idroom') . ' = ' . (int)$room_id);
1673
1674 $dbo->setQuery($q, 0, 1);
1675 $tariffs = $dbo->loadObject();
1676
1677 if (!$tariffs || !$tariffs->min_nights || !$tariffs->max_nights) {
1678 return false;
1679 }
1680
1681 // set values
1682 $this->min_max_los_tariffs_map[$room_id] = [$tariffs->min_nights, $tariffs->max_nights];
1683 }
1684
1685 // check if the number of nights of stay is within the range of tariffs los map
1686 return ($nights >= min($this->min_max_los_tariffs_map[$room_id]) && $nights <= max($this->min_max_los_tariffs_map[$room_id]));
1687 }
1688
1689 /**
1690 * Sets the stay dates, check-in and check-out date timestamps.
1691 *
1692 * @param string $from check-in date string in Y-m-d or VBO format.
1693 * @param string $to check-out date string in Y-m-d or VBO format.
1694 *
1695 * @return self
1696 */
1697 public function setStayDates($from, $to)
1698 {
1699 if (empty($from) || empty($to)) {
1700 return $this;
1701 }
1702
1703 $checkinh = 0;
1704 $checkinm = 0;
1705 $checkouth = 0;
1706 $checkoutm = 0;
1707 $timeopst = VikBooking::getTimeOpenStore();
1708 if (is_array($timeopst)) {
1709 if ($timeopst[0] < $timeopst[1]) {
1710 // check-in not allowed on a day where there is already a check out (no arrivals/depatures on the same day)
1711 $this->inonout_allowed = false;
1712 }
1713 $opent = VikBooking::getHoursMinutes($timeopst[0]);
1714 $closet = VikBooking::getHoursMinutes($timeopst[1]);
1715 $checkinh = $opent[0];
1716 $checkinm = $opent[1];
1717 $checkouth = $closet[0];
1718 $checkoutm = $closet[1];
1719 }
1720 $from_ts = VikBooking::getDateTimestamp($from, $checkinh, $checkinm);
1721 $to_ts = VikBooking::getDateTimestamp($to, $checkouth, $checkoutm);
1722
1723 // set stay dates and timestamps
1724 $this->stay_dates = [date('Y-m-d', $from_ts), date('Y-m-d', $to_ts)];
1725 $this->stay_ts = [$from_ts, $to_ts];
1726
1727 return $this;
1728 }
1729
1730 /**
1731 * Returns the current stay date or timestamps.
1732 *
1733 * @param bool $ts whether to get the date timestamps.
1734 *
1735 * @return array the current stay date timestamps.
1736 */
1737 public function getStayDates($ts = false)
1738 {
1739 return $ts ? $this->stay_ts : $this->stay_dates;
1740 }
1741
1742 /**
1743 * Sets a room party with adults and children, by optionally replacing the others.
1744 *
1745 * @param int $adults the number of adults for this room party.
1746 * @param int $children the number of children for this room party.
1747 * @param bool $replace if true, any previously set room party will be replaced.
1748 *
1749 * @return self
1750 */
1751 public function setRoomParty($adults, $children = 0, $replace = false)
1752 {
1753 $room_party = [
1754 'adults' => $adults,
1755 'children' => $children,
1756 ];
1757
1758 if ($replace) {
1759 $this->room_parties = [$room_party];
1760 } else {
1761 array_push($this->room_parties, $room_party);
1762 }
1763
1764 return $this;
1765 }
1766
1767 /**
1768 * Returns the current room parties array.
1769 *
1770 * @return array the current room parties.
1771 */
1772 public function getRoomParties()
1773 {
1774 return $this->room_parties;
1775 }
1776
1777 /**
1778 * Sets and returns the flag to ignore the restrictions.
1779 *
1780 * @param bool $set the boolean status to set.
1781 *
1782 * @return bool the current ignore status.
1783 */
1784 public function ignoreRestrictions($set = null)
1785 {
1786 if (is_bool($set)) {
1787 $this->ignore_restrictions = $set;
1788 }
1789
1790 return $this->ignore_restrictions;
1791 }
1792
1793 /**
1794 * Takes the first payment ID "string", if available.
1795 *
1796 * @return string|null
1797 */
1798 protected function getDefaultPaymentId()
1799 {
1800 $dbo = JFactory::getDbo();
1801
1802 $q = "SELECT `id`, `name` FROM `#__vikbooking_gpayments` WHERE `published`=1 ORDER BY `ordering` ASC";
1803 $dbo->setQuery($q, 0, 1);
1804 $data = $dbo->loadAssoc();
1805
1806 if ($data) {
1807 return $data['id'] . '=' . $data['name'];
1808 }
1809
1810 return null;
1811 }
1812
1813 /**
1814 * Returns the current nights/transfers ratio for split stays.
1815 *
1816 * @return int the nights transfers ratio for the percent calculation.
1817 *
1818 * @since 1.16.0 (J) - 1.6.0 (WP)
1819 */
1820 public function getNightsTransfersRatio()
1821 {
1822 return $this->nights_transfers_ratio;
1823 }
1824
1825 /**
1826 * Returns the default nights/transfers ratio for split stays.
1827 *
1828 * @return int the nights transfers ratio defined in the configuration.
1829 *
1830 * @since 1.16.0 (J) - 1.6.0 (WP)
1831 */
1832 public function getDefaultNightsTransfersRatio()
1833 {
1834 $config = VBOFactory::getConfig();
1835
1836 return (int)$config->get('split_stay_ratio', 50);
1837 }
1838
1839 /**
1840 * Sets the nights/transfers ratio for split stays. Use it to start applying limits.
1841 *
1842 * @param mixed $ratio integer value, or the default config setting will be applied.
1843 *
1844 * @return self
1845 *
1846 * @since 1.16.0 (J) - 1.6.0 (WP)
1847 */
1848 public function setNightsTransfersRatio($ratio = null)
1849 {
1850 if (is_int($ratio)) {
1851 $this->nights_transfers_ratio = $ratio;
1852 } else {
1853 $this->nights_transfers_ratio = $this->getDefaultNightsTransfersRatio();
1854 }
1855
1856 return $this;
1857 }
1858
1859 /**
1860 * Tells whether we need to behave for the front-end booking process.
1861 *
1862 * @return bool true if we are in the front-end booking process or false.
1863 *
1864 * @since 1.16.0 (J) - 1.6.0 (WP)
1865 */
1866 public function isFrontBooking()
1867 {
1868 return (bool)$this->is_front_booking;
1869 }
1870
1871 /**
1872 * Toggles the flag to behave for the front-end booking process.
1873 *
1874 * @return self
1875 *
1876 * @since 1.16.0 (J) - 1.6.0 (WP)
1877 */
1878 public function setIsFrontBooking($is_front = true)
1879 {
1880 $this->is_front_booking = (bool)$is_front;
1881
1882 return $this;
1883 }
1884
1885 /**
1886 * This helper method aims to collect the stay dates of each room in
1887 * a booking with split stay. Records will be loaded by ID ascending,
1888 * so in the same exact way as the rooms get stored. For this reason,
1889 * it is then possible to match the stay dates of a room by array-key,
1890 * even if bookings with split stay should always have different room IDs.
1891 *
1892 * @param int $bid the website reservation ID.
1893 *
1894 * @return array the list of busy records with stay dates information.
1895 *
1896 * @since 1.16.0 (J) - 1.6.0 (WP)
1897 */
1898 public function loadSplitStayBusyRecords($bid)
1899 {
1900 $dbo = JFactory::getDbo();
1901
1902 $bid = (int)$bid;
1903
1904 /**
1905 * It is fundamental to keep the ID column as the busy record ID
1906 * to allow the room switching in case of booking modification.
1907 */
1908 $q = "SELECT `ob`.`idorder`, `b`.`id`, `b`.`idroom`, `b`.`checkin`, `b`.`checkout`, `b`.`sharedcal`
1909 FROM `#__vikbooking_ordersbusy` AS `ob`
1910 LEFT JOIN `#__vikbooking_busy` AS `b` ON `ob`.`idbusy`=`b`.`id`
1911 WHERE `ob`.`idorder`={$bid}
1912 ORDER BY `b`.`sharedcal` ASC, `b`.`id` ASC;";
1913 $dbo->setQuery($q);
1914 $records = $dbo->loadAssocList();
1915
1916 if (!$records) {
1917 // the booking may no longer exist, or maybe it was cancelled
1918 return [];
1919 }
1920
1921 return $records;
1922 }
1923
1924 /**
1925 * Returns a list of the tax rate records.
1926 *
1927 * @return array the list of tax rates.
1928 *
1929 * @since 1.16.0 (J) - 1.6.0 (WP)
1930 */
1931 public function getTaxRates()
1932 {
1933 $dbo = JFactory::getDbo();
1934 $dbo->setQuery("SELECT * FROM `#__vikbooking_iva`;");
1935
1936 return $dbo->loadAssocList();
1937 }
1938
1939 /**
1940 * Sets the IDs of the rooms to filter or use.
1941 *
1942 * @param mixed $room_ids the list of room IDs to filter or use, or int room ID.
1943 *
1944 * @return self
1945 */
1946 public function setRoomIds($room_ids = [])
1947 {
1948 if (is_scalar($room_ids)) {
1949 // single room ID integer
1950 $room_ids = [$room_ids];
1951 }
1952
1953 $this->room_ids = $room_ids;
1954
1955 return $this;
1956 }
1957
1958 /**
1959 * Returns the current room ids to filter or use.
1960 *
1961 * @return array the current room ids.
1962 */
1963 public function getRoomIds()
1964 {
1965 return $this->room_ids;
1966 }
1967
1968 /**
1969 * In case of no availability, overrides the default number of days to check
1970 * prior and after the originally requested check-in and check-out dates.
1971 *
1972 * @param int $days the total number of days to use back and forth.
1973 *
1974 * @return self
1975 */
1976 public function setBackForthDays($days)
1977 {
1978 if (is_int($days) && $days >= 0) {
1979 $this->back_and_forth = $days;
1980 }
1981
1982 return $this;
1983 }
1984
1985 /**
1986 * Returns the current number of back and forth days.
1987 *
1988 * @return int the current number of days.
1989 */
1990 public function getBackForthDays()
1991 {
1992 return $this->back_and_forth;
1993 }
1994
1995 /**
1996 * Sets warning messages by concatenating the existing ones.
1997 *
1998 * @param string $str
1999 *
2000 * @return self
2001 */
2002 protected function setWarning($str)
2003 {
2004 $this->warning .= $str . "\n";
2005
2006 return $this;
2007 }
2008
2009 /**
2010 * Gets the current warning string.
2011 *
2012 * @return string
2013 */
2014 public function getWarning()
2015 {
2016 return rtrim($this->warning, "\n");
2017 }
2018
2019 /**
2020 * Sets errors by concatenating the existing ones.
2021 *
2022 * @param string $str
2023 *
2024 * @return self
2025 */
2026 protected function setError($str)
2027 {
2028 $this->error .= $str . "\n";
2029
2030 return $this;
2031 }
2032
2033 /**
2034 * Gets the current error string.
2035 *
2036 * @return string
2037 */
2038 public function getError()
2039 {
2040 return rtrim($this->error, "\n");
2041 }
2042
2043 /**
2044 * Gets the current error code.
2045 *
2046 * @return int
2047 */
2048 public function getErrorCode()
2049 {
2050 return $this->errorCode;
2051 }
2052 }
2053