| @@ -154,8 +154,15 @@ | ||
| 154 | 154 | // load dependencies |
| 155 | 155 | if (!class_exists('TACVBO')) { |
| 156 | 156 | require_once VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'tac.vikbooking.php'; |
| 157 | 157 | } |
| 158 | + | |
| 159 | + /** | |
| 160 | + * Set default nights transfer ratio. | |
| 161 | + * | |
| 162 | + * @since 1.18.5 (J) - 1.8.5 (WP) | |
| 163 | + */ | |
| 164 | + $this->setNightsTransfersRatio(); | |
| 158 | 165 | } |
| 159 | 166 | |
| 160 | 167 | /** |
| 161 | 168 | * Returns the global object, either a new instance or the existing instance |
| @@ -270,24 +277,98 @@ | ||
| 270 | 277 | return 'No rooms available due to room or rate plan restrictions.'; |
| 271 | 278 | default: |
| 272 | 279 | return 'Unknown error code.'; |
| 273 | 280 | } |
| 274 | - | |
| 275 | 281 | } |
| 276 | 282 | |
| 277 | 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 | + /** | |
| 278 | 356 | * Loads all rooms in VBO and maps them into an associative array. |
| 279 | 357 | * |
| 280 | 358 | * @param array $ids optional list of IDs to load. |
| 281 | 359 | * @param int $max optional rooms limit to fetch. |
| 360 | + * @param bool $anew true to avoid object caching. | |
| 282 | 361 | * |
| 283 | 362 | * @return array the associative list of rooms. |
| 284 | 363 | * |
| 285 | 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. | |
| 286 | 367 | */ |
| 287 | - public function loadRooms(array $ids = [], $max = 0) | |
| 368 | + public function loadRooms(array $ids = [], $max = 0, $anew = false) | |
| 288 | 369 | { |
| 289 | - if ($this->all_rooms) { | |
| 370 | + if ($this->all_rooms && !$anew) { | |
| 290 | 371 | // return previously cached array if available |
| 291 | 372 | return $this->all_rooms; |
| 292 | 373 | } |
| 293 | 374 | |
| @@ -310,9 +391,27 @@ | ||
| 310 | 391 | ]) |
| 311 | 392 | ->from($dbo->qn('#__vikbooking_rooms')); |
| 312 | 393 | |
| 313 | 394 | if ($ids) { |
| 395 | + // cast all IDs to signed integers | |
| 314 | 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 | |
| 315 | 414 | $q->where($dbo->qn('id') . ' IN (' . implode(', ', $ids) . ')'); |
| 316 | 415 | } |
| 317 | 416 | |
| 318 | 417 | $q->order($dbo->qn('avail') . ' DESC'); |
| @@ -321,9 +420,9 @@ | ||
| 321 | 420 | $dbo->setQuery($q, 0, $max); |
| 322 | 421 | $room_rows = $dbo->loadAssocList(); |
| 323 | 422 | |
| 324 | 423 | if (!$room_rows) { |
| 325 | - return $this->all_rooms; | |
| 424 | + return $anew ? [] : $this->all_rooms; | |
| 326 | 425 | } |
| 327 | 426 | |
| 328 | 427 | if ($this->isFrontBooking()) { |
| 329 | 428 | // apply translations on rooms |
| @@ -330,13 +429,19 @@ | ||
| 330 | 429 | $vbo_tn = VikBooking::getTranslator(); |
| 331 | 430 | $vbo_tn->translateContents($room_rows, '#__vikbooking_rooms'); |
| 332 | 431 | } |
| 333 | 432 | |
| 433 | + $assoc_rooms = []; | |
| 334 | 434 | foreach ($room_rows as $room) { |
| 335 | - $this->all_rooms[$room['id']] = $room; | |
| 435 | + $assoc_rooms[$room['id']] = $room; | |
| 336 | 436 | } |
| 337 | 437 | |
| 338 | - return $this->all_rooms; | |
| 438 | + if (!$anew) { | |
| 439 | + // cache room records | |
| 440 | + $this->all_rooms = $assoc_rooms; | |
| 441 | + } | |
| 442 | + | |
| 443 | + return $anew ? $assoc_rooms : $this->all_rooms; | |
| 339 | 444 | } |
| 340 | 445 | |
| 341 | 446 | /** |
| 342 | 447 | * Sets the current rooms as an associative array of information. The |
| @@ -371,17 +476,19 @@ | ||
| 371 | 476 | return $rooms; |
| 372 | 477 | } |
| 373 | 478 | |
| 374 | 479 | /** |
| 375 | - * Finds a room by name. | |
| 480 | + * Finds a room (or multiple rooms) by name. | |
| 376 | 481 | * |
| 377 | 482 | * @param string $name The room name to look for. |
| 483 | + * @param bool $all True to return a list of matches. | |
| 378 | 484 | * |
| 379 | 485 | * @return array |
| 380 | 486 | * |
| 381 | 487 | * @since 1.16.10 (J) - 1.6.10 (WP) |
| 488 | + * @since 1.18.17 (J) - 1.8.17 (WP) added argument $all. | |
| 382 | 489 | */ |
| 383 | - public function getRoomByName(string $name) | |
| 490 | + public function getRoomByName(string $name, bool $all = false) | |
| 384 | 491 | { |
| 385 | 492 | $dbo = JFactory::getDbo(); |
| 386 | 493 | |
| 387 | 494 | $q = $dbo->getQuery(true) |
| @@ -389,12 +496,18 @@ | ||
| 389 | 496 | ->from($dbo->qn('#__vikbooking_rooms')) |
| 390 | 497 | ->order($dbo->qn('avail') . ' DESC') |
| 391 | 498 | ->order($dbo->qn('name') . ' ASC'); |
| 392 | 499 | |
| 393 | - foreach (array_filter(explode(' ', $name)) as $nm_part) { | |
| 500 | + foreach (array_filter(preg_split("/[\s\-_.,]+/", $name)) as $nm_part) { | |
| 394 | 501 | $q->where($dbo->qn('name') . ' LIKE ' . $dbo->q("%{$nm_part}%")); |
| 395 | 502 | } |
| 396 | 503 | |
| 504 | + if ($all) { | |
| 505 | + // match all records | |
| 506 | + $dbo->setQuery($q); | |
| 507 | + return $dbo->loadAssocList(); | |
| 508 | + } | |
| 509 | + | |
| 397 | 510 | $dbo->setQuery($q, 0, 1); |
| 398 | 511 | $record = $dbo->loadAssoc(); |
| 399 | 512 | |
| 400 | 513 | return $record ?? []; |
| @@ -402,13 +515,17 @@ | ||
| 402 | 515 | |
| 403 | 516 | /** |
| 404 | 517 | * Loads all rate plans in VBO and maps them into an associative array. |
| 405 | 518 | * |
| 519 | + * @param bool $no_cache True to avoid internal caching. | |
| 520 | + * | |
| 406 | 521 | * @return array the associative list of rate plans. |
| 522 | + * | |
| 523 | + * @since 1.17.6 (J) - 1.7.6 (WP) added $no_cache argument. | |
| 407 | 524 | */ |
| 408 | - public function loadRatePlans() | |
| 525 | + public function loadRatePlans($no_cache = false) | |
| 409 | 526 | { |
| 410 | - if ($this->all_rplans) { | |
| 527 | + if (!$no_cache && $this->all_rplans) { | |
| 411 | 528 | // return previously cached array if available |
| 412 | 529 | return $this->all_rplans; |
| 413 | 530 | } |
| 414 | 531 | |
| @@ -442,9 +559,11 @@ | ||
| 442 | 559 | |
| 443 | 560 | // sort rate plans |
| 444 | 561 | $rate_plans = VikBooking::sortRatePlans($rate_plans, true); |
| 445 | 562 | |
| 446 | - $this->all_rplans = $rate_plans; | |
| 563 | + if (!$no_cache) { | |
| 564 | + $this->all_rplans = $rate_plans; | |
| 565 | + } | |
| 447 | 566 | |
| 448 | 567 | return $rate_plans; |
| 449 | 568 | } |
| 450 | 569 | |
| @@ -451,18 +570,20 @@ | ||
| 451 | 570 | /** |
| 452 | 571 | * Returns a list of rate plans derived from the given parent rate plan ID. |
| 453 | 572 | * |
| 454 | 573 | * @param int $parent_rate_id The parent rate plan ID. |
| 574 | + * @param ?array $all_rate_plans Optional cached rate plans list. | |
| 455 | 575 | * |
| 456 | 576 | * @return array |
| 457 | 577 | * |
| 458 | 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. | |
| 459 | 580 | */ |
| 460 | - public function getDerivedRatePlans(int $parent_rate_id) | |
| 581 | + public function getDerivedRatePlans(int $parent_rate_id, ?array $all_rate_plans = null) | |
| 461 | 582 | { |
| 462 | 583 | $derived_rplans = []; |
| 463 | 584 | |
| 464 | - foreach ($this->loadRatePlans() as $rp_id => $rplan) { | |
| 585 | + foreach (($all_rate_plans ?: $this->loadRatePlans()) as $rp_id => $rplan) { | |
| 465 | 586 | if ($rplan['derived_id'] && $rplan['derived_id'] == $parent_rate_id && $rplan['derived_data']) { |
| 466 | 587 | // this rate plan is derived from the given parent rate plan |
| 467 | 588 | $derived_rplans[] = $rplan; |
| 468 | 589 | } |
| @@ -471,8 +592,226 @@ | ||
| 471 | 592 | return $derived_rplans; |
| 472 | 593 | } |
| 473 | 594 | |
| 474 | 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 | + /** | |
| 475 | 814 | * Returns the inventory for the rooms and dates set. |
| 476 | 815 | * |
| 477 | 816 | * @param bool $restrictions Whether to include booking restrictions data. |
| 478 | 817 | * |
| @@ -659,9 +998,9 @@ | ||
| 659 | 998 | return false; |
| 660 | 999 | } |
| 661 | 1000 | |
| 662 | 1001 | // count injected rooms, if any |
| 663 | - $tot_rooms = count($this->room_ids); | |
| 1002 | + $tot_rooms = (int) (($params['rooms_count'] ?? 0) ?: count($this->room_ids)); | |
| 664 | 1003 | |
| 665 | 1004 | // build options array for TACVBO |
| 666 | 1005 | $options = [ |
| 667 | 1006 | 'hash' => md5('vbo.e4j.vbo'), |
| @@ -738,9 +1077,9 @@ | ||
| 738 | 1077 | } |
| 739 | 1078 | |
| 740 | 1079 | // optional filter by room IDs will be applied on this flow |
| 741 | 1080 | $found_rids = array_keys($website_rates); |
| 742 | - $unwanted_rids = $tot_rooms ? array_diff($found_rids, $this->room_ids) : []; | |
| 1081 | + $unwanted_rids = $this->room_ids ? array_diff($found_rids, $this->room_ids) : []; | |
| 743 | 1082 | foreach ($unwanted_rids as $rid) { |
| 744 | 1083 | unset($website_rates[$rid]); |
| 745 | 1084 | } |
| 746 | 1085 | |
| @@ -747,8 +1086,20 @@ | ||
| 747 | 1086 | return $website_rates; |
| 748 | 1087 | } |
| 749 | 1088 | |
| 750 | 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 | + /** | |
| 751 | 1102 | * Finds the available suggestions in case of no availability previously occurred |
| 752 | 1103 | * while getting the rates. This method should be called after getRates() so that |
| 753 | 1104 | * a valid errorCode to be analized will be available, unless code is forced. |
| 754 | 1105 | * Suggestions can include closest booking dates and/or different room-guest parties. |
| @@ -807,15 +1158,15 @@ | ||
| 807 | 1158 | * we build a list of available solutions for booking split stays on the same dates. The |
| 808 | 1159 | * visibility should be public so that other Views could use just this method. |
| 809 | 1160 | * |
| 810 | 1161 | * @param array $room_ids list of unavailable, yet compatible, room IDs. |
| 811 | - * @param array $busy_list optional list of busy records for the involved dates. | |
| 1162 | + * @param ?array $busy_list optional list of busy records for the involved dates. | |
| 812 | 1163 | * |
| 813 | 1164 | * @return array associative list of available split stays, or empty array. |
| 814 | 1165 | * |
| 815 | 1166 | * @since 1.16.0 (J) - 1.6.0 (WP) |
| 816 | 1167 | */ |
| 817 | - public function findSplitStays(array $room_ids = [], array $busy_list = null) | |
| 1168 | + public function findSplitStays(array $room_ids = [], ?array $busy_list = null) | |
| 818 | 1169 | { |
| 819 | 1170 | if (!$room_ids) { |
| 820 | 1171 | return []; |
| 821 | 1172 | } |
| @@ -916,15 +1267,15 @@ | ||
| 916 | 1267 | } |
| 917 | 1268 | } |
| 918 | 1269 | } |
| 919 | 1270 | |
| 920 | - if (!count($avroom_nights)) { | |
| 1271 | + if (!$avroom_nights) { | |
| 921 | 1272 | // no rooms available at all, there's no way to do anything |
| 922 | 1273 | return []; |
| 923 | 1274 | } |
| 924 | 1275 | |
| 925 | 1276 | // make sure all nights requested can be satisfied by at least one room |
| 926 | - foreach ($groupdays as $gday) { | |
| 1277 | + foreach ($groupdays as $gday_index => $gday) { | |
| 927 | 1278 | $day_key = date('Y-m-d', $gday); |
| 928 | 1279 | $day_av = false; |
| 929 | 1280 | foreach ($avroom_nights as $rid => $av_nights) { |
| 930 | 1281 | if (in_array($day_key, $av_nights)) { |
| @@ -932,10 +1283,11 @@ | ||
| 932 | 1283 | $day_av = true; |
| 933 | 1284 | break; |
| 934 | 1285 | } |
| 935 | 1286 | } |
| 936 | - if (!$day_av) { | |
| 937 | - // this night is not available in any room, unable to proceed | |
| 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 | |
| 938 | 1290 | return []; |
| 939 | 1291 | } |
| 940 | 1292 | } |
| 941 | 1293 | |
| @@ -1133,9 +1485,9 @@ | ||
| 1133 | 1485 | } |
| 1134 | 1486 | |
| 1135 | 1487 | // apply nights/transfers ratio limit (unless disabled) |
| 1136 | 1488 | $nights_transfers_ratio = $this->getNightsTransfersRatio(); |
| 1137 | - if ($nights_transfers_ratio > 0 && $nights_transfers_ratio < 100) { | |
| 1489 | + if ($nights_transfers_ratio > 0 && $nights_transfers_ratio <= 100) { | |
| 1138 | 1490 | // count and apply limits |
| 1139 | 1491 | foreach ($split_stay_sols as $k => $split_stay_sol) { |
| 1140 | 1492 | // count nights and transfers |
| 1141 | 1493 | $split_stay_transfers = count($split_stay_sol) - 1; |
| @@ -1150,11 +1502,14 @@ | ||
| 1150 | 1502 | unset($split_stay_sols[$k]); |
| 1151 | 1503 | $altered_sols = true; |
| 1152 | 1504 | } |
| 1153 | 1505 | } |
| 1506 | + } else { | |
| 1507 | + // split stays have been disabled | |
| 1508 | + $split_stay_sols = []; | |
| 1154 | 1509 | } |
| 1155 | 1510 | |
| 1156 | - if ($altered_sols && count($split_stay_sols)) { | |
| 1511 | + if ($altered_sols && $split_stay_sols) { | |
| 1157 | 1512 | // restore the array keys |
| 1158 | 1513 | $split_stay_sols = array_values($split_stay_sols); |
| 1159 | 1514 | } |
| 1160 | 1515 | |
| @@ -2025,13 +2380,13 @@ | ||
| 2025 | 2380 | return $this; |
| 2026 | 2381 | } |
| 2027 | 2382 | |
| 2028 | 2383 | /** |
| 2029 | - * Returns the current stay date or timestamps. | |
| 2384 | + * Returns the current stay dates or timestamps. | |
| 2030 | 2385 | * |
| 2031 | 2386 | * @param bool $ts whether to get the date timestamps. |
| 2032 | 2387 | * |
| 2033 | - * @return array the current stay date timestamps. | |
| 2388 | + * @return array the current stay dates or timestamps. | |
| 2034 | 2389 | */ |
| 2035 | 2390 | public function getStayDates($ts = false) |
| 2036 | 2391 | { |
| 2037 | 2392 | return $ts ? $this->stay_ts : $this->stay_dates; |
| @@ -2146,23 +2501,28 @@ | ||
| 2146 | 2501 | * @since 1.16.0 (J) - 1.6.0 (WP) |
| 2147 | 2502 | */ |
| 2148 | 2503 | public function getDefaultNightsTransfersRatio() |
| 2149 | 2504 | { |
| 2150 | - $config = VBOFactory::getConfig(); | |
| 2505 | + static $transferRatio = null; | |
| 2151 | 2506 | |
| 2152 | - return (int)$config->get('split_stay_ratio', 50); | |
| 2507 | + if ($transferRatio === null) { | |
| 2508 | + $config = VBOFactory::getConfig(); | |
| 2509 | + $transferRatio = (int) $config->get('split_stay_ratio', 50); | |
| 2510 | + } | |
| 2511 | + | |
| 2512 | + return $transferRatio; | |
| 2153 | 2513 | } |
| 2154 | 2514 | |
| 2155 | 2515 | /** |
| 2156 | 2516 | * Sets the nights/transfers ratio for split stays. Use it to start applying limits. |
| 2157 | 2517 | * |
| 2158 | - * @param mixed $ratio integer value, or the default config setting will be applied. | |
| 2518 | + * @param ?int $ratio Optional ratio integer value, or default value will apply. | |
| 2159 | 2519 | * |
| 2160 | 2520 | * @return self |
| 2161 | 2521 | * |
| 2162 | 2522 | * @since 1.16.0 (J) - 1.6.0 (WP) |
| 2163 | 2523 | */ |
| 2164 | - public function setNightsTransfersRatio($ratio = null) | |
| 2524 | + public function setNightsTransfersRatio(?int $ratio = null) | |
| 2165 | 2525 | { |
| 2166 | 2526 | if (is_int($ratio)) { |
| 2167 | 2527 | $this->nights_transfers_ratio = $ratio; |
| 2168 | 2528 | } else { |
| @@ -2180,9 +2540,9 @@ | ||
| 2180 | 2540 | * @since 1.16.0 (J) - 1.6.0 (WP) |
| 2181 | 2541 | */ |
| 2182 | 2542 | public function isFrontBooking() |
| 2183 | 2543 | { |
| 2184 | - return (bool)$this->is_front_booking; | |
| 2544 | + return (bool) $this->is_front_booking; | |
| 2185 | 2545 | } |
| 2186 | 2546 | |
| 2187 | 2547 | /** |
| 2188 | 2548 | * Toggles the flag to behave for the front-end booking process. |