PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.7.2
VikBooking Hotel Booking Engine & PMS v1.7.2
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.7.2, at admin/helpers/availability.php

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