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