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

2,368 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 ];
677
678 if ($tot_rooms > 1 && count($this->room_parties) == $tot_rooms) {
679 // re-build list of adults and children
680 $options['adults'] = [];
681 $options['children'] = [];
682 for ($i = 0; $i < $tot_rooms; $i++) {
683 $options['adults'][] = $this->getPartyGuests('adults', $i);
684 $options['children'][] = $this->getPartyGuests('children', $i);
685 }
686 }
687
688 // check for implicit settings
689 if (!empty($params['max_rooms_limit'])) {
690 // set rooms maximum limit
691 TACVBO::$maxRoomsLimit = (int) $params['max_rooms_limit'];
692 // unset implicit setting
693 unset($params['max_rooms_limit']);
694 } else {
695 // reset to initial value for subsequent calls
696 TACVBO::$maxRoomsLimit = 0;
697 }
698
699 if (is_array(($params['forced_room_ids'] ?? null))) {
700 // set allowed room IDs
701 TACVBO::$forcedRoomIds = $params['forced_room_ids'];
702 // unset implicit setting
703 unset($params['forced_room_ids']);
704 } else {
705 // reset to initial value for subsequent calls
706 TACVBO::$forcedRoomIds = [];
707 }
708
709 // merge default options with params, if any
710 $options = array_merge($options, $params);
711
712 // invoke TACVBO class by injecting the options
713 TACVBO::$getArray = true;
714 TACVBO::$ignoreRestrictions = $this->ignore_restrictions;
715 TACVBO::$ignoreAvailability = $this->ignore_availability;
716 $website_rates = TACVBO::tac_av_l($options);
717
718 // store the error code occurred (if any)
719 $this->errorCode = TACVBO::getErrorCode();
720
721 if (!is_array($website_rates)) {
722 // critical error
723 $this->setError(str_replace('e4j.error.', '', $website_rates));
724 return false;
725 }
726
727 if (isset($website_rates['e4j.error'])) {
728 // calculation/availability error
729 $this->setError($website_rates['e4j.error']);
730 // check if reserved keys like "fullybooked" are present
731 if (isset($website_rates['fullybooked']) && is_array($website_rates['fullybooked'])) {
732 // store fully booked rooms array
733 $this->fully_booked = $website_rates['fullybooked'];
734 }
735 // always return false
736 return false;
737 }
738
739 // optional filter by room IDs will be applied on this flow
740 $found_rids = array_keys($website_rates);
741 $unwanted_rids = $tot_rooms ? array_diff($found_rids, $this->room_ids) : [];
742 foreach ($unwanted_rids as $rid) {
743 unset($website_rates[$rid]);
744 }
745
746 return $website_rates;
747 }
748
749 /**
750 * Finds the available suggestions in case of no availability previously occurred
751 * while getting the rates. This method should be called after getRates() so that
752 * a valid errorCode to be analized will be available, unless code is forced.
753 * Suggestions can include closest booking dates and/or different room-guest parties.
754 *
755 * @param int $force_code the error code to force (no availability or party unsatisfied).
756 * @param array $force_rooms an optional list of room IDs to consider for the suggestions.
757 *
758 * @return array array of alternative dates, alternative room-parties and split stays.
759 *
760 * @since 1.16.0 (J) - 1.6.0 (WP) list of split stays available is also returned.
761 */
762 public function findSuggestions($force_code = 0, $force_rooms = [])
763 {
764 // reset error and warning strings to start a new calculation
765 $this->error = '';
766 $this->warning = '';
767
768 // build containers for the two types of suggestions
769 $alternative_dates = [];
770 $alternative_parties = [];
771 $split_stay_sols = [];
772
773 // the error code to parse
774 $use_ecode = $force_code ? $force_code : $this->errorCode;
775
776 if (empty($use_ecode)) {
777 // do not continue if no valid errors previously occurred or forced
778 $this->setError('Empty error code');
779 return [$alternative_dates, $alternative_parties, $split_stay_sols];
780 }
781
782 if ($use_ecode == 7 && (count($this->fully_booked) || count($force_rooms))) {
783 // get the closest booking dates for the compatible, yet unavailable, rooms
784 $use_rooms = count($force_rooms) ? $force_rooms : $this->fully_booked;
785 $alternative_dates = $this->findClosestRoomDateSolutions($use_rooms);
786 // calculate the split stay solutions available for the compatible rooms
787 $split_stay_sols = $this->findSplitStays($use_rooms);
788 }
789
790 if ($use_ecode == 3) {
791 // no rooms found for the given party, suggest alternative parties
792 $active_rooms = count($force_rooms) ? $force_rooms : array_keys($this->filterPublishedRooms());
793 // match all the available rooms in the requested or near dates
794 $all_solutions = $this->findClosestRoomDateSolutions($active_rooms);
795 // sort solutions by bigger rooms
796 $all_solutions = $this->sortBiggerRoomSolutions($all_solutions);
797 // find matching solutions for the requested party
798 $alternative_parties = $this->matchSolutionsParty($all_solutions);
799 }
800
801 return [$alternative_dates, $alternative_parties, $split_stay_sols];
802 }
803
804 /**
805 * Given a list of unavailable room IDs, yet compatible with the party and LOS requested,
806 * we build a list of available solutions for booking split stays on the same dates. The
807 * visibility should be public so that other Views could use just this method.
808 *
809 * @param array $room_ids list of unavailable, yet compatible, room IDs.
810 * @param array $busy_list optional list of busy records for the involved dates.
811 *
812 * @return array associative list of available split stays, or empty array.
813 *
814 * @since 1.16.0 (J) - 1.6.0 (WP)
815 */
816 public function findSplitStays(array $room_ids = [], array $busy_list = null)
817 {
818 if (!$room_ids) {
819 return [];
820 }
821
822 // get all website rooms
823 $all_rooms = $this->loadRooms();
824
825 // validate max occupancy for the given rooms
826 if (count($this->room_parties) === 1) {
827 // we only use the first party for the occupancy validation
828 $party_adults = $this->getPartyGuests('adults', $party = 0);
829 $party_children = $this->getPartyGuests('children', $party = 0);
830 foreach ($room_ids as $rindex => $rid) {
831 if (!isset($all_rooms[$rid])) {
832 unset($room_ids[$rindex]);
833 continue;
834 }
835 if ($party_adults > $all_rooms[$rid]['toadult'] || $party_children > $all_rooms[$rid]['tochild'] || ($party_adults + $party_children) > $all_rooms[$rid]['totpeople']) {
836 // max occupancy not met
837 unset($room_ids[$rindex]);
838 continue;
839 }
840 }
841
842 if (!$room_ids) {
843 return [];
844 }
845
846 // reset array keys
847 $room_ids = array_values($room_ids);
848 }
849
850 // get original check-in and check-out timestamps
851 list($orig_checkin_ts, $orig_checkout_ts) = $this->getStayDates(true);
852 $info_from = getdate($orig_checkin_ts);
853 $info_to = getdate($orig_checkout_ts);
854
855 // the final check-out date
856 $final_checkout_ymd = date('Y-m-d', $orig_checkout_ts);
857
858 // count original length of stay and nights involved
859 $tot_nights = $this->countNightsOfStay();
860 $groupdays = VikBooking::getGroupDays($orig_checkin_ts, $orig_checkout_ts, $tot_nights);
861
862 if ($tot_nights < 2) {
863 // useless to waste time on finding a split stay if not at least 2 nights
864 return [];
865 }
866
867 // load the occupied records for these dates and rooms
868 $busy_records = !is_null($busy_list) ? $busy_list : VikBooking::loadBusyRecords($room_ids, $orig_checkin_ts, strtotime('+1 day', $orig_checkout_ts));
869
870 // calculate available rooms for each night
871 $avroom_nights = [];
872 foreach ($room_ids as $rid) {
873 if (!isset($all_rooms[$rid])) {
874 continue;
875 }
876 $room = $all_rooms[$rid];
877 foreach ($groupdays as $gday) {
878 $day_key = date('Y-m-d', $gday);
879 $bfound = 0;
880 if (!isset($busy_records[$rid])) {
881 $busy_records[$rid] = [];
882 }
883 foreach ($busy_records[$rid] as $bu) {
884 $busy_info_in = getdate($bu['checkin']);
885 $busy_info_out = getdate($bu['checkout']);
886 $busy_in_ts = mktime(0, 0, 0, $busy_info_in['mon'], $busy_info_in['mday'], $busy_info_in['year']);
887 $busy_out_ts = mktime(0, 0, 0, $busy_info_out['mon'], $busy_info_out['mday'], $busy_info_out['year']);
888 if ($gday >= $busy_in_ts && $gday == $busy_out_ts && !$this->inonout_allowed && $room['units'] < 2) {
889 // check-ins on check-outs not allowed
890 $bfound++;
891 if ($bfound >= $room['units']) {
892 break;
893 }
894 }
895 if ($gday >= $busy_in_ts && $gday < $busy_out_ts) {
896 $bfound++;
897 if ($bfound >= $room['units']) {
898 break;
899 }
900 }
901 }
902 if ($bfound < $room['units']) {
903 // push this night as available for this room
904 if (!isset($avroom_nights[$rid])) {
905 $avroom_nights[$rid] = [];
906 }
907 $avroom_nights[$rid][] = $day_key;
908 } else {
909 // room not available on this night, make sure to unset any previous value
910 if (isset($avroom_nights[$rid]) && in_array($day_key, $avroom_nights[$rid])) {
911 $unav_key = array_search($day_key, $avroom_nights[$rid]);
912 unset($avroom_nights[$rid][$unav_key]);
913 $avroom_nights[$rid] = array_values($avroom_nights[$rid]);
914 }
915 }
916 }
917 }
918
919 if (!count($avroom_nights)) {
920 // no rooms available at all, there's no way to do anything
921 return [];
922 }
923
924 // make sure all nights requested can be satisfied by at least one room
925 foreach ($groupdays as $gday) {
926 $day_key = date('Y-m-d', $gday);
927 $day_av = false;
928 foreach ($avroom_nights as $rid => $av_nights) {
929 if (in_array($day_key, $av_nights)) {
930 // night was found
931 $day_av = true;
932 break;
933 }
934 }
935 if (!$day_av) {
936 // this night is not available in any room, unable to proceed
937 return [];
938 }
939 }
940
941 // count the number of consecutive nights per room
942 $cons_room_nights = [];
943 $tot_gdays = count($groupdays);
944 foreach ($groupdays as $k => $gday) {
945 $day_key = date('Y-m-d', $gday);
946 if (!isset($cons_room_nights[$day_key])) {
947 $cons_room_nights[$day_key] = [];
948 }
949 foreach ($avroom_nights as $rid => $av_nights) {
950 if (in_array($day_key, $av_nights)) {
951 if (!isset($cons_room_nights[$day_key][$rid])) {
952 $cons_room_nights[$day_key][$rid] = [];
953 }
954 // count the next consecutive nights of stay
955 $cons_room_nights[$day_key][$rid][] = $day_key;
956 for ($j = ($k + 1); $j < $tot_gdays; $j++) {
957 $next_day_key = date('Y-m-d', $groupdays[$j]);
958 if (in_array($next_day_key, $av_nights)) {
959 $cons_room_nights[$day_key][$rid][] = $next_day_key;
960 } else {
961 break;
962 }
963 }
964 }
965 }
966 }
967
968 // sort the solutions with the highest number of consecutive nights to reduce the splits
969 $cons_room_nights_sorted = [];
970 foreach ($cons_room_nights as $day_key => $cons_nights) {
971 $cons_room_nights_cnt = [];
972 foreach ($cons_nights as $rid => $cons_dates) {
973 $cons_room_nights_cnt[$rid] = count($cons_dates);
974 }
975 // sort the array in a descending order
976 arsort($cons_room_nights_cnt);
977 // restore sorted values in cloned array
978 $cons_room_nights_sorted[$day_key] = [];
979 foreach ($cons_room_nights_cnt as $rid => $tot_cons_nights) {
980 $cons_room_nights_sorted[$day_key][$rid] = $cons_room_nights[$day_key][$rid];
981 }
982 }
983 $cons_room_nights = $cons_room_nights_sorted;
984
985 // validate the data just built
986 $first_day_key = date('Y-m-d', $groupdays[0]);
987 if (!isset($cons_room_nights[$first_day_key]) || !count($cons_room_nights[$first_day_key]) || count($cons_room_nights) != count($groupdays)) {
988 // unable to proceed
989 return [];
990 }
991
992 // remove the consecutive nights from the check-out date as this won't be a night of stay
993 unset($cons_room_nights[$final_checkout_ymd]);
994
995 // build the split stay solutions
996 $split_stay_sols = [];
997
998 // the number of rooms available on the first night should determine the number of split stay solutions
999 foreach ($cons_room_nights[$first_day_key] as $start_rid => $cons_nights) {
1000 // start container of the various splits for this stay
1001 $split_stay_sol = [];
1002
1003 // calculate last consecutive night available
1004 $leave_date = end($cons_nights);
1005 $leave_date_info = getdate(strtotime($leave_date));
1006
1007 // set the check-out date to the day after the last night
1008 $checkout_ymd = date('Y-m-d', mktime(0, 0, 0, $leave_date_info['mon'], ($leave_date_info['mday'] + 1), $leave_date_info['year']));
1009
1010 // define the first stay
1011 $split_stay = [
1012 'idroom' => $start_rid,
1013 'room_name' => $all_rooms[$start_rid]['name'],
1014 'checkin' => $cons_nights[0],
1015 'checkout' => $checkout_ymd,
1016 'nights' => $this->countNightsOfStay(strtotime($cons_nights[0]), strtotime($checkout_ymd)),
1017 ];
1018
1019 // make sure this room has got a tariff defined for this number of nights of stay
1020 if (!$this->roomNightsAllowed($start_rid, $split_stay['nights'])) {
1021 // no tariffs found for this los
1022 continue;
1023 }
1024
1025 // push first stay
1026 $split_stay_sol[] = $split_stay;
1027
1028 // loop through the next stays
1029 while (isset($cons_room_nights[$checkout_ymd])) {
1030 /**
1031 * For the next splits, we use just the first available rooms, which is
1032 * the one with the highest number of consecutive nights available.
1033 */
1034 foreach ($cons_room_nights[$checkout_ymd] as $rid => $split_cons_nights) {
1035 // calculate last consecutive night available
1036 $leave_date = end($split_cons_nights);
1037 $leave_date_info = getdate(strtotime($leave_date));
1038
1039 // set the check-out date to the day after the last night
1040 $checkout_ymd = date('Y-m-d', mktime(0, 0, 0, $leave_date_info['mon'], ($leave_date_info['mday'] + 1), $leave_date_info['year']));
1041 if ($leave_date == $final_checkout_ymd) {
1042 // check-out date reached
1043 $checkout_ymd = $final_checkout_ymd;
1044 }
1045
1046 // count nights of stay
1047 $split_nights = $this->countNightsOfStay(strtotime($split_cons_nights[0]), strtotime($checkout_ymd));
1048
1049 // make sure this room has got a tariff defined for this number of nights of stay
1050 if (!$this->roomNightsAllowed($rid, $split_nights)) {
1051 // no tariffs found for this los, abort solution
1052 $split_stay_sol = [];
1053 break 2;
1054 }
1055
1056 // push split stay
1057 $split_stay_sol[] = [
1058 'idroom' => $rid,
1059 'room_name' => $all_rooms[$rid]['name'],
1060 'checkin' => $split_cons_nights[0],
1061 'checkout' => $checkout_ymd,
1062 'nights' => $split_nights,
1063 ];
1064
1065 // we try to reduce the number of splits by considering just the first room
1066 break;
1067 }
1068 }
1069
1070 if (count($split_stay_sol) < 2) {
1071 // not a split stay, but rather a fully available room
1072 continue;
1073 }
1074
1075 // push split stay solution
1076 $split_stay_sols[] = $split_stay_sol;
1077 }
1078
1079 /**
1080 * Load rooms involved in all split stays in order to validate
1081 * global/room-level restrictions and closing dates on the stay.
1082 */
1083 $rooms_involved = [];
1084 foreach ($split_stay_sols as $split_stay_sol) {
1085 foreach ($split_stay_sol as $split_stay) {
1086 if (!in_array($split_stay['idroom'], $rooms_involved)) {
1087 $rooms_involved[] = $split_stay['idroom'];
1088 }
1089 }
1090 }
1091
1092 // load restrictions for all rooms involved
1093 $all_restrictions = VikBooking::loadRestrictions(true, $rooms_involved);
1094 $glob_restrictions = VikBooking::globalRestrictions($all_restrictions);
1095 $invalid_room_restr = [];
1096
1097 // validate global restrictions
1098 if (VikBooking::validateRoomRestriction($glob_restrictions, $info_from, $info_to, $tot_nights)) {
1099 // global restrictions apply over this stay
1100 return [];
1101 }
1102
1103 // validate closing dates
1104 if (VikBooking::validateClosingDates($orig_checkin_ts, $orig_checkout_ts)) {
1105 // global closing dates apply over this stay
1106 return [];
1107 }
1108
1109 // validate restrictions at room level
1110 foreach ($rooms_involved as $rid) {
1111 // load restrictions at room level
1112 $room_level_restr = VikBooking::roomRestrictions($rid, $all_restrictions);
1113 if (VikBooking::validateRoomRestriction($room_level_restr, $info_from, $info_to, $tot_nights)) {
1114 // room-level restrictions apply over this stay
1115 $invalid_room_restr[] = $rid;
1116 }
1117 }
1118
1119 // unset the split stays with the restricted rooms (if any)
1120 $altered_sols = false;
1121 foreach ($invalid_room_restr as $rid) {
1122 foreach ($split_stay_sols as $k => $split_stay_sol) {
1123 foreach ($split_stay_sol as $split_stay) {
1124 if ($rid == $split_stay['idroom']) {
1125 // this booking split stay cannot be suggested because of this room
1126 unset($split_stay_sols[$k]);
1127 $altered_sols = true;
1128 continue 2;
1129 }
1130 }
1131 }
1132 }
1133
1134 // apply nights/transfers ratio limit (unless disabled)
1135 $nights_transfers_ratio = $this->getNightsTransfersRatio();
1136 if ($nights_transfers_ratio > 0 && $nights_transfers_ratio < 100) {
1137 // count and apply limits
1138 foreach ($split_stay_sols as $k => $split_stay_sol) {
1139 // count nights and transfers
1140 $split_stay_transfers = count($split_stay_sol) - 1;
1141 $split_stay_nights = 0;
1142 foreach ($split_stay_sol as $split_stay_room) {
1143 $split_stay_nights += $split_stay_room['nights'];
1144 }
1145 // max allowed transfers
1146 $max_transfers = round($split_stay_nights * $nights_transfers_ratio / 100, 0);
1147 if (!$split_stay_transfers || $split_stay_transfers > $max_transfers) {
1148 // unset solution
1149 unset($split_stay_sols[$k]);
1150 $altered_sols = true;
1151 }
1152 }
1153 }
1154
1155 if ($altered_sols && count($split_stay_sols)) {
1156 // restore the array keys
1157 $split_stay_sols = array_values($split_stay_sols);
1158 }
1159
1160 // return the available booking split stay solutions (if any)
1161 return $split_stay_sols;
1162 }
1163
1164 /**
1165 * Returns the number of guests requested from the given room-party index.
1166 *
1167 * @param string $guest either "adults", "children" or "guests".
1168 * @param int $party the party index number, 0 by default.
1169 *
1170 * @return int the total number of guests requested in the party.
1171 */
1172 protected function getPartyGuests($guest = 'adults', $party = 0)
1173 {
1174 if (!isset($this->room_parties[$party])) {
1175 return 0;
1176 }
1177
1178 if (!strcasecmp($guest, 'adults')) {
1179 // adults
1180 return $this->room_parties[$party]['adults'];
1181 }
1182
1183 if (!strcasecmp($guest, 'children')) {
1184 // children
1185 return $this->room_parties[$party]['children'];
1186 }
1187
1188 // total guests
1189 $tot_guests = 0;
1190 foreach ($this->room_parties as $rparty) {
1191 $tot_guests += $rparty['adults'];
1192 $tot_guests += $rparty['children'];
1193 }
1194
1195 return $tot_guests;
1196 }
1197
1198 /**
1199 * Given a list of unavailable room IDs, yet compatible with the party and LOS requested,
1200 * we build a list of available dates when such rooms could be booked for the same LOS.
1201 *
1202 * @param array $room_ids list of unavailable, yet compatible, room IDs.
1203 *
1204 * @return array associative list of available room-dates, or empty array.
1205 */
1206 protected function findClosestRoomDateSolutions($room_ids = [])
1207 {
1208 if (!$room_ids) {
1209 return [];
1210 }
1211
1212 // get all website rooms
1213 $all_rooms = $this->loadRooms();
1214
1215 // get original check-in and check-out timestamps
1216 list($orig_checkin_ts, $orig_checkout_ts) = $this->getStayDates(true);
1217 $info_from = getdate($orig_checkin_ts);
1218 $info_to = getdate($orig_checkout_ts);
1219
1220 // count original length of stay
1221 $tot_nights = $this->countNightsOfStay();
1222
1223 // earliest checkin timestamp allowed
1224 $lim_past_ts = mktime(0, 0, 0, date('n'), ((int)date('j') + VikBooking::getMinDaysAdvance()), date('Y'));
1225
1226 // suggested range of dates (+/- "back and forth" days from original dates)
1227 $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']);
1228 if ($sug_from_ts < $lim_past_ts) {
1229 $sug_from_ts = $lim_past_ts;
1230 // since we are close to the requested check-in, double up the "back and forth" for the max date
1231 $this->setBackForthDays($this->getBackForthDays() * 2);
1232 }
1233 $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']);
1234 $sug_to_ts = $sug_to_ts < $sug_from_ts ? $sug_from_ts : $sug_to_ts;
1235
1236 // get days timestamps for suggestions
1237 $groupdays = [];
1238 $sug_start_info = getdate($sug_from_ts);
1239 $sug_from_midnight = mktime(0, 0, 0, $sug_start_info['mon'], $sug_start_info['mday'], $sug_start_info['year']);
1240 $sug_start_info = getdate($sug_from_midnight);
1241 while ($sug_start_info[0] <= $sug_to_ts) {
1242 array_push($groupdays, $sug_start_info[0]);
1243 $sug_start_info = getdate(mktime(0, 0, 0, $sug_start_info['mon'], ($sug_start_info['mday'] + 1), $sug_start_info['year']));
1244 }
1245
1246 // build suggestions array of dates with some availability for the given rooms
1247 $suggestions = [];
1248 $busy_records = VikBooking::loadBusyRecords($room_ids, $sug_from_ts, strtotime('+1 day', $sug_to_ts));
1249 foreach ($room_ids as $rid) {
1250 if (!isset($all_rooms[$rid])) {
1251 continue;
1252 }
1253 $room = $all_rooms[$rid];
1254 foreach ($groupdays as $gday) {
1255 $day_key = date('Y-m-d', $gday);
1256 $bfound = 0;
1257 if (!isset($busy_records[$rid])) {
1258 $busy_records[$rid] = [];
1259 }
1260 foreach ($busy_records[$rid] as $bu) {
1261 $busy_info_in = getdate($bu['checkin']);
1262 $busy_info_out = getdate($bu['checkout']);
1263 $busy_in_ts = mktime(0, 0, 0, $busy_info_in['mon'], $busy_info_in['mday'], $busy_info_in['year']);
1264 $busy_out_ts = mktime(0, 0, 0, $busy_info_out['mon'], $busy_info_out['mday'], $busy_info_out['year']);
1265 if ($gday >= $busy_in_ts && $gday == $busy_out_ts && !$this->inonout_allowed && $room['units'] < 2) {
1266 // check-ins on check-outs not allowed
1267 $bfound++;
1268 if ($bfound >= $room['units']) {
1269 break;
1270 }
1271 }
1272 if ($gday >= $busy_in_ts && $gday < $busy_out_ts) {
1273 $bfound++;
1274 if ($bfound >= $room['units']) {
1275 break;
1276 }
1277 }
1278 }
1279 if ($bfound < $room['units']) {
1280 if (!isset($suggestions[$day_key])) {
1281 $suggestions[$day_key] = [];
1282 }
1283 $room_day = $room;
1284 $room_day['units_left'] = $room['units'] - $bfound;
1285 $suggestions[$day_key] = $suggestions[$day_key] + [$rid => $room_day];
1286 }
1287 }
1288 }
1289
1290 if (!$suggestions) {
1291 // no available nights found for the prior and next "back and forth" days for the given rooms
1292 return [];
1293 }
1294
1295 // build the solutions array with keys=checkin, values=all rooms suited for the requested number of nights
1296 $solutions = [];
1297 // get all rooms available for the number of nights requested in the suggestions array of dates
1298 foreach ($suggestions as $kday => $rooms) {
1299 $day_ts_info = getdate(strtotime($kday));
1300 foreach ($rooms as $rid => $room) {
1301 $suitable = true;
1302 $room_days_av_left = [$kday => $room['units_left']];
1303 for ($i = 1; $i < $tot_nights; $i++) {
1304 $next_night = mktime(0, 0, 0, $day_ts_info['mon'], ($day_ts_info['mday'] + $i), $day_ts_info['year']);
1305 $next_night_dt = date('Y-m-d', $next_night);
1306 if (!isset($suggestions[$next_night_dt]) || !isset($suggestions[$next_night_dt][$rid])) {
1307 $suitable = false;
1308 break;
1309 }
1310 $room_days_av_left[$next_night_dt] = $suggestions[$next_night_dt][$rid]['units_left'];
1311 }
1312 if ($suitable === true) {
1313 if (!isset($solutions[$kday])) {
1314 $solutions[$kday] = [];
1315 }
1316 unset($room['units_left']);
1317 $room['days_av_left'] = $room_days_av_left;
1318 $solutions[$kday] = $solutions[$kday] + [$rid => $room];
1319 }
1320 }
1321 }
1322
1323 if (!count($solutions)) {
1324 // the requested length of stay could not be satisfied for any available night
1325 return [];
1326 }
1327
1328 // sort the solutions by the closest checkin date to the one requested
1329 $sortmap = [];
1330 $orig_checkin_ymd = date('Y-m-d', $orig_checkin_ts);
1331 foreach ($solutions as $kday => $solution) {
1332 $kdayts = strtotime($kday);
1333 $sortmap[$kdayts] = $kdayts > $orig_checkin_ts ? ($kdayts - $orig_checkin_ts) : ($orig_checkin_ts - $kdayts);
1334 if ($orig_checkin_ymd == $kday) {
1335 // the original check-in day is available, so we want it to be first, regardless of the check-in time
1336 $sortmap[$kdayts] = 1;
1337 }
1338 }
1339 asort($sortmap);
1340 $sorted = [];
1341 foreach ($sortmap as $kdayts => $v) {
1342 $kday = date('Y-m-d', $kdayts);
1343 $sorted[$kday] = $solutions[$kday];
1344 }
1345 $solutions = $sorted;
1346 unset($sorted);
1347
1348 /**
1349 * Load rooms involved in the final alternative solutions in order
1350 * to validate global/room-level restrictions and closing dates.
1351 *
1352 * @since 1.15.4 (J) - 1.5.10 (WP)
1353 */
1354 $rooms_involved = [];
1355 foreach ($solutions as $arrive_ymd => $roomsol) {
1356 foreach (array_keys($roomsol) as $rid) {
1357 if (!in_array($rid, $rooms_involved)) {
1358 $rooms_involved[] = $rid;
1359 }
1360 }
1361 }
1362
1363 // load restrictions for all rooms involved
1364 $all_restrictions = VikBooking::loadRestrictions(true, $rooms_involved);
1365 $glob_restrictions = VikBooking::globalRestrictions($all_restrictions);
1366 $room_level_restr = [];
1367
1368 foreach ($solutions as $arrive_ymd => $roomsol) {
1369 // build suggested stay dates
1370 $sug_in = getdate(strtotime($arrive_ymd));
1371 $sug_out = getdate(mktime(0, 0, 0, $sug_in['mon'], ($sug_in['mday'] + $tot_nights), $sug_in['year']));
1372 // validate global restrictions
1373 if (VikBooking::validateRoomRestriction($glob_restrictions, $sug_in, $sug_out, $tot_nights)) {
1374 // global restrictions apply over this stay
1375 unset($solutions[$arrive_ymd]);
1376 continue;
1377 }
1378 // validate closing dates
1379 if (VikBooking::validateClosingDates($sug_in[0], $sug_out[0])) {
1380 // global closing dates apply over this stay
1381 unset($solutions[$arrive_ymd]);
1382 continue;
1383 }
1384 // validate restrictions at room level
1385 foreach ($roomsol as $rid => $rdata) {
1386 if (!isset($room_level_restr[$rid])) {
1387 // load restrictions at room level
1388 $room_level_restr[$rid] = VikBooking::roomRestrictions($rid, $all_restrictions);
1389 }
1390 if (VikBooking::validateRoomRestriction($room_level_restr[$rid], $sug_in, $sug_out, $tot_nights)) {
1391 // room-level restrictions apply over this stay
1392 unset($solutions[$arrive_ymd][$rid]);
1393 if (!count($solutions[$arrive_ymd])) {
1394 // unset the entire suggested date
1395 unset($solutions[$arrive_ymd]);
1396 break;
1397 }
1398 continue;
1399 }
1400 }
1401 }
1402
1403 if (!count($solutions)) {
1404 // the calculated suggestions do not meet the restrictions or the closing dates
1405 return [];
1406 }
1407
1408 // return the solution alternative dates for all rooms available
1409 return $solutions;
1410 }
1411
1412 /**
1413 * Sorts an associative array of room-solutions by the bigger rooms.
1414 *
1415 * @param array $solutions the date solutions obtained for some rooms.
1416 *
1417 * @return array the same array sorted by bigger rooms on top.
1418 */
1419 protected function sortBiggerRoomSolutions($solutions)
1420 {
1421 if (!is_array($solutions) || !$solutions) {
1422 return $solutions;
1423 }
1424
1425 // sort rooms-solutions by max-adults, 'max-guests', 'max-children', in a descending order
1426 foreach ($solutions as $kday => $solution) {
1427 // with this sorting, we will have the bigger rooms on top to quickly fit the party requested
1428 uasort($solutions[$kday], function($a, $b) {
1429 if ($a['toadult'] == $b['toadult']) {
1430 if ($a['totpeople'] == $b['totpeople']) {
1431 return $a['tochild'] > $b['tochild'] ? -1 : 1;
1432 }
1433 return $a['totpeople'] > $b['totpeople'] ? -1 : 1;
1434 }
1435 return $a['toadult'] > $b['toadult'] ? -1 : 1;
1436 });
1437 }
1438
1439 return $solutions;
1440 }
1441
1442 /**
1443 * Given a list of available dates and rooms (solutions), attempts
1444 * to match a party of rooms that fits the party requested.
1445 *
1446 * @param array $solutions the list of available dates and related rooms.
1447 *
1448 * @return array list of alternative party solutions, if any.
1449 */
1450 protected function matchSolutionsParty($solutions)
1451 {
1452 if (!is_array($solutions) || !$solutions) {
1453 return [];
1454 }
1455
1456 // build the list of alternative parties
1457 $alternative_parties = [];
1458
1459 // build list of party guests
1460 $party_guests = [
1461 'adults' => 0,
1462 'children' => 0,
1463 ];
1464 foreach ($this->room_parties as $rparty) {
1465 $party_guests['adults'] += $rparty['adults'];
1466 $party_guests['children'] += $rparty['children'];
1467 }
1468
1469 // check if the rooms of each solution can fit the number of guests requested, unset the solution otherwise
1470 foreach ($solutions as $kday => $solution) {
1471 $solution_guests = [
1472 'adults' => 0,
1473 'children' => 0,
1474 ];
1475 foreach ($solution as $rid => $roomsol) {
1476 // count minimum units left for this room
1477 $room_min_uleft = min($roomsol['days_av_left']);
1478 // check if this solution of rooms can allocate all the guests requested
1479 if ($roomsol['totpeople'] < ($roomsol['toadult'] + $roomsol['tochild']) && !$party_guests['children']) {
1480 // in case of no children requested, we ignore them to avoid adjusting the room capacity
1481 $roomsol['tochild'] = 0;
1482 }
1483 if ($roomsol['totpeople'] < ($roomsol['toadult'] + $roomsol['tochild'])) {
1484 /**
1485 * The sum of the max_adults and max_children exceeds the max_guests: lower the adults
1486 * we can take first (if party children > 0), then the children, until sum=max_guests
1487 */
1488 while (($roomsol['toadult'] > 0 || $roomsol['tochild'] > 0)) {
1489 if (!$party_guests['children'] && $roomsol['totpeople'] == $roomsol['toadult']) {
1490 /**
1491 * When no children requested in the party, we cannot under-utilize rooms.
1492 * Break the loop without lowering the 'toadult'.
1493 */
1494 $roomsol['tochild'] = 0;
1495 break;
1496 }
1497 if ($party_guests['children'] && $solution_guests['children'] >= $party_guests['children']) {
1498 /**
1499 * If all the children requested were allocated in other solutions,
1500 * we should not under-utilize rooms by reducing the number of adults.
1501 */
1502 break;
1503 }
1504 if ($roomsol['toadult'] > 0 && $party_guests['children'] > 0 && !($roomsol['tochild'] > $party_guests['children'])) {
1505 /**
1506 * We lower first the adults that we put in this room, only if there are children in the party
1507 * and if the children in the party are more than the 'max_children' of this room.
1508 */
1509 $roomsol['toadult']--;
1510 if ($roomsol['totpeople'] >= ($roomsol['toadult'] + $roomsol['tochild'])) {
1511 break;
1512 }
1513 }
1514 if ($roomsol['tochild'] > 0) {
1515 // if the max_guests is still greater than the sum of adults+children we take, take out one child
1516 $roomsol['tochild']--;
1517 if ($roomsol['totpeople'] >= ($roomsol['toadult'] + $roomsol['tochild'])) {
1518 break;
1519 }
1520 }
1521 if ($roomsol['toadult'] > 0) {
1522 // 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
1523 $roomsol['toadult']--;
1524 if ($roomsol['totpeople'] >= ($roomsol['toadult'] + $roomsol['tochild'])) {
1525 break;
1526 }
1527 }
1528 }
1529 }
1530 $solution_guests['adults'] += $roomsol['toadult'] * $room_min_uleft;
1531 $solution_guests['children'] += $roomsol['tochild'] * $room_min_uleft;
1532 // update 'max_adults' and 'max_children' for this solution (for later guests allocation)
1533 $solution[$rid]['toadult'] = $roomsol['toadult'];
1534 $solution[$rid]['tochild'] = $roomsol['tochild'];
1535 }
1536
1537 $solutions[$kday] = $solution;
1538 if ($solution_guests['adults'] < $party_guests['adults'] || $solution_guests['children'] < $party_guests['children']) {
1539 // the guests we can allocate with the solution of this day are not enough: unset the solution
1540 unset($solutions[$kday]);
1541 continue;
1542 }
1543
1544 // if we get to this point we can suggest a booking solution for the party requested, but in different rooms
1545 if (!isset($alternative_parties[$kday])) {
1546 $alternative_parties[$kday] = [];
1547 }
1548
1549 // re-loop over the rooms in this solution to build the booking solution for this day
1550 $guests_allocated = [
1551 'adults' => 0,
1552 'children' => 0
1553 ];
1554
1555 /**
1556 * The rooms available for an alternative booking solutions have been sorted by capacity
1557 * in a descending order to quickly fit the guest party requested. However, if a smaller
1558 * and cheaper room was capable of fitting all guests, we should opt for this solution.
1559 *
1560 * @since 1.15.4 (J) - 1.5.9 (WP)
1561 */
1562 $smaller_fit_found = false;
1563 $smaller_solutions = array_reverse($solution, true);
1564 foreach ($smaller_solutions as $rid => $roomsol) {
1565 if ($party_guests['adults'] > 0 && $party_guests['adults'] > $roomsol['toadult']) {
1566 // too many adults requested for this small room
1567 continue;
1568 }
1569 if ($party_guests['children'] > 0 && $party_guests['children'] > $roomsol['tochild']) {
1570 // too many children requested for this small room
1571 continue;
1572 }
1573 if (($party_guests['adults'] + $party_guests['children']) > $roomsol['totpeople']) {
1574 // too many guests requested for this small room
1575 continue;
1576 }
1577 // we've got a fitting room which could be smaller
1578 $roomsol['guests_allocation'] = [
1579 'adults' => $party_guests['adults'],
1580 'children' => $party_guests['children'],
1581 ];
1582 array_push($alternative_parties[$kday], $roomsol);
1583 // turn flag on and break the loop
1584 $smaller_fit_found = true;
1585 break;
1586 }
1587 if ($smaller_fit_found) {
1588 // no need to parse the rooms from the largest to the smallest
1589 continue;
1590 }
1591
1592 foreach ($solution as $rid => $roomsol) {
1593 // count minimum units left for this room
1594 $room_min_uleft = min($roomsol['days_av_left']);
1595 // fullfil all the units of this room
1596 for ($units_taken = 0; $units_taken < $room_min_uleft; $units_taken++) {
1597 $current_allocation = [
1598 'adults' => 0,
1599 'children' => 0
1600 ];
1601 if ($guests_allocated['adults'] < $party_guests['adults']) {
1602 $humans_taken = $roomsol['toadult'];
1603 $missing_humans = $party_guests['adults'] - $guests_allocated['adults'];
1604 $humans_taken = $humans_taken > $missing_humans ? $missing_humans : $humans_taken;
1605
1606 $current_allocation['adults'] = $humans_taken;
1607 $guests_allocated['adults'] += $humans_taken;
1608 }
1609 if ($guests_allocated['children'] < $party_guests['children']) {
1610 $humans_taken = $roomsol['tochild'];
1611 $missing_humans = $party_guests['children'] - $guests_allocated['children'];
1612 $humans_taken = $humans_taken > $missing_humans ? $missing_humans : $humans_taken;
1613
1614 $current_allocation['children'] = $humans_taken;
1615 $guests_allocated['children'] += $humans_taken;
1616 }
1617 $roomsol['guests_allocation'] = $current_allocation;
1618 array_push($alternative_parties[$kday], $roomsol);
1619 if ($guests_allocated['adults'] >= $party_guests['adults'] && $guests_allocated['children'] >= $party_guests['children']) {
1620 // we have allocated all guests, exit the for-loop
1621 break;
1622 }
1623 }
1624 if ($guests_allocated['adults'] >= $party_guests['adults'] && $guests_allocated['children'] >= $party_guests['children']) {
1625 //we have allocated all guests with this solution, no need to loop over other rooms available in this day.
1626 break;
1627 }
1628 }
1629 }
1630
1631 // return the alternative parties found, if any
1632 return $alternative_parties;
1633 }
1634
1635 /**
1636 * Given a list of alternative dates obtained from an inquiry/request information,
1637 * composes a valid room-rate array to store the inquiry reservation. By calling this
1638 * method, the original stay dates will be overwritten.
1639 *
1640 * @param array $alt_dates the list of alternative dates found for the stay.
1641 * @param object $customer a stdClass object with the basic customer details.
1642 *
1643 * @return int the ID of the newly created inquiry reservation.
1644 */
1645 public function allocateAltDatesInquiry($alt_dates, $customer)
1646 {
1647 if (!is_array($alt_dates) || !$alt_dates) {
1648 return 0;
1649 }
1650
1651 foreach ($alt_dates as $ymd => $rooms) {
1652 // we expect just one room-type for the party, and we use the first suggestion
1653 foreach ($rooms as $rid => $alt_stay) {
1654 if (empty($alt_stay['days_av_left']) || !is_array($alt_stay['days_av_left'])) {
1655 // invalid structure
1656 continue;
1657 }
1658 // compose the new stay dates
1659 $sugg_checkin_dt = null;
1660 $sugg_checkout_dt = null;
1661 foreach ($alt_stay['days_av_left'] as $dayk => $uleft) {
1662 if (empty($sugg_checkin_dt)) {
1663 // grab the first date
1664 $sugg_checkin_dt = $dayk;
1665 }
1666 // always overwrite until last date
1667 $sugg_checkout_dt = $dayk;
1668 }
1669 // increase check-out date by one day (day after last night of stay)
1670 $sugg_out_info = getdate(strtotime($sugg_checkout_dt));
1671 $sugg_checkout_dt = date('Y-m-d', mktime(0, 0, 0, $sugg_out_info['mon'], ($sugg_out_info['mday'] + 1), $sugg_out_info['year']));
1672
1673 // set the new stay dates
1674 $this->setStayDates($sugg_checkin_dt, $sugg_checkout_dt);
1675
1676 // build the room rate plan array without any rate plan information
1677 $room_rplan = [
1678 'idroom' => $alt_stay['id'],
1679 ];
1680
1681 // create the inquiry reservation for the closest alternative dates
1682 return $this->createInquiryReservation($room_rplan, $customer);
1683 }
1684 }
1685
1686 return 0;
1687 }
1688
1689 /**
1690 * Given a list of alternative parties obtained from an inquiry/request information,
1691 * composes valid room-rate arrays to store the inquiry reservation. By calling this
1692 * method, the original stay dates and room party will be overwritten.
1693 *
1694 * @param array $alt_parties the list of alternative parties found for the stay.
1695 * @param object $customer a stdClass object with the basic customer details.
1696 *
1697 * @return int the ID of the newly created inquiry reservation.
1698 */
1699 public function allocateAltPartyInquiry($alt_parties, $customer)
1700 {
1701 if (!is_array($alt_parties) || !$alt_parties) {
1702 return 0;
1703 }
1704
1705 // build list of rooms to assign to the inquiry reservation
1706 $room_rates = [];
1707
1708 // start party counter
1709 $party_counter = 0;
1710
1711 foreach ($alt_parties as $ymd => $alt_rooms) {
1712 // we expect to have more than one room-type for the large party suggestion
1713 foreach ($alt_rooms as $alt_room) {
1714 if (empty($alt_room['guests_allocation']) || !is_array($alt_room['guests_allocation'])) {
1715 // invalid structure
1716 continue;
1717 }
1718 if (empty($alt_room['days_av_left']) || !is_array($alt_room['days_av_left'])) {
1719 // invalid structure
1720 continue;
1721 }
1722 // compose the new stay dates
1723 $sugg_checkin_dt = null;
1724 $sugg_checkout_dt = null;
1725 foreach ($alt_room['days_av_left'] as $dayk => $uleft) {
1726 if (empty($sugg_checkin_dt)) {
1727 // grab the first date
1728 $sugg_checkin_dt = $dayk;
1729 }
1730 // always overwrite until last date
1731 $sugg_checkout_dt = $dayk;
1732 }
1733 // increase check-out date by one day (day after last night of stay)
1734 $sugg_out_info = getdate(strtotime($sugg_checkout_dt));
1735 $sugg_checkout_dt = date('Y-m-d', mktime(0, 0, 0, $sugg_out_info['mon'], ($sugg_out_info['mday'] + 1), $sugg_out_info['year']));
1736
1737 // set the new stay dates
1738 $this->setStayDates($sugg_checkin_dt, $sugg_checkout_dt);
1739
1740 // set the current guests party (the first will replace the previous party, others will be pushed)
1741 $this->setRoomParty($alt_room['guests_allocation']['adults'], $alt_room['guests_allocation']['children'], ($party_counter === 0));
1742
1743 // increase party counter
1744 $party_counter++;
1745
1746 // push current room with no rate plan information
1747 array_push($room_rates, [
1748 'idroom' => $alt_room['id'],
1749 ]);
1750 }
1751
1752 if (count($room_rates)) {
1753 // we use the closest dates in the first suggestion party array
1754 break;
1755 }
1756 }
1757
1758 // count total rooms in the party
1759 $tot_room_party = count($room_rates);
1760
1761 if (!$tot_room_party) {
1762 // something went wrong
1763 return 0;
1764 }
1765
1766 // grab the main/first room reservation
1767 $room_rplan = $room_rates[0];
1768
1769 // build extra rooms
1770 $extra_rooms = [];
1771 if ($tot_room_party > 1) {
1772 // grab the remaining rooms
1773 unset($room_rates[0]);
1774 $extra_rooms = array_values($room_rates);
1775 }
1776
1777 // create the inquiry reservation for the closest alternative dates and rooms party
1778 return $this->createInquiryReservation($room_rplan, $customer, $extra_rooms);
1779 }
1780
1781 /**
1782 * Creates a new pending reservation from the inquiry/request information.
1783 * Requires a valid room-rate array to be available, or in case suggestions should
1784 * be applied, the room-rate array should be adjusted to comply with this method.
1785 *
1786 * @param array $room_rplan a room-rate array to allocate the booking.
1787 * @param object $customer a stdClass object with the basic customer details.
1788 * @param array $extra_rooms optional list of additional room-rate arrays to store
1789 * in case of alternative parties suggested.
1790 *
1791 * @return int the ID of the newly created inquiry reservation.
1792 */
1793 public function createInquiryReservation($room_rplan, $customer, $extra_rooms = array())
1794 {
1795 if (!is_array($room_rplan) || empty($room_rplan['idroom'])) {
1796 return 0;
1797 }
1798
1799 if (empty($this->stay_ts) || empty($this->room_parties)) {
1800 // no stay dates or room party set
1801 return 0;
1802 }
1803
1804 $dbo = JFactory::getDbo();
1805
1806 // build reservation object
1807 $res_obj = new stdClass;
1808 $res_obj->custdata = $customer->custdata;
1809 $res_obj->ts = time();
1810 $res_obj->status = 'standby';
1811 $res_obj->days = $this->countNightsOfStay();
1812 $res_obj->checkin = $this->stay_ts[0];
1813 $res_obj->checkout = $this->stay_ts[1];
1814 $res_obj->custmail = $customer->email;
1815 $res_obj->sid = VikBooking::getSecretLink();
1816 $res_obj->idpayment = $this->getDefaultPaymentId();
1817 $res_obj->roomsnum = count($this->room_parties);
1818 if (!empty($room_rplan['cost'])) {
1819 $res_obj->total = (float)$room_rplan['cost'];
1820 }
1821 $res_obj->adminnotes = $customer->adminnotes;
1822 $res_obj->lang = $customer->lang;
1823 $res_obj->country = $customer->country;
1824 if (!empty($room_rplan['taxes'])) {
1825 $res_obj->tot_taxes = (float)$room_rplan['taxes'];
1826 }
1827 if (!empty($room_rplan['city_taxes'])) {
1828 $res_obj->tot_city_taxes = (float)$room_rplan['city_taxes'];
1829 }
1830 $res_obj->phone = $customer->phone;
1831 $res_obj->type = 'inquiry';
1832
1833 // store record
1834 if (!$dbo->insertObject('#__vikbooking_orders', $res_obj, 'id')) {
1835 // could not store the booking record
1836 return 0;
1837 }
1838
1839 // get the ID of the newly created reservation
1840 $res_id = $res_obj->id;
1841
1842 // check if mandatory options should be assigned (not in case of suggestions)
1843 $room_options = null;
1844 if (!empty($room_rplan['idprice']) && isset($res_obj->tot_city_taxes)) {
1845 $mand_taxes = VikBooking::getMandatoryTaxesFees([$room_rplan['idroom']], $this->getPartyGuests('adults', 0), $res_obj->days);
1846 if (is_array($mand_taxes) && !empty($mand_taxes['options'])) {
1847 $room_options = implode(';', $mand_taxes['options']);
1848 }
1849 }
1850
1851 // build room-reservation object
1852 $room_res_obj = new stdClass;
1853 $room_res_obj->idorder = $res_id;
1854 $room_res_obj->idroom = $room_rplan['idroom'];
1855 $room_res_obj->adults = $this->getPartyGuests('adults', 0);
1856 $room_res_obj->children = $this->getPartyGuests('children', 0);
1857 if (!empty($room_rplan['idprice'])) {
1858 $room_res_obj->idtar = $this->getTariffId($room_rplan['idroom'], $room_rplan['idprice'], $res_obj->days);
1859 }
1860 $room_res_obj->optionals = $room_options;
1861 $room_res_obj->t_first_name = $customer->name;
1862 $room_res_obj->t_last_name = $customer->lname;
1863
1864 // store record
1865 if (!$dbo->insertObject('#__vikbooking_ordersrooms', $room_res_obj, 'id')) {
1866 // could not store the room-reservation record
1867 return $res_id;
1868 }
1869
1870 // in case of suggestions for alternative room parties, parse the extra rooms
1871 $party_index = 1;
1872 foreach ($extra_rooms as $extra_room) {
1873 if (!is_array($extra_room) || empty($extra_room['idroom'])) {
1874 continue;
1875 }
1876 // build additional room-reservation object
1877 $room_res_obj = new stdClass;
1878 $room_res_obj->idorder = $res_id;
1879 $room_res_obj->idroom = $extra_room['idroom'];
1880 $room_res_obj->adults = $this->getPartyGuests('adults', $party_index);
1881 $room_res_obj->children = $this->getPartyGuests('children', $party_index);
1882
1883 // store record
1884 $dbo->insertObject('#__vikbooking_ordersrooms', $room_res_obj, 'id');
1885
1886 // increase room party index
1887 $party_index++;
1888 }
1889
1890 // return the newly created reservation ID
1891 return $res_id;
1892 }
1893
1894 /**
1895 * Attempts to get the tariff ID for the given room, rate plan and nights.
1896 *
1897 * @param int $room_id the ID of the VBO room.
1898 * @param int $rplan_id the rate plan ID in VBO.
1899 * @param int $nights the number of nights of stay.
1900 *
1901 * @return int|null the tariff ID or null.
1902 */
1903 public function getTariffId($room_id, $rplan_id, $nights)
1904 {
1905 if (empty($room_id) || empty($rplan_id) || $nights < 1) {
1906 return null;
1907 }
1908
1909 $dbo = JFactory::getDbo();
1910
1911 $q = "SELECT `id` FROM `#__vikbooking_dispcost` WHERE `idroom`={$room_id} AND `days`={$nights} AND `idprice`={$rplan_id}";
1912 $dbo->setQuery($q, 0, 1);
1913 $res = $dbo->loadResult();
1914
1915 if ($res) {
1916 return (int)$res;
1917 }
1918
1919 return null;
1920 }
1921
1922 /**
1923 * Grabs the details of a given booking id.
1924 *
1925 * @param int $bid the booking ID to look for.
1926 *
1927 * @return array empty array or booking record details.
1928 */
1929 public function getBookingDetails($bid)
1930 {
1931 $bid = (int)$bid;
1932
1933 $dbo = JFactory::getDbo();
1934
1935 $q = "SELECT `o`.*, `co`.`idcustomer`, CONCAT_WS(' ', `c`.`first_name`, `c`.`last_name`) AS `customer_fullname`, `c`.`country` AS `customer_country`, `c`.`pic`
1936 FROM `#__vikbooking_orders` AS `o`
1937 LEFT JOIN `#__vikbooking_customers_orders` AS `co` ON `co`.`idorder`=`o`.`id`
1938 LEFT JOIN `#__vikbooking_customers` AS `c` ON `c`.`id`=`co`.`idcustomer`
1939 WHERE `o`.`id`={$bid}";
1940 $dbo->setQuery($q, 0, 1);
1941 $row = $dbo->loadAssoc();
1942 if ($row) {
1943 return $row;
1944 }
1945
1946 return [];
1947 }
1948
1949 /**
1950 * Validates if the room allows the given number of nights of stay by checking if a
1951 * tariff is defined for the given length of stay. Useful for particular rate tables.
1952 *
1953 * @param int $room_id the ID of the VBO room.
1954 * @param int $nights the number of nights of stay.
1955 *
1956 * @return bool true if a tariff is found between min and max nights.
1957 *
1958 * @since 1.16.3 (J) - 1.6.3 (WP)
1959 */
1960 public function roomNightsAllowed($room_id, $nights)
1961 {
1962 if (!isset($this->min_max_los_tariffs_map[$room_id])) {
1963 $dbo = JFactory::getDbo();
1964
1965 $q = $dbo->getQuery(true)
1966 ->select('MIN(' . $dbo->qn('t.days') . ') AS ' . $dbo->qn('min_nights'))
1967 ->select('MAX(' . $dbo->qn('t.days') . ') AS ' . $dbo->qn('max_nights'))
1968 ->from($dbo->qn('#__vikbooking_dispcost', 't'))
1969 ->where($dbo->qn('t.idroom') . ' = ' . (int)$room_id);
1970
1971 $dbo->setQuery($q, 0, 1);
1972 $tariffs = $dbo->loadObject();
1973
1974 if (!$tariffs || !$tariffs->min_nights || !$tariffs->max_nights) {
1975 return false;
1976 }
1977
1978 // set values
1979 $this->min_max_los_tariffs_map[$room_id] = [$tariffs->min_nights, $tariffs->max_nights];
1980 }
1981
1982 // check if the number of nights of stay is within the range of tariffs los map
1983 return ($nights >= min($this->min_max_los_tariffs_map[$room_id]) && $nights <= max($this->min_max_los_tariffs_map[$room_id]));
1984 }
1985
1986 /**
1987 * Sets the stay dates, check-in and check-out date timestamps.
1988 *
1989 * @param string $from check-in date string in Y-m-d or VBO format.
1990 * @param string $to check-out date string in Y-m-d or VBO format.
1991 *
1992 * @return self
1993 */
1994 public function setStayDates($from, $to)
1995 {
1996 if (empty($from) || empty($to)) {
1997 return $this;
1998 }
1999
2000 $checkinh = 0;
2001 $checkinm = 0;
2002 $checkouth = 0;
2003 $checkoutm = 0;
2004 $timeopst = VikBooking::getTimeOpenStore();
2005 if (is_array($timeopst)) {
2006 if ($timeopst[0] < $timeopst[1]) {
2007 // check-in not allowed on a day where there is already a check out (no arrivals/depatures on the same day)
2008 $this->inonout_allowed = false;
2009 }
2010 $opent = VikBooking::getHoursMinutes($timeopst[0]);
2011 $closet = VikBooking::getHoursMinutes($timeopst[1]);
2012 $checkinh = $opent[0];
2013 $checkinm = $opent[1];
2014 $checkouth = $closet[0];
2015 $checkoutm = $closet[1];
2016 }
2017 $from_ts = VikBooking::getDateTimestamp($from, $checkinh, $checkinm);
2018 $to_ts = VikBooking::getDateTimestamp($to, $checkouth, $checkoutm);
2019
2020 // set stay dates and timestamps
2021 $this->stay_dates = [date('Y-m-d', $from_ts), date('Y-m-d', $to_ts)];
2022 $this->stay_ts = [$from_ts, $to_ts];
2023
2024 return $this;
2025 }
2026
2027 /**
2028 * Returns the current stay date or timestamps.
2029 *
2030 * @param bool $ts whether to get the date timestamps.
2031 *
2032 * @return array the current stay date timestamps.
2033 */
2034 public function getStayDates($ts = false)
2035 {
2036 return $ts ? $this->stay_ts : $this->stay_dates;
2037 }
2038
2039 /**
2040 * Sets a room party with adults and children, by optionally replacing the others.
2041 *
2042 * @param int $adults the number of adults for this room party.
2043 * @param int $children the number of children for this room party.
2044 * @param bool $replace if true, any previously set room party will be replaced.
2045 *
2046 * @return self
2047 */
2048 public function setRoomParty($adults, $children = 0, $replace = false)
2049 {
2050 $room_party = [
2051 'adults' => $adults,
2052 'children' => $children,
2053 ];
2054
2055 if ($replace) {
2056 $this->room_parties = [$room_party];
2057 } else {
2058 array_push($this->room_parties, $room_party);
2059 }
2060
2061 return $this;
2062 }
2063
2064 /**
2065 * Returns the current room parties array.
2066 *
2067 * @return array the current room parties.
2068 */
2069 public function getRoomParties()
2070 {
2071 return $this->room_parties;
2072 }
2073
2074 /**
2075 * Sets and returns the flag to ignore the restrictions.
2076 *
2077 * @param bool $set the boolean status to set.
2078 *
2079 * @return bool the current ignore status.
2080 */
2081 public function ignoreRestrictions($set = null)
2082 {
2083 if (is_bool($set)) {
2084 $this->ignore_restrictions = $set;
2085 }
2086
2087 return $this->ignore_restrictions;
2088 }
2089
2090 /**
2091 * Sets and returns the flag to ignore the rooms availability.
2092 *
2093 * @param bool $set the boolean status to set.
2094 *
2095 * @return bool the current ignore status.
2096 *
2097 * @since 1.16.10 (J) - 1.6.10 (WP)
2098 */
2099 public function ignoreAvailability($set = null)
2100 {
2101 if (is_bool($set)) {
2102 $this->ignore_availability = $set;
2103 }
2104
2105 return $this->ignore_availability;
2106 }
2107
2108 /**
2109 * Takes the first payment ID "string", if available.
2110 *
2111 * @return string|null
2112 */
2113 protected function getDefaultPaymentId()
2114 {
2115 $dbo = JFactory::getDbo();
2116
2117 $q = "SELECT `id`, `name` FROM `#__vikbooking_gpayments` WHERE `published`=1 ORDER BY `ordering` ASC";
2118 $dbo->setQuery($q, 0, 1);
2119 $data = $dbo->loadAssoc();
2120
2121 if ($data) {
2122 return $data['id'] . '=' . $data['name'];
2123 }
2124
2125 return null;
2126 }
2127
2128 /**
2129 * Returns the current nights/transfers ratio for split stays.
2130 *
2131 * @return int the nights transfers ratio for the percent calculation.
2132 *
2133 * @since 1.16.0 (J) - 1.6.0 (WP)
2134 */
2135 public function getNightsTransfersRatio()
2136 {
2137 return $this->nights_transfers_ratio;
2138 }
2139
2140 /**
2141 * Returns the default nights/transfers ratio for split stays.
2142 *
2143 * @return int the nights transfers ratio defined in the configuration.
2144 *
2145 * @since 1.16.0 (J) - 1.6.0 (WP)
2146 */
2147 public function getDefaultNightsTransfersRatio()
2148 {
2149 $config = VBOFactory::getConfig();
2150
2151 return (int)$config->get('split_stay_ratio', 50);
2152 }
2153
2154 /**
2155 * Sets the nights/transfers ratio for split stays. Use it to start applying limits.
2156 *
2157 * @param mixed $ratio integer value, or the default config setting will be applied.
2158 *
2159 * @return self
2160 *
2161 * @since 1.16.0 (J) - 1.6.0 (WP)
2162 */
2163 public function setNightsTransfersRatio($ratio = null)
2164 {
2165 if (is_int($ratio)) {
2166 $this->nights_transfers_ratio = $ratio;
2167 } else {
2168 $this->nights_transfers_ratio = $this->getDefaultNightsTransfersRatio();
2169 }
2170
2171 return $this;
2172 }
2173
2174 /**
2175 * Tells whether we need to behave for the front-end booking process.
2176 *
2177 * @return bool true if we are in the front-end booking process or false.
2178 *
2179 * @since 1.16.0 (J) - 1.6.0 (WP)
2180 */
2181 public function isFrontBooking()
2182 {
2183 return (bool)$this->is_front_booking;
2184 }
2185
2186 /**
2187 * Toggles the flag to behave for the front-end booking process.
2188 *
2189 * @return self
2190 *
2191 * @since 1.16.0 (J) - 1.6.0 (WP)
2192 */
2193 public function setIsFrontBooking($is_front = true)
2194 {
2195 $this->is_front_booking = (bool)$is_front;
2196
2197 return $this;
2198 }
2199
2200 /**
2201 * This helper method aims to collect the stay dates of each room in
2202 * a booking with split stay. Records will be loaded by ID ascending,
2203 * so in the same exact way as the rooms get stored. For this reason,
2204 * it is then possible to match the stay dates of a room by array-key,
2205 * even if bookings with split stay should always have different room IDs.
2206 *
2207 * @param int $bid the website reservation ID.
2208 *
2209 * @return array the list of busy records with stay dates information.
2210 *
2211 * @since 1.16.0 (J) - 1.6.0 (WP)
2212 */
2213 public function loadSplitStayBusyRecords($bid)
2214 {
2215 $dbo = JFactory::getDbo();
2216
2217 $bid = (int)$bid;
2218
2219 /**
2220 * It is fundamental to keep the ID column as the busy record ID
2221 * to allow the room switching in case of booking modification.
2222 */
2223 $q = "SELECT `ob`.`idorder`, `b`.`id`, `b`.`idroom`, `b`.`checkin`, `b`.`checkout`, `b`.`sharedcal`
2224 FROM `#__vikbooking_ordersbusy` AS `ob`
2225 LEFT JOIN `#__vikbooking_busy` AS `b` ON `ob`.`idbusy`=`b`.`id`
2226 WHERE `ob`.`idorder`={$bid}
2227 ORDER BY `b`.`sharedcal` ASC, `b`.`id` ASC;";
2228 $dbo->setQuery($q);
2229 $records = $dbo->loadAssocList();
2230
2231 if (!$records) {
2232 // the booking may no longer exist, or maybe it was cancelled
2233 return [];
2234 }
2235
2236 return $records;
2237 }
2238
2239 /**
2240 * Returns a list of the tax rate records.
2241 *
2242 * @return array the list of tax rates.
2243 *
2244 * @since 1.16.0 (J) - 1.6.0 (WP)
2245 */
2246 public function getTaxRates()
2247 {
2248 $dbo = JFactory::getDbo();
2249 $dbo->setQuery("SELECT * FROM `#__vikbooking_iva`;");
2250
2251 return $dbo->loadAssocList();
2252 }
2253
2254 /**
2255 * Sets the IDs of the rooms to filter or use.
2256 *
2257 * @param mixed $room_ids the list of room IDs to filter or use, or int room ID.
2258 *
2259 * @return self
2260 */
2261 public function setRoomIds($room_ids = [])
2262 {
2263 if (is_scalar($room_ids)) {
2264 // single room ID integer
2265 $room_ids = [$room_ids];
2266 }
2267
2268 $this->room_ids = $room_ids;
2269
2270 return $this;
2271 }
2272
2273 /**
2274 * Returns the current room ids to filter or use.
2275 *
2276 * @return array the current room ids.
2277 */
2278 public function getRoomIds()
2279 {
2280 return $this->room_ids;
2281 }
2282
2283 /**
2284 * In case of no availability, overrides the default number of days to check
2285 * prior and after the originally requested check-in and check-out dates.
2286 *
2287 * @param int $days the total number of days to use back and forth.
2288 *
2289 * @return self
2290 */
2291 public function setBackForthDays($days)
2292 {
2293 if (is_int($days) && $days >= 0) {
2294 $this->back_and_forth = $days;
2295 }
2296
2297 return $this;
2298 }
2299
2300 /**
2301 * Returns the current number of back and forth days.
2302 *
2303 * @return int the current number of days.
2304 */
2305 public function getBackForthDays()
2306 {
2307 return $this->back_and_forth;
2308 }
2309
2310 /**
2311 * Sets warning messages by concatenating the existing ones.
2312 *
2313 * @param string $str
2314 *
2315 * @return self
2316 */
2317 protected function setWarning($str)
2318 {
2319 $this->warning .= $str . "\n";
2320
2321 return $this;
2322 }
2323
2324 /**
2325 * Gets the current warning string.
2326 *
2327 * @return string
2328 */
2329 public function getWarning()
2330 {
2331 return rtrim($this->warning, "\n");
2332 }
2333
2334 /**
2335 * Sets errors by concatenating the existing ones.
2336 *
2337 * @param string $str
2338 *
2339 * @return self
2340 */
2341 protected function setError($str)
2342 {
2343 $this->error .= $str . "\n";
2344
2345 return $this;
2346 }
2347
2348 /**
2349 * Gets the current error string.
2350 *
2351 * @return string
2352 */
2353 public function getError()
2354 {
2355 return rtrim($this->error, "\n");
2356 }
2357
2358 /**
2359 * Gets the current error code.
2360 *
2361 * @return int
2362 */
2363 public function getErrorCode()
2364 {
2365 return $this->errorCode;
2366 }
2367 }
2368