| @@ -13,1587 +13,1966 @@ | ||
| 13 | 13 | |
| 14 | 14 | /** |
| 15 | 15 | * VikBooking model pricing. |
| 16 | 16 | * |
| 17 | - * @since 1.16.10 (J) - 1.6.10 (WP) | |
| 17 | + * @since 1.16.10 (J) - 1.6.10 (WP) | |
| 18 | 18 | */ |
| 19 | 19 | class VBOModelPricing extends JObject |
| 20 | 20 | { |
| 21 | - /** @var array */ | |
| 22 | - protected $room_rate_plans = []; | |
| 21 | + /** @var array */ | |
| 22 | + protected $room_rate_plans = []; | |
| 23 | 23 | |
| 24 | - /** @var array */ | |
| 25 | - protected $channels_updated_list = []; | |
| 24 | + /** @var array */ | |
| 25 | + protected $channels_updated_list = []; | |
| 26 | 26 | |
| 27 | - /** @var array */ | |
| 28 | - protected $channel_warnings = []; | |
| 27 | + /** @var array */ | |
| 28 | + protected $channel_warnings = []; | |
| 29 | 29 | |
| 30 | - /** @var array */ | |
| 31 | - protected $channel_errors = []; | |
| 30 | + /** @var array */ | |
| 31 | + protected $channel_errors = []; | |
| 32 | 32 | |
| 33 | - /** @var array */ | |
| 34 | - protected $cached_restrictions = []; | |
| 33 | + /** @var array */ | |
| 34 | + protected $cached_restrictions = []; | |
| 35 | 35 | |
| 36 | - /** | |
| 37 | - * Proxy for immediately accessing the object and bind data. | |
| 38 | - * | |
| 39 | - * @param array|object $data optional data to bind. | |
| 40 | - * @param boolean $anew true for forcing a new instance. | |
| 41 | - * | |
| 42 | - * @return self | |
| 43 | - */ | |
| 44 | - public static function getInstance($data = []) | |
| 45 | - { | |
| 46 | - return new static($data); | |
| 47 | - } | |
| 36 | + /** @var array */ | |
| 37 | + protected static $cached_all_rate_plans = []; | |
| 48 | 38 | |
| 49 | - /** | |
| 50 | - * Returns the information about rates and restrictions for | |
| 51 | - * a given room in a range of dates. | |
| 52 | - * | |
| 53 | - * @param array $options Options for getting room rates. | |
| 54 | - * | |
| 55 | - * @return array | |
| 56 | - * | |
| 57 | - * @throws Exception | |
| 58 | - */ | |
| 59 | - public function getRoomRates(array $options) | |
| 60 | - { | |
| 61 | - $dbo = JFactory::getDbo(); | |
| 39 | + /** @var array */ | |
| 40 | + protected static $cached_base_rates = []; | |
| 62 | 41 | |
| 63 | - // gather options | |
| 64 | - $from_date = (string) ($options['from_date'] ?? ''); | |
| 65 | - $to_date = (string) ($options['to_date'] ?? ''); | |
| 66 | - $id_room = (int) ($options['id_room'] ?? 0); | |
| 67 | - $id_price = (int) ($options['id_price'] ?? 0); | |
| 68 | - $all_rplans = (bool) ($options['all_rplans'] ?? false); | |
| 69 | - $restrictions = (bool) ($options['restrictions'] ?? true); | |
| 42 | + /** | |
| 43 | + * Proxy for immediately accessing the object and bind data. | |
| 44 | + * | |
| 45 | + * @param array|object $data optional data to bind. | |
| 46 | + * @param boolean $anew true for forcing a new instance. | |
| 47 | + * | |
| 48 | + * @return self | |
| 49 | + */ | |
| 50 | + public static function getInstance($data = []) | |
| 51 | + { | |
| 52 | + return new static($data); | |
| 53 | + } | |
| 70 | 54 | |
| 71 | - if (!$from_date || !$to_date) { | |
| 72 | - // must be in Y-m-d format | |
| 73 | - throw new InvalidArgumentException('Missing dates for applying the new rates or restriction.', 400); | |
| 74 | - } | |
| 55 | + /** | |
| 56 | + * Returns the information about rates and restrictions for | |
| 57 | + * a given room in a range of dates. | |
| 58 | + * | |
| 59 | + * @param array $options Options for getting room rates. | |
| 60 | + * | |
| 61 | + * @return array | |
| 62 | + * | |
| 63 | + * @throws Exception | |
| 64 | + * | |
| 65 | + * @since 1.18.0 (J) - 1.8.0 (WP) added support for multiple room IDs and faster processing. | |
| 66 | + */ | |
| 67 | + public function getRoomRates(array $options) | |
| 68 | + { | |
| 69 | + // gather options | |
| 70 | + $from_date = (string) ($options['from_date'] ?? ''); | |
| 71 | + $to_date = (string) ($options['to_date'] ?? ''); | |
| 72 | + $id_room = (int) ($options['id_room'] ?? 0); | |
| 73 | + $id_price = (int) ($options['id_price'] ?? 0); | |
| 74 | + $all_rplans = (bool) ($options['all_rplans'] ?? false); | |
| 75 | + $restrictions = (bool) ($options['restrictions'] ?? true); | |
| 76 | + $id_rooms = array_values(array_filter(array_map('intval', (array) ($options['id_rooms'] ?? [])))); | |
| 75 | 77 | |
| 76 | - if (JFactory::getDate($to_date) < JFactory::getDate($from_date)) { | |
| 77 | - // invalid dates | |
| 78 | - throw new InvalidArgumentException('Invalid dates received.', 400); | |
| 79 | - } | |
| 78 | + if (!$from_date || !$to_date) { | |
| 79 | + // must be in Y-m-d format | |
| 80 | + throw new InvalidArgumentException('Missing dates for applying the new rates or restriction.', 400); | |
| 81 | + } | |
| 80 | 82 | |
| 81 | - if (!$id_room) { | |
| 82 | - throw new InvalidArgumentException('Room record ID is mandatory.', 400); | |
| 83 | - } | |
| 83 | + if (JFactory::getDate($to_date) < JFactory::getDate($from_date)) { | |
| 84 | + // invalid dates | |
| 85 | + throw new InvalidArgumentException('Invalid dates received.', 400); | |
| 86 | + } | |
| 84 | 87 | |
| 85 | - /** | |
| 86 | - * Disable season records caching because new rates will have to be re-calculated | |
| 87 | - * for the response by checking the same exact dates. | |
| 88 | - */ | |
| 89 | - VikBooking::setSeasonsCache(false); | |
| 88 | + if (!$id_room && !$id_rooms) { | |
| 89 | + throw new InvalidArgumentException('Room record ID is mandatory.', 400); | |
| 90 | + } | |
| 90 | 91 | |
| 91 | - // load check-in and check-out times | |
| 92 | - list($checkin_h, $checkin_m, $checkout_h, $checkout_m) = VBOModelReservation::getInstance()->loadCheckinOutTimes(); | |
| 92 | + // collect the list of room IDs involved | |
| 93 | + $listing_ids = array_values(array_filter(array_merge([$id_room], $id_rooms))); | |
| 93 | 94 | |
| 94 | - // date format | |
| 95 | - $vbo_df = VikBooking::getDateFormat(); | |
| 96 | - $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y/m/d'); | |
| 95 | + // load check-in and check-out times | |
| 96 | + list($checkin_h, $checkin_m, $checkout_h, $checkout_m) = VBOModelReservation::getInstance()->loadCheckinOutTimes(); | |
| 97 | 97 | |
| 98 | - if (!$all_rplans) { | |
| 99 | - // get room rates either from the provided rate plan ID or by fetching the main one | |
| 100 | - if (!$id_price) { | |
| 101 | - // load all rate plans | |
| 102 | - $all_rate_plans = VikBooking::getAvailabilityInstance(true)->loadRatePlans(); | |
| 98 | + // date format | |
| 99 | + $vbo_df = VikBooking::getDateFormat(); | |
| 100 | + $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y/m/d'); | |
| 103 | 101 | |
| 104 | - // use the first (main) rate plan ID after the automatic sorting | |
| 105 | - foreach ($all_rate_plans as $all_rate_plan) { | |
| 106 | - $id_price = $all_rate_plan['id']; | |
| 107 | - break; | |
| 108 | - } | |
| 109 | - } | |
| 102 | + // get room rates either from the provided rate plan ID or by fetching the main one | |
| 103 | + if (!$all_rplans && !$id_price) { | |
| 104 | + // load all rate plans | |
| 105 | + $all_rate_plans = self::$cached_all_rate_plans ?: VikBooking::getAvailabilityInstance(true)->loadRatePlans(); | |
| 106 | + self::$cached_all_rate_plans = $all_rate_plans; | |
| 110 | 107 | |
| 111 | - if (!$id_price) { | |
| 112 | - throw new Exception('No rate plans configured.', 500); | |
| 113 | - } | |
| 108 | + // use the first (main) rate plan ID after the automatic sorting | |
| 109 | + foreach ($all_rate_plans as $all_rate_plan) { | |
| 110 | + $id_price = $all_rate_plan['id']; | |
| 111 | + break; | |
| 112 | + } | |
| 114 | 113 | |
| 115 | - // read the rates for the lowest number of nights for a specific rate plan ID | |
| 116 | - $q = "SELECT `r`.`id`,`r`.`idroom`,`r`.`days`,`r`.`idprice`,`r`.`cost`,`p`.`name` | |
| 117 | - FROM `#__vikbooking_dispcost` AS `r` | |
| 118 | - INNER JOIN ( | |
| 119 | - SELECT MIN(`days`) AS `min_days` | |
| 120 | - FROM `#__vikbooking_dispcost` | |
| 121 | - WHERE `idroom`=" . $id_room . " AND `idprice`=" . $id_price . " | |
| 122 | - GROUP BY `idroom` | |
| 123 | - ) AS `r2` ON `r`.`days`=`r2`.`min_days` | |
| 124 | - LEFT JOIN `#__vikbooking_prices` `p` ON `p`.`id`=`r`.`idprice` AND `p`.`id`=" . $id_price . " | |
| 125 | - WHERE `r`.`idroom`=" . $id_room . " AND `r`.`idprice`=" . $id_price . " | |
| 126 | - GROUP BY `r`.`id`,`r`.`idroom`,`r`.`days`,`r`.`idprice`,`r`.`cost`,`p`.`name` | |
| 127 | - ORDER BY `r`.`days` ASC, `r`.`cost` ASC;"; | |
| 128 | - } else { | |
| 129 | - // get room rates from all rate plans configured for the given room | |
| 130 | - // read the rates for the lowest number of nights for all rate plans | |
| 131 | - $q = "SELECT `r`.`id`,`r`.`idroom`,`r`.`days`,`r`.`idprice`,`r`.`cost`,`p`.`name` | |
| 132 | - FROM `#__vikbooking_dispcost` AS `r` | |
| 133 | - INNER JOIN ( | |
| 134 | - SELECT MIN(`days`) AS `min_days` | |
| 135 | - FROM `#__vikbooking_dispcost` | |
| 136 | - WHERE `idroom`=" . $id_room . " | |
| 137 | - GROUP BY `idroom` | |
| 138 | - ) AS `r2` ON `r`.`days`=`r2`.`min_days` | |
| 139 | - LEFT JOIN `#__vikbooking_prices` `p` ON `p`.`id`=`r`.`idprice` | |
| 140 | - WHERE `r`.`idroom`=" . $id_room . " | |
| 141 | - GROUP BY `r`.`id`,`r`.`idroom`,`r`.`days`,`r`.`idprice`,`r`.`cost`,`p`.`name` | |
| 142 | - ORDER BY `r`.`days` ASC, `r`.`cost` ASC;"; | |
| 143 | - } | |
| 114 | + if (!$id_price) { | |
| 115 | + throw new Exception('No rate plans configured.', 500); | |
| 116 | + } | |
| 117 | + } | |
| 144 | 118 | |
| 145 | - // load room rates from db | |
| 146 | - $dbo->setQuery($q); | |
| 147 | - $roomrates = $dbo->loadAssocList(); | |
| 119 | + // pool of room rates | |
| 120 | + $pool_roomrates = []; | |
| 148 | 121 | |
| 149 | - foreach ($roomrates as $rrk => $rrv) { | |
| 150 | - $roomrates[$rrk]['cost'] = round(($rrv['cost'] / $rrv['days']), 2); | |
| 151 | - $roomrates[$rrk]['days'] = 1; | |
| 152 | - } | |
| 122 | + // iterate over all listing IDs to obtain the respective room rates | |
| 123 | + foreach ($listing_ids as $listing_id) { | |
| 124 | + if (!$all_rplans) { | |
| 125 | + // read the rates for the lowest number of nights for a specific rate plan ID | |
| 126 | + $roomrates = $this->getBaseRoomRates($listing_id, $id_price); | |
| 127 | + } else { | |
| 128 | + // get room rates from all rate plans configured for the given room | |
| 129 | + // read the rates for the lowest number of nights for all rate plans | |
| 130 | + $roomrates = $this->getBaseRoomRates($listing_id); | |
| 131 | + } | |
| 153 | 132 | |
| 154 | - if (!$roomrates) { | |
| 155 | - // terminate the process by throwing an error | |
| 156 | - throw new UnexpectedValueException('No rates found for the given room ID.', 400); | |
| 157 | - } | |
| 133 | + if ($roomrates) { | |
| 134 | + // set room rates to the pool | |
| 135 | + $pool_roomrates[$listing_id] = $roomrates; | |
| 136 | + } | |
| 137 | + } | |
| 158 | 138 | |
| 159 | - // fetch all restrictions, if requested | |
| 160 | - $all_restrictions = $restrictions ? VikBooking::loadRestrictions(true, [$id_room]) : []; | |
| 139 | + if (!$pool_roomrates) { | |
| 140 | + // terminate the process by throwing an error | |
| 141 | + throw new UnexpectedValueException('No rates found for the given listing ID(s).', 400); | |
| 142 | + } | |
| 161 | 143 | |
| 162 | - // calculate global minimum stay | |
| 163 | - $glob_minlos = VikBooking::getDefaultNightsCalendar(); | |
| 164 | - $glob_minlos = $glob_minlos < 1 ? 1 : $glob_minlos; | |
| 144 | + // fetch all restrictions, if requested | |
| 145 | + $all_restrictions = $restrictions ? VikBooking::loadRestrictions(true, $listing_ids) : []; | |
| 165 | 146 | |
| 166 | - // read current room rates | |
| 167 | - $current_rates = []; | |
| 147 | + // calculate global minimum stay | |
| 148 | + $glob_minlos = VikBooking::getDefaultNightsCalendar(); | |
| 149 | + $glob_minlos = $glob_minlos < 1 ? 1 : $glob_minlos; | |
| 168 | 150 | |
| 169 | - // dates involved | |
| 170 | - $start_ts = strtotime($from_date); | |
| 171 | - $end_ts = strtotime($to_date); | |
| 151 | + // dates involved | |
| 152 | + $start_ts = strtotime($from_date); | |
| 153 | + $end_ts = strtotime($to_date); | |
| 172 | 154 | |
| 173 | - /** | |
| 174 | - * Preload seasonal records in favour of CPU usage, but against RAM usage. | |
| 175 | - * | |
| 176 | - * @since 1.17.2 (J) - 1.7.2 (WP) | |
| 177 | - */ | |
| 178 | - $cached_seasons = VikBooking::getDateSeasonRecords($start_ts, ($end_ts + ($checkout_h * 3600)), [$id_room]); | |
| 155 | + // read current room rates | |
| 156 | + $current_rates_pool = []; | |
| 179 | 157 | |
| 180 | - // loop through all the requested range of dates | |
| 181 | - $infostart = getdate($start_ts); | |
| 182 | - while ($infostart[0] > 0 && $infostart[0] <= $end_ts) { | |
| 183 | - // calculate timestamps | |
| 184 | - $tomorrow_ts = mktime(0, 0, 0, $infostart['mon'], ($infostart['mday'] + 1), $infostart['year']); | |
| 185 | - $today_tsin = VikBooking::getDateTimestamp(date($df, $infostart[0]), $checkin_h, $checkin_m); | |
| 186 | - $today_tsout = VikBooking::getDateTimestamp(date($df, $tomorrow_ts), $checkout_h, $checkout_m); | |
| 187 | - $today_mid_ts = mktime(0, 0, 0, $infostart['mon'], $infostart['mday'], $infostart['year']); | |
| 158 | + /** | |
| 159 | + * Preload seasonal records in favour of CPU usage, but against RAM usage. | |
| 160 | + * | |
| 161 | + * @since 1.17.2 (J) - 1.7.2 (WP) | |
| 162 | + * @since 1.18.5 (J) - 1.8.5 (WP) added support for week-day seasons cache. | |
| 163 | + */ | |
| 164 | + $cached_seasons = VikBooking::getDateSeasonRecords($start_ts, ($end_ts + ($checkout_h * 3600)), $listing_ids); | |
| 165 | + $cached_wdayseasons = VikBooking::getWdaySeasonRecords(); | |
| 188 | 166 | |
| 189 | - // calculate tariffs for this day | |
| 190 | - $tars = VikBooking::applySeasonsRoom($roomrates, $today_tsin, $today_tsout, [], $cached_seasons); | |
| 167 | + // loop through all the requested range of dates | |
| 168 | + $infostart = getdate($start_ts); | |
| 169 | + while ($infostart[0] > 0 && $infostart[0] <= $end_ts) { | |
| 170 | + // calculate timestamps | |
| 171 | + $tomorrow_ts = mktime(0, 0, 0, $infostart['mon'], ($infostart['mday'] + 1), $infostart['year']); | |
| 172 | + $today_tsin = VikBooking::getDateTimestamp(date($df, $infostart[0]), $checkin_h, $checkin_m); | |
| 173 | + $today_tsout = VikBooking::getDateTimestamp(date($df, $tomorrow_ts), $checkout_h, $checkout_m); | |
| 174 | + $today_mid_ts = mktime(0, 0, 0, $infostart['mon'], $infostart['mday'], $infostart['year']); | |
| 191 | 175 | |
| 192 | - foreach ($tars as $index => $tar) { | |
| 193 | - // apply rounding to 2 decimals at most | |
| 194 | - $tars[$index]['cost'] = round($tar['cost'], 2); | |
| 176 | + // current day key | |
| 177 | + $day_key = date('Y-m-d', $infostart[0]); | |
| 195 | 178 | |
| 196 | - // set formatted cost | |
| 197 | - $tars[$index]['formatted_cost'] = VikBooking::numberFormat($tar['cost']); | |
| 179 | + if (count($pool_roomrates) > 1 && !$all_rplans) { | |
| 180 | + // process all listings at once through the cached season records in case of | |
| 181 | + // multiple listings and single rate plan to reduce the work load | |
| 182 | + $listing_tars = VikBooking::applySeasonalPrices($pool_roomrates, $today_tsin, $today_tsout, $cached_seasons, $cached_wdayseasons); | |
| 198 | 183 | |
| 199 | - // calculate restrictions | |
| 200 | - $tars[$index]['restrictions'] = []; | |
| 201 | - if ($restrictions) { | |
| 202 | - $restr = VikBooking::parseSeasonRestrictions($today_mid_ts, $tomorrow_ts, 1, $all_restrictions); | |
| 203 | - if (!$restr) { | |
| 204 | - $restr = ['minlos' => $glob_minlos]; | |
| 205 | - } | |
| 206 | - // set day restrictions | |
| 207 | - $tars[$index]['restrictions'] = $restr; | |
| 208 | - } | |
| 209 | - } | |
| 184 | + // scan the tariff results | |
| 185 | + foreach ($listing_tars as $listing_id => $tars) { | |
| 186 | + // initialize listing rates, if needed | |
| 187 | + if (!isset($current_rates_pool[$listing_id])) { | |
| 188 | + $current_rates_pool[$listing_id] = []; | |
| 189 | + } | |
| 210 | 190 | |
| 211 | - if (!$all_rplans) { | |
| 212 | - // set rate for this day (single rate plan) | |
| 213 | - $current_rates[(date('Y-m-d', $infostart[0]))] = $tars[0]; | |
| 214 | - } else { | |
| 215 | - // set rates for this day (all rate plans) | |
| 216 | - $current_rates[(date('Y-m-d', $infostart[0]))] = $tars; | |
| 217 | - } | |
| 191 | + // filter restrictions by the current listing | |
| 192 | + $listing_restrictions = VikBooking::listingRestrictions($all_restrictions, [$listing_id]); | |
| 218 | 193 | |
| 219 | - // go to next day | |
| 220 | - $infostart = getdate($tomorrow_ts); | |
| 221 | - } | |
| 194 | + foreach ($tars as $index => $tar) { | |
| 195 | + // apply rounding to 2 decimals at most | |
| 196 | + $tars[$index]['cost'] = round($tar['cost'], 2); | |
| 222 | 197 | |
| 223 | - // free memory up | |
| 224 | - unset($cached_seasons); | |
| 198 | + // set formatted cost | |
| 199 | + $tars[$index]['formatted_cost'] = VikBooking::numberFormat($tar['cost']); | |
| 225 | 200 | |
| 226 | - // return the calculated rates | |
| 227 | - return $current_rates; | |
| 228 | - } | |
| 201 | + // calculate restrictions | |
| 202 | + $tars[$index]['restrictions'] = []; | |
| 203 | + if ($restrictions) { | |
| 204 | + $restr = VikBooking::parseSeasonRestrictions($today_mid_ts, $tomorrow_ts, 1, $listing_restrictions); | |
| 205 | + if (!$restr) { | |
| 206 | + $restr = ['minlos' => $glob_minlos]; | |
| 207 | + } | |
| 208 | + // set day restrictions | |
| 209 | + $tars[$index]['restrictions'] = $restr; | |
| 210 | + } | |
| 211 | + } | |
| 229 | 212 | |
| 230 | - /** | |
| 231 | - * Applies new rates and/or restrictions to the given room and rate plan(s). | |
| 232 | - * Changes are always applied to the website rates, and eventually also on the OTAs. | |
| 233 | - * | |
| 234 | - * @return array | |
| 235 | - * | |
| 236 | - * @throws Exception | |
| 237 | - * | |
| 238 | - * @since 1.17.1 (J) - 1.7.1 (WP) added support to CTA, CTD and Max LOS restrictions. | |
| 239 | - */ | |
| 240 | - public function modifyRateRestrictions() | |
| 241 | - { | |
| 242 | - $dbo = JFactory::getDbo(); | |
| 213 | + // set rate for this day (single rate plan) | |
| 214 | + $current_rates_pool[$listing_id][$day_key] = $tars[0]; | |
| 215 | + } | |
| 216 | + } else { | |
| 217 | + // iterate over all listing IDs involved | |
| 218 | + foreach ($listing_ids as $listing_id) { | |
| 219 | + if (!isset($pool_roomrates[$listing_id])) { | |
| 220 | + continue; | |
| 221 | + } | |
| 243 | 222 | |
| 244 | - // expected and supported properties binded | |
| 245 | - $from_date = (string) $this->get('from_date', ''); | |
| 246 | - $to_date = (string) $this->get('to_date', ''); | |
| 247 | - $id_room = (int) $this->get('id_room', 0); | |
| 248 | - $id_price = (int) $this->get('id_price', 0); | |
| 249 | - $rplan_name = $this->get('rplan_name', ''); | |
| 250 | - $rate = (float) $this->get('rate', 0); | |
| 251 | - $min_los = (int) $this->get('min_los', 0); | |
| 252 | - $max_los = (int) $this->get('max_los', 0); | |
| 253 | - $cta_wdays = (array) $this->get('cta_wdays', []); | |
| 254 | - $ctd_wdays = (array) $this->get('ctd_wdays', []); | |
| 255 | - $upd_otas = (bool) $this->get('update_otas', true); | |
| 256 | - $close_rplan = (bool) $this->get('close_rate_plan', false); | |
| 257 | - $merge_restr = (bool) $this->get('merge_restrictions', true); | |
| 258 | - $ota_pricing = (array) $this->get('ota_pricing', []); | |
| 223 | + // initialize listing rates, if needed | |
| 224 | + if (!isset($current_rates_pool[$listing_id])) { | |
| 225 | + $current_rates_pool[$listing_id] = []; | |
| 226 | + } | |
| 259 | 227 | |
| 260 | - if (!$from_date || !$to_date) { | |
| 261 | - // must be in Y-m-d format | |
| 262 | - throw new InvalidArgumentException('Missing dates for applying the new rates or restriction.', 400); | |
| 263 | - } | |
| 228 | + // filter restrictions by the current listing | |
| 229 | + $listing_restrictions = VikBooking::listingRestrictions($all_restrictions, [$listing_id]); | |
| 264 | 230 | |
| 265 | - if (!$id_room) { | |
| 266 | - throw new InvalidArgumentException('Room record ID is mandatory.', 400); | |
| 267 | - } | |
| 231 | + // calculate listing tariffs for this day | |
| 232 | + $tars = VikBooking::applySeasonsRoom($pool_roomrates[$listing_id], $today_tsin, $today_tsout, [], $cached_seasons, $cached_wdayseasons); | |
| 268 | 233 | |
| 269 | - /** | |
| 270 | - * Disable season records caching because new rates will have to be re-calculated | |
| 271 | - * for the response by checking the same exact dates. | |
| 272 | - */ | |
| 273 | - VikBooking::setSeasonsCache(false); | |
| 234 | + foreach ($tars as $index => $tar) { | |
| 235 | + // apply rounding to 2 decimals at most | |
| 236 | + $tars[$index]['cost'] = round($tar['cost'], 2); | |
| 274 | 237 | |
| 275 | - // load check-in and check-out times | |
| 276 | - list($checkin_h, $checkin_m, $checkout_h, $checkout_m) = VBOModelReservation::getInstance([], true)->loadCheckinOutTimes(); | |
| 238 | + // set formatted cost | |
| 239 | + $tars[$index]['formatted_cost'] = VikBooking::numberFormat($tar['cost']); | |
| 277 | 240 | |
| 278 | - // date format | |
| 279 | - $vbo_df = VikBooking::getDateFormat(); | |
| 280 | - $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y/m/d'); | |
| 241 | + // calculate restrictions | |
| 242 | + $tars[$index]['restrictions'] = []; | |
| 243 | + if ($restrictions) { | |
| 244 | + $restr = VikBooking::parseSeasonRestrictions($today_mid_ts, $tomorrow_ts, 1, $listing_restrictions); | |
| 245 | + if (!$restr) { | |
| 246 | + $restr = ['minlos' => $glob_minlos]; | |
| 247 | + } | |
| 248 | + // set day restrictions | |
| 249 | + $tars[$index]['restrictions'] = $restr; | |
| 250 | + } | |
| 251 | + } | |
| 281 | 252 | |
| 282 | - // access the availability helper | |
| 283 | - $av_helper = VikBooking::getAvailabilityInstance(true); | |
| 253 | + if (!$all_rplans) { | |
| 254 | + // set rate for this day (single rate plan) | |
| 255 | + $current_rates_pool[$listing_id][$day_key] = $tars[0]; | |
| 256 | + } else { | |
| 257 | + // set rates for this day (all rate plans) | |
| 258 | + $current_rates_pool[$listing_id][$day_key] = $tars; | |
| 259 | + } | |
| 260 | + } | |
| 261 | + } | |
| 284 | 262 | |
| 285 | - if (!$id_price) { | |
| 286 | - // load all rate plans | |
| 287 | - $all_rate_plans = $av_helper->loadRatePlans(); | |
| 263 | + // go to next day | |
| 264 | + $infostart = getdate($tomorrow_ts); | |
| 265 | + } | |
| 288 | 266 | |
| 289 | - // use the first rate plan ID after the automatic sorting | |
| 290 | - foreach ($all_rate_plans as $all_rate_plan) { | |
| 291 | - $id_price = $all_rate_plan['id']; | |
| 292 | - break; | |
| 293 | - } | |
| 267 | + // free memory up | |
| 268 | + unset($cached_seasons, $cached_wdayseasons); | |
| 294 | 269 | |
| 295 | - if ($rplan_name) { | |
| 296 | - // check if the given rate plan name is found | |
| 297 | - $rplan_name = trim(preg_replace("/[^a-z]/i", ' ', $rplan_name)); | |
| 298 | - foreach ($all_rate_plans as $all_rate_plan) { | |
| 299 | - $match_against = trim(preg_replace("/[^a-z]/i", ' ', $all_rate_plan['name'])); | |
| 300 | - if (stripos($match_against, $rplan_name) !== false || stripos($rplan_name, $match_against) !== false) { | |
| 301 | - // use the matched rate plan instead | |
| 302 | - $id_price = $all_rate_plan['id']; | |
| 303 | - break; | |
| 304 | - } | |
| 305 | - } | |
| 306 | - } | |
| 307 | - } | |
| 270 | + if ($id_room && isset($current_rates_pool[$id_room])) { | |
| 271 | + // single listing room rates requested | |
| 272 | + return $current_rates_pool[$id_room]; | |
| 273 | + } | |
| 308 | 274 | |
| 309 | - if (!$id_price) { | |
| 310 | - throw new Exception('No rate plans configured.', 500); | |
| 311 | - } | |
| 275 | + // return the calculated rates for all listings | |
| 276 | + return $current_rates_pool; | |
| 277 | + } | |
| 312 | 278 | |
| 313 | - // load the eventually involved derived rate plans from the given rate ID | |
| 314 | - $derived_rate_plans = $av_helper->getDerivedRatePlans($id_price); | |
| 279 | + /** | |
| 280 | + * Calculates and applies new rates (and restrictions) to the given room and rate plan(s). | |
| 281 | + * Rather than immediately setting a fixed rate (exact rate), calculates the current room | |
| 282 | + * rates for the given range of dates for either increasing or decreasing them. | |
| 283 | + * | |
| 284 | + * @return array | |
| 285 | + * | |
| 286 | + * @throws Exception | |
| 287 | + * | |
| 288 | + * @since 1.18.6 (J) - 1.8.6 (WP) | |
| 289 | + */ | |
| 290 | + public function increaseDecreaseRoomRates() | |
| 291 | + { | |
| 292 | + // expected and supported properties binded | |
| 293 | + $from_date = (string) $this->get('from_date', ''); | |
| 294 | + $to_date = (string) $this->get('to_date', ''); | |
| 295 | + $id_room = (int) $this->get('id_room', 0); | |
| 296 | + $id_price = (int) $this->get('id_price', 0); | |
| 297 | + $addsub_op = (int) $this->get('addsub_op', 0); | |
| 298 | + $addsub_amount = (float) $this->get('addsub_amount', 0); | |
| 299 | + $addsub_value = (int) $this->get('addsub_value', 0); | |
| 300 | + $upd_otas = (bool) $this->get('update_otas', true); | |
| 301 | + $async_rar = (bool) $this->get('async_rar', false); | |
| 315 | 302 | |
| 316 | - // build the list of rate plans involved by adding the requested one | |
| 317 | - $rate_plans_pool = [ | |
| 318 | - $id_price => [ | |
| 319 | - 'id' => $id_price, | |
| 320 | - // the main rate plan selected for the update is NEVER considered as derived, even if it actually was. | |
| 321 | - 'is_derived' => 0, | |
| 322 | - 'derived_data' => null, | |
| 323 | - 'rate' => $rate, | |
| 324 | - ], | |
| 325 | - ]; | |
| 303 | + if (!$from_date || !$to_date) { | |
| 304 | + // must be in Y-m-d format | |
| 305 | + throw new InvalidArgumentException('Missing dates for applying the new rates or restriction.', 400); | |
| 306 | + } | |
| 326 | 307 | |
| 327 | - foreach ($derived_rate_plans as $derived_rate_plan) { | |
| 328 | - if (isset($rate_plans_pool[$derived_rate_plan['id']])) { | |
| 329 | - // skip duplicate rate plan | |
| 330 | - continue; | |
| 331 | - } | |
| 308 | + if (!$id_room) { | |
| 309 | + throw new InvalidArgumentException('Room record ID is mandatory.', 400); | |
| 310 | + } | |
| 332 | 311 | |
| 333 | - // calculate new rate for this derived rate plan | |
| 334 | - $rplan_derived_rate = $rate; | |
| 335 | - if (($derived_rate_plan['derived_data']['mode'] ?? 'discount') == 'discount') { | |
| 336 | - // discount rate | |
| 337 | - if (($derived_rate_plan['derived_data']['type'] ?? 'percent') == 'percent') { | |
| 338 | - // percent value | |
| 339 | - $rplan_derived_rate = $rplan_derived_rate * (100 - (float) ($derived_rate_plan['derived_data']['value'] ?? 0)) / 100; | |
| 340 | - } else { | |
| 341 | - // absolute value | |
| 342 | - $rplan_derived_rate -= (float) ($derived_rate_plan['derived_data']['value'] ?? 0); | |
| 343 | - } | |
| 344 | - } else { | |
| 345 | - // increase rate | |
| 346 | - if (($derived_rate_plan['derived_data']['type'] ?? 'percent') == 'percent') { | |
| 347 | - // percent value | |
| 348 | - $rplan_derived_rate = $rplan_derived_rate * (100 + (float) ($derived_rate_plan['derived_data']['value'] ?? 0)) / 100; | |
| 349 | - } else { | |
| 350 | - // absolute value | |
| 351 | - $rplan_derived_rate += (float) ($derived_rate_plan['derived_data']['value'] ?? 0); | |
| 352 | - } | |
| 353 | - } | |
| 312 | + // get full command properties eligible for setting new rates | |
| 313 | + $rate_command = $this->getProperties(); | |
| 354 | 314 | |
| 355 | - if ($rplan_derived_rate < 0) { | |
| 356 | - // negative rates are not allowed | |
| 357 | - continue; | |
| 358 | - } | |
| 315 | + // unset command properties related to increase/decrease rates and get rid of initial dates | |
| 316 | + unset( | |
| 317 | + $rate_command['addsub_op'], | |
| 318 | + $rate_command['addsub_amount'], | |
| 319 | + $rate_command['addsub_value'], | |
| 320 | + $rate_command['from_date'], | |
| 321 | + $rate_command['to_date'], | |
| 322 | + $rate_command['async_rar'] | |
| 323 | + ); | |
| 359 | 324 | |
| 360 | - // make sure to apply rounding | |
| 361 | - $rplan_derived_rate = round($rplan_derived_rate, 2); | |
| 325 | + // fetch current room rates for the given dates by ignoring restrictions | |
| 326 | + $room_rates = $this->getRoomRates([ | |
| 327 | + 'from_date' => $from_date, | |
| 328 | + 'to_date' => $to_date, | |
| 329 | + 'id_room' => $id_room, | |
| 330 | + 'id_price' => $id_price, | |
| 331 | + 'all_rplans' => false, | |
| 332 | + 'restrictions' => false, | |
| 333 | + ]); | |
| 362 | 334 | |
| 363 | - // push derived rate plan to the update pool | |
| 364 | - $rate_plans_pool[$derived_rate_plan['id']] = [ | |
| 365 | - 'id' => $derived_rate_plan['id'], | |
| 366 | - 'is_derived' => 1, | |
| 367 | - 'derived_data' => $derived_rate_plan['derived_data'], | |
| 368 | - 'rate' => $rplan_derived_rate, | |
| 369 | - ]; | |
| 370 | - } | |
| 335 | + // build raw list of rates data instructions per day | |
| 336 | + $rates_daily_data = []; | |
| 371 | 337 | |
| 372 | - // the newly applied rates | |
| 373 | - $newly_rates = []; | |
| 338 | + // calculate the new rates to set for every inventory date | |
| 339 | + foreach ($room_rates as $day_key => $tariff) { | |
| 340 | + // get day room rate | |
| 341 | + $currentRate = (float) ($tariff['cost'] ?? 0); | |
| 374 | 342 | |
| 375 | - // apply the pricing modification to all the involved rate plans | |
| 376 | - foreach ($rate_plans_pool as $rplan_id => $rplan_info) { | |
| 377 | - // set rate plan ID | |
| 378 | - $now_id_price = $rplan_info['id']; | |
| 343 | + // ensure the rate plan ID is set | |
| 344 | + if (!$id_price && intval($tariff['idprice']) > 0) { | |
| 345 | + $id_price = (int) $tariff['idprice']; | |
| 346 | + } | |
| 379 | 347 | |
| 380 | - // set rate to apply to the current rate plan | |
| 381 | - $rate = $rplan_info['rate']; | |
| 348 | + // calculate the fixed (exact) new rate to set | |
| 349 | + if ($addsub_op === 1) { | |
| 350 | + // increase rate | |
| 351 | + if ($addsub_value === 1) { | |
| 352 | + // percent | |
| 353 | + $currentRate += $currentRate * $addsub_amount / 100; | |
| 354 | + } else { | |
| 355 | + // fixed | |
| 356 | + $currentRate += $addsub_amount; | |
| 357 | + } | |
| 358 | + } else { | |
| 359 | + // decrease rate | |
| 360 | + if ($addsub_value === 1) { | |
| 361 | + // percent | |
| 362 | + $currentRate -= $currentRate * $addsub_amount / 100; | |
| 363 | + } else { | |
| 364 | + // fixed | |
| 365 | + $currentRate -= $addsub_amount; | |
| 366 | + } | |
| 367 | + } | |
| 382 | 368 | |
| 383 | - // read the rates for the lowest number of nights | |
| 384 | - $q = "SELECT `r`.`id`,`r`.`idroom`,`r`.`days`,`r`.`idprice`,`r`.`cost`,`p`.`name` | |
| 385 | - FROM `#__vikbooking_dispcost` AS `r` | |
| 386 | - INNER JOIN ( | |
| 387 | - SELECT MIN(`days`) AS `min_days` | |
| 388 | - FROM `#__vikbooking_dispcost` | |
| 389 | - WHERE `idroom`=" . $id_room . " AND `idprice`=" . $now_id_price . " | |
| 390 | - GROUP BY `idroom` | |
| 391 | - ) AS `r2` ON `r`.`days`=`r2`.`min_days` | |
| 392 | - LEFT JOIN `#__vikbooking_prices` `p` ON `p`.`id`=`r`.`idprice` AND `p`.`id`=" . $now_id_price . " | |
| 393 | - WHERE `r`.`idroom`=" . $id_room . " AND `r`.`idprice`=" . $now_id_price . " | |
| 394 | - GROUP BY `r`.`id`,`r`.`idroom`,`r`.`days`,`r`.`idprice`,`r`.`cost`,`p`.`name` | |
| 395 | - ORDER BY `r`.`days` ASC, `r`.`cost` ASC;"; | |
| 369 | + if ($currentRate < 0) { | |
| 370 | + throw new Exception(sprintf('Invalid (negative) rate calculated for %s.', $day_key), 400); | |
| 371 | + } | |
| 396 | 372 | |
| 397 | - $dbo->setQuery($q); | |
| 398 | - $roomrates = $dbo->loadAssocList(); | |
| 373 | + // push rate data instructions with date and calculated rate | |
| 374 | + $rates_daily_data[] = array_merge($rate_command, [ | |
| 375 | + 'rate' => $currentRate, | |
| 376 | + 'date' => $day_key, | |
| 377 | + 'idprice' => $id_price, | |
| 378 | + ]); | |
| 379 | + } | |
| 399 | 380 | |
| 400 | - foreach ($roomrates as $rrk => $rrv) { | |
| 401 | - $roomrates[$rrk]['cost'] = round(($rrv['cost'] / $rrv['days']), 2); | |
| 402 | - $roomrates[$rrk]['days'] = 1; | |
| 403 | - } | |
| 381 | + // normalize single and contiguous dates into a range | |
| 382 | + $rates_data_range = []; | |
| 383 | + $rates_data_sdate = null; | |
| 384 | + $rates_data_edate = null; | |
| 385 | + $rates_data_sign = null; | |
| 386 | + foreach ($rates_daily_data as $rate_data) { | |
| 387 | + // clone rate data command by deleting the date key | |
| 388 | + $rate_data_command = $rate_data; | |
| 389 | + unset($rate_data_command['date']); | |
| 404 | 390 | |
| 405 | - if (!$roomrates) { | |
| 406 | - if ($rplan_info['is_derived']) { | |
| 407 | - // this is not the main rate plan we are updating | |
| 408 | - continue; | |
| 409 | - } | |
| 391 | + // calculate rate data signature | |
| 392 | + $current_data_sign = serialize($rate_data_command); | |
| 410 | 393 | |
| 411 | - // terminate the process by throwing an error | |
| 412 | - throw new UnexpectedValueException('No rates found for the given room ID.', 400); | |
| 413 | - } | |
| 394 | + if (!$rates_data_sdate) { | |
| 395 | + // set range start date | |
| 396 | + $rates_data_sdate = $rate_data['date']; | |
| 397 | + // set range end date | |
| 398 | + $rates_data_edate = $rate_data['date']; | |
| 399 | + // set rate data signature | |
| 400 | + $rates_data_sign = $current_data_sign; | |
| 401 | + // go to next rate data object | |
| 402 | + continue; | |
| 403 | + } | |
| 414 | 404 | |
| 415 | - // turn the rates list into a single-level array | |
| 416 | - $roomrates = $roomrates[0]; | |
| 405 | + if ($current_data_sign != $rates_data_sign || $rate_data['date'] != date('Y-m-d', strtotime('+1 day', strtotime($rates_data_edate)))) { | |
| 406 | + // push previous rate-data object | |
| 407 | + $rates_data_range[] = array_merge([ | |
| 408 | + 'start' => $rates_data_sdate, | |
| 409 | + 'end' => $rates_data_edate, | |
| 410 | + ], unserialize($rates_data_sign)); | |
| 411 | + // set range start date | |
| 412 | + $rates_data_sdate = $rate_data['date']; | |
| 413 | + // set range end date | |
| 414 | + $rates_data_edate = $rate_data['date']; | |
| 415 | + // set rate data signature | |
| 416 | + $rates_data_sign = $current_data_sign; | |
| 417 | + } else { | |
| 418 | + // contiguous date with equal rate-data information found | |
| 419 | + $rates_data_edate = $rate_data['date']; | |
| 420 | + } | |
| 421 | + } | |
| 417 | 422 | |
| 418 | - // set rate plan name | |
| 419 | - $rate_plan_name = $roomrates['name']; | |
| 423 | + if ($rates_data_sdate && $rates_data_edate) { | |
| 424 | + // push last rate-data object | |
| 425 | + $rates_data_range[] = array_merge([ | |
| 426 | + 'start' => $rates_data_sdate, | |
| 427 | + 'end' => $rates_data_edate, | |
| 428 | + ], unserialize($rates_data_sign)); | |
| 429 | + } | |
| 420 | 430 | |
| 421 | - // dates involved | |
| 422 | - $start_ts = strtotime($from_date); | |
| 423 | - $end_ts = strtotime($to_date); | |
| 431 | + // set the sorted and grouped rate-data objects | |
| 432 | + $rates_data = $rates_data_range; | |
| 433 | + unset($rates_data_range); | |
| 424 | 434 | |
| 425 | - /** | |
| 426 | - * Preload seasonal records in favour of CPU usage, but against RAM usage. | |
| 427 | - * | |
| 428 | - * @since 1.17.2 (J) - 1.7.2 (WP) | |
| 429 | - */ | |
| 430 | - $cached_seasons = VikBooking::getDateSeasonRecords($start_ts, ($end_ts + ($checkout_h * 3600)), [$id_room]); | |
| 435 | + // count the number of requests to perform to determine if | |
| 436 | + // OTAs should be updated in background, asynchronously | |
| 437 | + $async_upd_otas = $upd_otas && (count($rates_data) > 10 || $async_rar); | |
| 431 | 438 | |
| 432 | - // read current room rates | |
| 433 | - $current_rates = []; | |
| 434 | - $infostart = getdate($start_ts); | |
| 435 | - while ($infostart[0] > 0 && $infostart[0] <= $end_ts) { | |
| 436 | - $tomorrow_ts = mktime(0, 0, 0, $infostart['mon'], ($infostart['mday'] + 1), $infostart['year']); | |
| 437 | - $today_tsin = VikBooking::getDateTimestamp(date($df, $infostart[0]), $checkin_h, $checkin_m); | |
| 438 | - $today_tsout = VikBooking::getDateTimestamp(date($df, $tomorrow_ts), $checkout_h, $checkout_m); | |
| 439 | + // gather the results list | |
| 440 | + $new_rates_list = []; | |
| 439 | 441 | |
| 440 | - // apply seasonal rates by injecting the cached seasonal records | |
| 441 | - $tars = VikBooking::applySeasonsRoom([$roomrates], $today_tsin, $today_tsout, [], $cached_seasons); | |
| 442 | + // iterate all rate-data request objects | |
| 443 | + foreach ($rates_data as $rate_data) { | |
| 444 | + // normalize properties from current rate data | |
| 445 | + $rate_data['from_date'] = $rate_data['start']; | |
| 446 | + $rate_data['to_date'] = $rate_data['end']; | |
| 447 | + unset($rate_data['start'], $rate_data['end']); | |
| 442 | 448 | |
| 443 | - // apply rounding to 2 decimals at most | |
| 444 | - $tars[0]['cost'] = round($tars[0]['cost'], 2); | |
| 449 | + // check if OTAs should be updated | |
| 450 | + $rate_data['update_otas'] = !$async_upd_otas && $upd_otas; | |
| 445 | 451 | |
| 446 | - $current_rates[(date('Y-m-d', $infostart[0]))] = $tars[0]; | |
| 452 | + // bind rate data properties | |
| 453 | + $this->setProperties($rate_data); | |
| 447 | 454 | |
| 448 | - $infostart = getdate($tomorrow_ts); | |
| 449 | - } | |
| 455 | + // apply the new rate/restrictions | |
| 456 | + $new_rates = $this->modifyRateRestrictions(); | |
| 450 | 457 | |
| 451 | - if (!$current_rates) { | |
| 452 | - if ($rplan_info['is_derived']) { | |
| 453 | - // this is not the main rate plan we are updating | |
| 454 | - continue; | |
| 455 | - } | |
| 458 | + if ($new_rates['vcm'] ?? null) { | |
| 459 | + // merge with previous channel manager results, if any | |
| 460 | + $new_rates_list['vcm'] = array_merge(($new_rates_list['vcm'] ?? []), $new_rates['vcm']); | |
| 456 | 461 | |
| 457 | - // terminate the process by throwing an error | |
| 458 | - throw new UnexpectedValueException('No seasonal rates found for the given room ID.', 400); | |
| 459 | - } | |
| 462 | + // unset channel manager results property | |
| 463 | + unset($new_rates['vcm']); | |
| 464 | + } | |
| 460 | 465 | |
| 461 | - $all_days = array_keys($current_rates); | |
| 462 | - $season_intervals = []; | |
| 463 | - $firstind = 0; | |
| 464 | - $firstdaycost = $current_rates[$all_days[0]]['cost']; | |
| 465 | - $nextdaycost = false; | |
| 466 | - for ($i = 1; $i < count($all_days); $i++) { | |
| 467 | - $ind = $all_days[$i]; | |
| 468 | - $nextdaycost = $current_rates[$ind]['cost']; | |
| 469 | - if ($firstdaycost != $nextdaycost) { | |
| 470 | - $interval = [ | |
| 471 | - 'from' => $all_days[$firstind], | |
| 472 | - 'to' => $all_days[($i - 1)], | |
| 473 | - 'cost' => $firstdaycost | |
| 474 | - ]; | |
| 475 | - $season_intervals[] = $interval; | |
| 476 | - $firstdaycost = $nextdaycost; | |
| 477 | - $firstind = $i; | |
| 478 | - } | |
| 479 | - } | |
| 480 | - if ($nextdaycost === false) { | |
| 481 | - $interval = [ | |
| 482 | - 'from' => $all_days[$firstind], | |
| 483 | - 'to' => $all_days[$firstind], | |
| 484 | - 'cost' => $firstdaycost | |
| 485 | - ]; | |
| 486 | - $season_intervals[] = $interval; | |
| 487 | - } elseif ($firstdaycost == $nextdaycost) { | |
| 488 | - $interval = [ | |
| 489 | - 'from' => $all_days[$firstind], | |
| 490 | - 'to' => $all_days[($i - 1)], | |
| 491 | - 'cost' => $firstdaycost | |
| 492 | - ]; | |
| 493 | - $season_intervals[] = $interval; | |
| 494 | - } | |
| 495 | - foreach ($season_intervals as $sik => $siv) { | |
| 496 | - if ((float)$siv['cost'] == $rate) { | |
| 497 | - unset($season_intervals[$sik]); | |
| 498 | - } | |
| 499 | - } | |
| 466 | + // merge daily rates applied | |
| 467 | + $new_rates_list = array_merge($new_rates_list, $new_rates); | |
| 468 | + } | |
| 500 | 469 | |
| 501 | - if (!$season_intervals) { | |
| 502 | - // do not raise this error if it was requested to set the restriction or to close a rate plan | |
| 503 | - if ((!($min_los > 0) && !$close_rplan) || !$upd_otas) { | |
| 504 | - if ($rplan_info['is_derived']) { | |
| 505 | - // this is not the main rate plan we are updating | |
| 506 | - continue; | |
| 507 | - } | |
| 470 | + // check whether OTAs should be updated asynchronously | |
| 471 | + if ($async_upd_otas) { | |
| 472 | + // trigger an automatic bulk action for uploading the rates just applied to the PMS | |
| 473 | + VikChannelManager::autoBulkActions([ | |
| 474 | + 'from_date' => $from_date, | |
| 475 | + 'to_date' => $to_date, | |
| 476 | + 'forced_rooms' => [$id_room], | |
| 477 | + 'update' => 'rates', | |
| 478 | + ]); | |
| 479 | + } | |
| 508 | 480 | |
| 509 | - // terminate the process by throwing an error | |
| 510 | - throw new RuntimeException('No rates modification needed with the given parameters.', 500); | |
| 511 | - } | |
| 512 | - } | |
| 481 | + // returned the merged list of rate results | |
| 482 | + return $new_rates_list; | |
| 483 | + } | |
| 513 | 484 | |
| 514 | - if ($rate > 0) { | |
| 515 | - // make sure to set a cost greater than zero to avoid errors | |
| 516 | - foreach ($season_intervals as $sik => $siv) { | |
| 517 | - $first = strtotime($siv['from']); | |
| 518 | - $second = strtotime($siv['to']); | |
| 485 | + /** | |
| 486 | + * Applies new rates and/or restrictions to the given room and rate plan(s). | |
| 487 | + * Changes are always applied to the website rates, and eventually also on the OTAs. | |
| 488 | + * | |
| 489 | + * @return array | |
| 490 | + * | |
| 491 | + * @throws Exception | |
| 492 | + * | |
| 493 | + * @since 1.17.1 (J) - 1.7.1 (WP) added support to CTA, CTD and Max LOS restrictions. | |
| 494 | + */ | |
| 495 | + public function modifyRateRestrictions() | |
| 496 | + { | |
| 497 | + $dbo = JFactory::getDbo(); | |
| 519 | 498 | |
| 520 | - if ($second > 0 && $second == $first) { | |
| 521 | - $second += 86399; | |
| 522 | - } | |
| 499 | + // expected and supported properties binded | |
| 500 | + $from_date = (string) $this->get('from_date', ''); | |
| 501 | + $to_date = (string) $this->get('to_date', ''); | |
| 502 | + $id_room = (int) $this->get('id_room', 0); | |
| 503 | + $id_price = (int) $this->get('id_price', 0); | |
| 504 | + $rplan_name = $this->get('rplan_name', ''); | |
| 505 | + $rate = (float) $this->get('rate', 0); | |
| 506 | + $min_los = (int) $this->get('min_los', 0); | |
| 507 | + $max_los = (int) $this->get('max_los', 0); | |
| 508 | + $cta_wdays = (array) $this->get('cta_wdays', []); | |
| 509 | + $ctd_wdays = (array) $this->get('ctd_wdays', []); | |
| 510 | + $upd_otas = (bool) $this->get('update_otas', true); | |
| 511 | + $close_rplan = (bool) $this->get('close_rate_plan', false); | |
| 512 | + $merge_restr = (bool) $this->get('merge_restrictions', true); | |
| 513 | + $ota_pricing = (array) $this->get('ota_pricing', []); | |
| 514 | + $skip_derived = (bool) $this->get('skip_derived', false); | |
| 515 | + $use_cache = (bool) $this->get('use_cache', false); | |
| 523 | 516 | |
| 524 | - if (!($second > $first)) { | |
| 525 | - unset($season_intervals[$sik]); | |
| 526 | - continue; | |
| 527 | - } | |
| 517 | + if (!$from_date || !$to_date) { | |
| 518 | + // must be in Y-m-d format | |
| 519 | + throw new InvalidArgumentException('Missing dates for applying the new rates or restriction.', 400); | |
| 520 | + } | |
| 528 | 521 | |
| 529 | - $baseone = getdate($first); | |
| 530 | - $basets = mktime(0, 0, 0, 1, 1, $baseone['year']); | |
| 531 | - $sfrom = $baseone[0] - $basets; | |
| 532 | - $basetwo = getdate($second); | |
| 533 | - $basets = mktime(0, 0, 0, 1, 1, $basetwo['year']); | |
| 534 | - $sto = $basetwo[0] - $basets; | |
| 522 | + if (!$id_room) { | |
| 523 | + throw new InvalidArgumentException('Room record ID is mandatory.', 400); | |
| 524 | + } | |
| 535 | 525 | |
| 536 | - // check leap year | |
| 537 | - if ($baseone['year'] % 4 == 0 && ($baseone['year'] % 100 != 0 || $baseone['year'] % 400 == 0)) { | |
| 538 | - $leapts = mktime(0, 0, 0, 2, 29, $baseone['year']); | |
| 539 | - if ($baseone[0] > $leapts) { | |
| 540 | - $sfrom -= 86400; | |
| 541 | - /** | |
| 542 | - * To avoid issue with leap years and dates near Feb 29th, we only reduce the seconds if these were reduced | |
| 543 | - * for the from-date of the seasons. Doing it just for the to-date in 2019 for 2020 (leap) produced invalid results. | |
| 544 | - */ | |
| 545 | - if ($basetwo['year'] % 4 == 0 && ($basetwo['year'] % 100 != 0 || $basetwo['year'] % 400 == 0)) { | |
| 546 | - $leapts = mktime(0, 0, 0, 2, 29, $basetwo['year']); | |
| 547 | - if ($basetwo[0] > $leapts) { | |
| 548 | - $sto -= date('d-m', $baseone[0]) != '31-12' && date('d-m', $basetwo[0]) == '31-12' ? 1 : 86400; | |
| 549 | - } | |
| 550 | - } | |
| 551 | - } | |
| 552 | - } | |
| 526 | + /** | |
| 527 | + * Disable season records caching because new rates will have to be re-calculated | |
| 528 | + * for the response by checking the same exact dates. | |
| 529 | + */ | |
| 530 | + VikBooking::setSeasonsCache(false); | |
| 553 | 531 | |
| 554 | - $tieyear = $baseone['year']; | |
| 555 | - $season_type = (float)$siv['cost'] > $rate ? "2" : "1"; | |
| 556 | - $season_diffcost = $season_type == "1" ? ($rate - (float)$siv['cost']) : ((float)$siv['cost'] - $rate); | |
| 557 | - $roomstr = "-" . $id_room . "-,"; | |
| 558 | - $season_name = date('Y-m-d H:i').' - '.substr($baseone['month'], 0, 3).' '.$baseone['mday'].($siv['from'] != $siv['to'] ? '/'.($baseone['month'] != $basetwo['month'] ? substr($basetwo['month'], 0, 3).' ' : '').$basetwo['mday'] : ''); | |
| 559 | - $pricestr = "-" . $now_id_price . "-,"; | |
| 532 | + // load check-in and check-out times | |
| 533 | + list($checkin_h, $checkin_m, $checkout_h, $checkout_m) = VBOModelReservation::getInstance([], true)->loadCheckinOutTimes(); | |
| 560 | 534 | |
| 561 | - // build and store season record | |
| 562 | - $season_record = new stdClass; | |
| 563 | - $season_record->type = $season_type == "1" ? 1 : 2; | |
| 564 | - $season_record->from = $sfrom; | |
| 565 | - $season_record->to = $sto; | |
| 566 | - $season_record->diffcost = $season_diffcost; | |
| 567 | - $season_record->idrooms = $roomstr; | |
| 568 | - $season_record->spname = $season_name; | |
| 569 | - $season_record->wdays = ''; | |
| 570 | - $season_record->checkinincl = 0; | |
| 571 | - $season_record->val_pcent = 1; | |
| 572 | - $season_record->losoverride = ''; | |
| 573 | - $season_record->year = $tieyear; | |
| 574 | - $season_record->idprices = $pricestr; | |
| 535 | + // date format | |
| 536 | + $vbo_df = VikBooking::getDateFormat(); | |
| 537 | + $df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y/m/d'); | |
| 575 | 538 | |
| 576 | - $dbo->insertObject('#__vikbooking_seasons', $season_record, 'id'); | |
| 539 | + // access the availability helper | |
| 540 | + $av_helper = VikBooking::getAvailabilityInstance(true); | |
| 577 | 541 | |
| 578 | - /** | |
| 579 | - * Push the newly created season record to the list of preloaded records. | |
| 580 | - * | |
| 581 | - * @since 1.17.2 (J) - 1.7.2 (WP) | |
| 582 | - */ | |
| 583 | - $cached_seasons[] = (array) $season_record; | |
| 584 | - } | |
| 585 | - } | |
| 542 | + // load all rate plans | |
| 543 | + $all_rate_plans = self::$cached_all_rate_plans ?: $av_helper->loadRatePlans(true); | |
| 544 | + self::$cached_all_rate_plans = $all_rate_plans; | |
| 586 | 545 | |
| 587 | - // calculate the involved dates | |
| 588 | - $start_ts = strtotime($from_date); | |
| 589 | - $end_ts = strtotime($to_date); | |
| 590 | - $infostart = getdate($start_ts); | |
| 591 | - $infoend = getdate($end_ts); | |
| 546 | + if (!$id_price) { | |
| 547 | + // use the first rate plan ID after the automatic sorting | |
| 548 | + foreach ($all_rate_plans as $all_rate_plan) { | |
| 549 | + $id_price = $all_rate_plan['id']; | |
| 550 | + break; | |
| 551 | + } | |
| 592 | 552 | |
| 593 | - /** | |
| 594 | - * Restrictions can be set only if VCM is enabled because we use the Connector Class. | |
| 595 | - * It is allowed to set just a restriction for the website without any rate modification. | |
| 596 | - * OTAs instead would need a rate to be passed in order to eventually transmit the restrictions. | |
| 597 | - */ | |
| 598 | - $current_minlos = 0; | |
| 599 | - $current_maxlos = 0; | |
| 600 | - $current_cta = []; | |
| 601 | - $current_ctd = []; | |
| 602 | - $split_ct_nodes = []; | |
| 603 | - $vboConnector = null; | |
| 553 | + if ($rplan_name) { | |
| 554 | + // check if the given rate plan name is found | |
| 555 | + $rplan_name = trim(preg_replace("/[^a-z]/i", ' ', $rplan_name)); | |
| 556 | + foreach ($all_rate_plans as $all_rate_plan) { | |
| 557 | + $match_against = trim(preg_replace("/[^a-z]/i", ' ', $all_rate_plan['name'])); | |
| 558 | + if (stripos($match_against, $rplan_name) !== false || stripos($rplan_name, $match_against) !== false) { | |
| 559 | + // use the matched rate plan instead | |
| 560 | + $id_price = $all_rate_plan['id']; | |
| 561 | + break; | |
| 562 | + } | |
| 563 | + } | |
| 564 | + } | |
| 565 | + } | |
| 604 | 566 | |
| 605 | - if (method_exists('VikChannelManager', 'getVikBookingConnectorInstance')) { | |
| 606 | - // invoke the Connector for any update request | |
| 607 | - $vboConnector = VikChannelManager::getVikBookingConnectorInstance(); | |
| 608 | - // set the caller to 'VBO' to reduce the sleep time between the requests | |
| 609 | - $vboConnector->caller = 'VBO'; | |
| 610 | - } else { | |
| 611 | - // make sure the OTA update flag is off | |
| 612 | - $upd_otas = false; | |
| 613 | - } | |
| 567 | + if (!$id_price) { | |
| 568 | + throw new Exception('No rate plans configured.', 500); | |
| 569 | + } | |
| 614 | 570 | |
| 615 | - // minimum length of stay is always necessary for creating a restriction even with other modifiers | |
| 616 | - if ($min_los > 0 && $vboConnector) { | |
| 617 | - // set the end date to the last second | |
| 618 | - $end_ts = mktime(23, 59, 59, $infoend['mon'], $infoend['mday'], $infoend['year']); | |
| 571 | + // load the eventually involved derived rate plans from the given rate ID | |
| 572 | + $derived_rate_plans = $skip_derived ? [] : $av_helper->getDerivedRatePlans($id_price, self::$cached_all_rate_plans); | |
| 619 | 573 | |
| 620 | - /** | |
| 621 | - * Setting just a min los restriction may lift a previously defined CTA/CTD rule for the same dates. | |
| 622 | - * If merging restrictions is not disabled, and if no other modifiers are set (max los, cta, ctd), the | |
| 623 | - * system will automatically calculate the current modifiers in order to keep them on the OTAs. Such | |
| 624 | - * calculated restriction modifiers will not need to be created on VikBooking, but just passed to the OTAs. | |
| 625 | - * If conflicting restriction modifiers are detected, it is recommended to schedule an auto bulk action. | |
| 626 | - * | |
| 627 | - * @since 1.17.1 (J) - 1.7.1 (WP) | |
| 628 | - */ | |
| 629 | - if ($merge_restr && !$max_los && !$cta_wdays && !$ctd_wdays) { | |
| 630 | - // calculate the restriction modifiers beside the min los and get additional details | |
| 631 | - list($calc_maxlos, $calc_cta, $calc_ctd, $split_ct_nodes, $conflicts) = $this->calculateRestrictionModifiers($start_ts, $end_ts, $id_room); | |
| 574 | + // build the list of rate plans involved by adding the requested one | |
| 575 | + $rate_plans_pool = [ | |
| 576 | + $id_price => [ | |
| 577 | + 'id' => $id_price, | |
| 578 | + // the main rate plan selected for the update is NEVER considered as derived, even if it actually was. | |
| 579 | + 'is_derived' => 0, | |
| 580 | + 'derived_data' => null, | |
| 581 | + 'rate' => $rate, | |
| 582 | + ], | |
| 583 | + ]; | |
| 632 | 584 | |
| 633 | - if ($calc_maxlos) { | |
| 634 | - // set calculated max los | |
| 635 | - $max_los = $calc_maxlos; | |
| 636 | - } | |
| 585 | + foreach ($derived_rate_plans as $derived_rate_plan) { | |
| 586 | + if (isset($rate_plans_pool[$derived_rate_plan['id']])) { | |
| 587 | + // skip duplicate rate plan | |
| 588 | + continue; | |
| 589 | + } | |
| 637 | 590 | |
| 638 | - if ($calc_cta) { | |
| 639 | - // set calculated cta week days | |
| 640 | - $cta_wdays = $calc_cta; | |
| 641 | - } | |
| 591 | + // calculate new rate for this derived rate plan | |
| 592 | + $rplan_derived_rate = $rate; | |
| 593 | + if (($derived_rate_plan['derived_data']['mode'] ?? 'discount') == 'discount') { | |
| 594 | + // discount rate | |
| 595 | + if (($derived_rate_plan['derived_data']['type'] ?? 'percent') == 'percent') { | |
| 596 | + // percent value | |
| 597 | + $rplan_derived_rate = $rplan_derived_rate * (100 - (float) ($derived_rate_plan['derived_data']['value'] ?? 0)) / 100; | |
| 598 | + } else { | |
| 599 | + // absolute value | |
| 600 | + $rplan_derived_rate -= (float) ($derived_rate_plan['derived_data']['value'] ?? 0); | |
| 601 | + } | |
| 602 | + } else { | |
| 603 | + // increase rate | |
| 604 | + if (($derived_rate_plan['derived_data']['type'] ?? 'percent') == 'percent') { | |
| 605 | + // percent value | |
| 606 | + $rplan_derived_rate = $rplan_derived_rate * (100 + (float) ($derived_rate_plan['derived_data']['value'] ?? 0)) / 100; | |
| 607 | + } else { | |
| 608 | + // absolute value | |
| 609 | + $rplan_derived_rate += (float) ($derived_rate_plan['derived_data']['value'] ?? 0); | |
| 610 | + } | |
| 611 | + } | |
| 642 | 612 | |
| 643 | - if ($calc_ctd) { | |
| 644 | - // set calculated ctd week days | |
| 645 | - $ctd_wdays = $calc_ctd; | |
| 646 | - } | |
| 647 | - } | |
| 613 | + if ($rplan_derived_rate < 0) { | |
| 614 | + // negative rates are not allowed | |
| 615 | + continue; | |
| 616 | + } | |
| 648 | 617 | |
| 649 | - if (!$rplan_info['is_derived']) { | |
| 650 | - // build the minimum stay restriction string, eventually inclusive of CTA/CTD rules | |
| 651 | - $vbo_min_los_str = $min_los; | |
| 652 | - if ($cta_wdays) { | |
| 653 | - // append cta instructions | |
| 654 | - $vbo_min_los_str .= 'CTA[' . implode(',', $cta_wdays) . ']'; | |
| 655 | - } | |
| 656 | - if ($ctd_wdays) { | |
| 657 | - // append ctd instructions | |
| 658 | - $vbo_min_los_str .= 'CTD[' . implode(',', $ctd_wdays) . ']'; | |
| 659 | - } | |
| 618 | + // make sure to apply rounding | |
| 619 | + $rplan_derived_rate = round($rplan_derived_rate, 2); | |
| 660 | 620 | |
| 661 | - // create the restriction in VBO (only for the parent rate since it will be a room-level restriction) | |
| 662 | - $restr_res = $vboConnector->createRestriction(date('Y-m-d H:i:s', $start_ts), date('Y-m-d H:i:s', $end_ts), [$id_room], [$vbo_min_los_str, $max_los]); | |
| 621 | + // push derived rate plan to the update pool | |
| 622 | + $rate_plans_pool[$derived_rate_plan['id']] = [ | |
| 623 | + 'id' => $derived_rate_plan['id'], | |
| 624 | + 'is_derived' => 1, | |
| 625 | + 'derived_data' => $derived_rate_plan['derived_data'], | |
| 626 | + 'rate' => $rplan_derived_rate, | |
| 627 | + ]; | |
| 628 | + } | |
| 663 | 629 | |
| 664 | - // update values for the response and the rest of the operations | |
| 665 | - if ($restr_res) { | |
| 666 | - $current_minlos = $min_los; | |
| 667 | - $current_maxlos = $max_los; | |
| 668 | - $current_cta = $cta_wdays; | |
| 669 | - $current_ctd = $ctd_wdays; | |
| 670 | - } else { | |
| 671 | - $current_minlos = 'e4j.error.' . $vboConnector->getError(); | |
| 672 | - } | |
| 673 | - } else { | |
| 674 | - // always update the min/max stay information | |
| 675 | - $current_minlos = $min_los; | |
| 676 | - $current_maxlos = $max_los; | |
| 677 | - $current_cta = $cta_wdays; | |
| 678 | - $current_ctd = $ctd_wdays; | |
| 679 | - } | |
| 680 | - } | |
| 630 | + // the newly applied rates | |
| 631 | + $newly_rates = []; | |
| 681 | 632 | |
| 682 | - // check if all dates involved share the same price | |
| 683 | - $common_rate = -1; | |
| 633 | + // apply the pricing modification to all the involved rate plans | |
| 634 | + foreach ($rate_plans_pool as $rplan_id => $rplan_info) { | |
| 635 | + // set rate plan ID | |
| 636 | + $now_id_price = $rplan_info['id']; | |
| 684 | 637 | |
| 685 | - // prepare output by re-calculating the new rates in real-time | |
| 686 | - while ($infostart[0] > 0 && $infostart[0] <= $end_ts) { | |
| 687 | - $tomorrow_ts = mktime(0, 0, 0, $infostart['mon'], ($infostart['mday'] + 1), $infostart['year']); | |
| 688 | - $today_tsin = VikBooking::getDateTimestamp(date($df, $infostart[0]), $checkin_h, $checkin_m); | |
| 689 | - $today_tsout = VikBooking::getDateTimestamp(date($df, $tomorrow_ts), $checkout_h, $checkout_m); | |
| 638 | + // set rate to apply to the current rate plan | |
| 639 | + $rate = $rplan_info['rate']; | |
| 690 | 640 | |
| 691 | - // apply seasonal rates by injecting the cached seasonal records | |
| 692 | - $tars = VikBooking::applySeasonsRoom([$roomrates], $today_tsin, $today_tsout, [], $cached_seasons); | |
| 641 | + // read the room rates for the lowest number of nights | |
| 642 | + $roomrates = $this->getBaseRoomRates($id_room, $now_id_price); | |
| 693 | 643 | |
| 694 | - // apply rounding to 2 decimals at most | |
| 695 | - $tars[0]['cost'] = round($tars[0]['cost'], 2); | |
| 644 | + if (!$roomrates) { | |
| 645 | + if ($rplan_info['is_derived']) { | |
| 646 | + // this is not the main rate plan we are updating | |
| 647 | + continue; | |
| 648 | + } | |
| 696 | 649 | |
| 697 | - if ($common_rate < 0) { | |
| 698 | - // save first-day common rate | |
| 699 | - $common_rate = $tars[0]['cost']; | |
| 700 | - } else { | |
| 701 | - // check if we've got a different rate for this day | |
| 702 | - if ($common_rate != $tars[0]['cost']) { | |
| 703 | - // freeze all controls, because this day has got a different cost | |
| 704 | - $common_rate = 0; | |
| 705 | - } | |
| 706 | - } | |
| 650 | + // terminate the process by throwing an error | |
| 651 | + throw new UnexpectedValueException('No rates found for the given room ID.', 400); | |
| 652 | + } | |
| 707 | 653 | |
| 708 | - $indkey = $infostart['mday'] . '-' . $infostart['mon'] . '-' . $infostart['year'] . '-' . $now_id_price; | |
| 709 | - $newly_rates[$indkey] = $tars[0]; | |
| 710 | - if (is_int($current_minlos) && $current_minlos > 0) { | |
| 711 | - $newly_rates[$indkey]['newminlos'] = $current_minlos; | |
| 712 | - } | |
| 654 | + // turn the rates list into a single-level array | |
| 655 | + $roomrates = $roomrates[0]; | |
| 713 | 656 | |
| 714 | - $infostart = getdate($tomorrow_ts); | |
| 715 | - } | |
| 657 | + // set rate plan name | |
| 658 | + $rate_plan_name = $roomrates['name']; | |
| 716 | 659 | |
| 717 | - // free memory up | |
| 718 | - unset($cached_seasons); | |
| 660 | + // dates involved | |
| 661 | + $start_ts = strtotime($from_date); | |
| 662 | + $end_ts = strtotime($to_date); | |
| 719 | 663 | |
| 720 | - /** | |
| 721 | - * Store a record in the rates flow for this rate modification on VBO. | |
| 722 | - */ | |
| 723 | - $rflow_handler = VikBooking::getRatesFlowInstance($anew = true); | |
| 724 | - if ($rflow_handler !== null) { | |
| 725 | - $rflow_record = $rflow_handler->getRecord() | |
| 726 | - ->setCreatedBy($this->get('_created_by', 'VBO')) | |
| 727 | - ->setDates($from_date, $to_date) | |
| 728 | - ->setVBORoomID($id_room) | |
| 729 | - ->setVBORatePlanID($now_id_price); | |
| 664 | + /** | |
| 665 | + * Preload seasonal records in favour of CPU usage, but against RAM usage. | |
| 666 | + * | |
| 667 | + * @since 1.17.2 (J) - 1.7.2 (WP) | |
| 668 | + * @since 1.18.5 (J) - 1.8.5 (WP) added support for week-day seasons cache. | |
| 669 | + */ | |
| 670 | + $cached_seasons = VikBooking::getDateSeasonRecords($start_ts, ($end_ts + ($checkout_h * 3600)), [$id_room]); | |
| 671 | + $cached_wdayseasons = VikBooking::getWdaySeasonRecords(); | |
| 730 | 672 | |
| 731 | - if ($rate > 0) { | |
| 732 | - // a new rate was set | |
| 733 | - $rflow_record->setNightlyFee($rate); | |
| 734 | - } | |
| 673 | + // read current room rates | |
| 674 | + $current_rates = []; | |
| 675 | + $infostart = getdate($start_ts); | |
| 676 | + while ($infostart[0] > 0 && $infostart[0] <= $end_ts) { | |
| 677 | + $tomorrow_ts = mktime(0, 0, 0, $infostart['mon'], ($infostart['mday'] + 1), $infostart['year']); | |
| 678 | + $today_tsin = VikBooking::getDateTimestamp(date($df, $infostart[0]), $checkin_h, $checkin_m); | |
| 679 | + $today_tsout = VikBooking::getDateTimestamp(date($df, $tomorrow_ts), $checkout_h, $checkout_m); | |
| 735 | 680 | |
| 736 | - if (is_int($current_minlos) && $current_minlos > 0) { | |
| 737 | - // push restriction extra data | |
| 738 | - $rflow_record->setRestrictions([ | |
| 739 | - 'minLOS' => $current_minlos, | |
| 740 | - 'maxLOS' => $current_maxlos, | |
| 741 | - 'cta' => (bool) $current_cta, | |
| 742 | - 'ctd' => (bool) $current_ctd, | |
| 743 | - ]); | |
| 744 | - } | |
| 681 | + // apply seasonal rates by injecting the cached seasonal records | |
| 682 | + $tars = VikBooking::applySeasonsRoom([$roomrates], $today_tsin, $today_tsout, [], $cached_seasons, $cached_wdayseasons); | |
| 745 | 683 | |
| 746 | - if (method_exists($rflow_record, 'setBaseFee')) { | |
| 747 | - $rflow_record->setBaseFee($roomrates['cost']); | |
| 748 | - } | |
| 684 | + // apply rounding to 2 decimals at most | |
| 685 | + $tars[0]['cost'] = round($tars[0]['cost'], 2); | |
| 749 | 686 | |
| 750 | - // push rates flow record | |
| 751 | - $rflow_handler->pushRecord($rflow_record); | |
| 687 | + $current_rates[(date('Y-m-d', $infostart[0]))] = $tars[0]; | |
| 752 | 688 | |
| 753 | - // store rates flow record | |
| 754 | - $rflow_handler->storeRecords(); | |
| 755 | - } | |
| 689 | + $infostart = getdate($tomorrow_ts); | |
| 690 | + } | |
| 756 | 691 | |
| 757 | - // check if restrictions can be transmitted to OTAs in case of no rate given | |
| 758 | - if ($upd_otas && $rate <= 0 && is_int($current_minlos) && $current_minlos > 0 && $common_rate > 0) { | |
| 759 | - // ensure OTAs will get the minimum stay by using the rate shared by all dates involved | |
| 760 | - $rate = $common_rate; | |
| 761 | - } | |
| 692 | + if (!$current_rates) { | |
| 693 | + if ($rplan_info['is_derived']) { | |
| 694 | + // this is not the main rate plan we are updating | |
| 695 | + continue; | |
| 696 | + } | |
| 762 | 697 | |
| 763 | - /** | |
| 764 | - * Channels will only be updated if a rate greater than zero was passed, | |
| 765 | - * and of course if the flag to the update the OTAs is enabled. Restrictions | |
| 766 | - * alone could not be pushed to the OTAs, or rate threshold errors would occur. | |
| 767 | - */ | |
| 768 | - if ($upd_otas && $rate > 0) { | |
| 769 | - // launch channel manager (from VBO, unlikely through the App, we update one rate plan per request) | |
| 770 | - $vcm_logos = VikBooking::getVcmChannelsLogo('', true); | |
| 771 | - $channels_updated = []; | |
| 772 | - $channels_bkdown = []; | |
| 773 | - $channels_success = []; | |
| 774 | - $channels_warnings = []; | |
| 775 | - $channels_errors = []; | |
| 698 | + // terminate the process by throwing an error | |
| 699 | + throw new UnexpectedValueException('No seasonal rates found for the given room ID.', 400); | |
| 700 | + } | |
| 776 | 701 | |
| 777 | - // load room details | |
| 778 | - $q = "SELECT `id`,`name`,`units` FROM `#__vikbooking_rooms` WHERE `id`=" . $id_room . ";"; | |
| 779 | - $dbo->setQuery($q); | |
| 780 | - $row = $dbo->loadAssoc(); | |
| 781 | - if ($row) { | |
| 782 | - $row['channels'] = []; | |
| 783 | - // get the mapped channels for this room | |
| 784 | - $q = "SELECT * FROM `#__vikchannelmanager_roomsxref` WHERE `idroomvb`=" . $id_room . ";"; | |
| 785 | - $dbo->setQuery($q); | |
| 786 | - foreach ($dbo->loadAssocList() as $ch_data) { | |
| 787 | - $row['channels'][$ch_data['idchannel']] = $ch_data; | |
| 788 | - } | |
| 789 | - } | |
| 702 | + $all_days = array_keys($current_rates); | |
| 703 | + $season_intervals = []; | |
| 704 | + $firstind = 0; | |
| 705 | + $firstdaycost = $current_rates[$all_days[0]]['cost']; | |
| 706 | + $nextdaycost = false; | |
| 707 | + for ($i = 1; $i < count($all_days); $i++) { | |
| 708 | + $ind = $all_days[$i]; | |
| 709 | + $nextdaycost = $current_rates[$ind]['cost']; | |
| 710 | + if ($firstdaycost != $nextdaycost) { | |
| 711 | + $interval = [ | |
| 712 | + 'from' => $all_days[$firstind], | |
| 713 | + 'to' => $all_days[($i - 1)], | |
| 714 | + 'cost' => $firstdaycost | |
| 715 | + ]; | |
| 716 | + $season_intervals[] = $interval; | |
| 717 | + $firstdaycost = $nextdaycost; | |
| 718 | + $firstind = $i; | |
| 719 | + } | |
| 720 | + } | |
| 721 | + if ($nextdaycost === false) { | |
| 722 | + $interval = [ | |
| 723 | + 'from' => $all_days[$firstind], | |
| 724 | + 'to' => $all_days[$firstind], | |
| 725 | + 'cost' => $firstdaycost | |
| 726 | + ]; | |
| 727 | + $season_intervals[] = $interval; | |
| 728 | + } elseif ($firstdaycost == $nextdaycost) { | |
| 729 | + $interval = [ | |
| 730 | + 'from' => $all_days[$firstind], | |
| 731 | + 'to' => $all_days[($i - 1)], | |
| 732 | + 'cost' => $firstdaycost | |
| 733 | + ]; | |
| 734 | + $season_intervals[] = $interval; | |
| 735 | + } | |
| 736 | + foreach ($season_intervals as $sik => $siv) { | |
| 737 | + if ((float)$siv['cost'] == $rate) { | |
| 738 | + unset($season_intervals[$sik]); | |
| 739 | + } | |
| 740 | + } | |
| 790 | 741 | |
| 791 | - if ($row && ($row['channels'] ?? [])) { | |
| 792 | - // this room is actually mapped to some channels supporting AV requests | |
| 793 | - // load the 'Bulk Action - Rates Upload' cache | |
| 794 | - $bulk_rates_cache = VikChannelManager::getBulkRatesCache(); | |
| 742 | + if (!$season_intervals) { | |
| 743 | + // do not raise this error if it was requested to set the restriction or to close a rate plan | |
| 744 | + if (!($min_los > 0) && !$close_rplan && !$upd_otas) { | |
| 745 | + if ($rplan_info['is_derived']) { | |
| 746 | + // this is not the main rate plan we are updating | |
| 747 | + continue; | |
| 748 | + } | |
| 795 | 749 | |
| 796 | - // check for custom ota pricing overrides | |
| 797 | - if ($ota_pricing) { | |
| 798 | - // build ota pricing overrides for each channel | |
| 799 | - $ota_pricing_overrides = []; | |
| 750 | + // terminate the process by throwing an error with code 409 (Conflict) for an easy identification | |
| 751 | + throw new RuntimeException('No rates modification needed with the given parameters.', 409); | |
| 752 | + } | |
| 753 | + } | |
| 800 | 754 | |
| 801 | - foreach ($ota_pricing as $ota_id => $pricing_command) { | |
| 802 | - if (!preg_match("/^(\+|\-)[0-9]+(\.|\,)?([0-9]+)?(\%|\*)$/", $pricing_command)) { | |
| 803 | - // invalid pricing instructions | |
| 804 | - continue; | |
| 805 | - } | |
| 755 | + if ($rate > 0) { | |
| 756 | + // make sure to set a cost greater than zero to avoid errors | |
| 757 | + foreach ($season_intervals as $sik => $siv) { | |
| 758 | + $first = strtotime($siv['from']); | |
| 759 | + $second = strtotime($siv['to']); | |
| 806 | 760 | |
| 807 | - // get pricing command | |
| 808 | - $rmodop = substr($pricing_command, 0, 1); | |
| 809 | - $rmodval = substr($pricing_command, -1, 1); | |
| 810 | - $rmodamount = (float) str_replace([$rmodop, $rmodval], '', $pricing_command); | |
| 761 | + if ($second > 0 && $second == $first) { | |
| 762 | + $second += 86399; | |
| 763 | + } | |
| 811 | 764 | |
| 812 | - if ($rmodamount <= 0) { | |
| 813 | - // invalid rate amount factor | |
| 814 | - continue; | |
| 815 | - } | |
| 765 | + if (!($second > $first)) { | |
| 766 | + unset($season_intervals[$sik]); | |
| 767 | + continue; | |
| 768 | + } | |
| 816 | 769 | |
| 817 | - // build ota pricing instructions | |
| 818 | - $ota_pricing_overrides[$ota_id] = [ | |
| 819 | - // modify rates | |
| 820 | - 1, | |
| 821 | - // increase or decrease | |
| 822 | - ($rmodop == '+' ? 1 : 0), | |
| 823 | - // amount | |
| 824 | - $rmodamount, | |
| 825 | - // percent or absolute | |
| 826 | - ($rmodval == '%' ? 1 : 0), | |
| 827 | - ]; | |
| 828 | - } | |
| 770 | + $baseone = getdate($first); | |
| 771 | + $basets = mktime(0, 0, 0, 1, 1, $baseone['year']); | |
| 772 | + $sfrom = $baseone[0] - $basets; | |
| 773 | + $basetwo = getdate($second); | |
| 774 | + $basets = mktime(0, 0, 0, 1, 1, $basetwo['year']); | |
| 775 | + $sto = $basetwo[0] - $basets; | |
| 829 | 776 | |
| 830 | - if ($ota_pricing_overrides && method_exists($vboConnector, 'setOTAPricingOverrides')) { | |
| 831 | - /** | |
| 832 | - * Set OTA pricing instruction overrides. | |
| 833 | - * | |
| 834 | - * @since 1.17.2 (J) - 1.7.2 (WP) | |
| 835 | - * | |
| 836 | - * @requires VCM >= 1.9.4 | |
| 837 | - */ | |
| 838 | - $vboConnector->setOTAPricingOverrides($ota_pricing_overrides); | |
| 839 | - } | |
| 840 | - } | |
| 777 | + // check leap year | |
| 778 | + if ($baseone['year'] % 4 == 0 && ($baseone['year'] % 100 != 0 || $baseone['year'] % 400 == 0)) { | |
| 779 | + $leapts = mktime(0, 0, 0, 2, 29, $baseone['year']); | |
| 780 | + if ($baseone[0] > $leapts) { | |
| 781 | + $sfrom -= 86400; | |
| 782 | + /** | |
| 783 | + * To avoid issue with leap years and dates near Feb 29th, we only reduce the seconds if these were reduced | |
| 784 | + * for the from-date of the seasons. Doing it just for the to-date in 2019 for 2020 (leap) produced invalid results. | |
| 785 | + */ | |
| 786 | + if ($basetwo['year'] % 4 == 0 && ($basetwo['year'] % 100 != 0 || $basetwo['year'] % 400 == 0)) { | |
| 787 | + $leapts = mktime(0, 0, 0, 2, 29, $basetwo['year']); | |
| 788 | + if ($basetwo[0] > $leapts) { | |
| 789 | + $sto -= date('d-m', $baseone[0]) != '31-12' && date('d-m', $basetwo[0]) == '31-12' ? 1 : 86400; | |
| 790 | + } | |
| 791 | + } | |
| 792 | + } | |
| 793 | + } | |
| 841 | 794 | |
| 842 | - // we update one rate plan per time, even though we could update all of them with a similar request | |
| 843 | - $rates_data = [ | |
| 844 | - [ | |
| 845 | - 'rate_id' => $now_id_price, | |
| 846 | - 'cost' => $rate, | |
| 847 | - ] | |
| 848 | - ]; | |
| 795 | + $tieyear = $baseone['year']; | |
| 796 | + $season_type = (float)$siv['cost'] > $rate ? "2" : "1"; | |
| 797 | + $season_diffcost = $season_type == "1" ? ($rate - (float)$siv['cost']) : ((float)$siv['cost'] - $rate); | |
| 798 | + $roomstr = "-" . $id_room . "-,"; | |
| 799 | + $season_name = date('Y-m-d H:i').' - '.substr($baseone['month'], 0, 3).' '.$baseone['mday'].($siv['from'] != $siv['to'] ? '/'.($baseone['month'] != $basetwo['month'] ? substr($basetwo['month'], 0, 3).' ' : '').$basetwo['mday'] : ''); | |
| 800 | + $pricestr = "-" . $now_id_price . "-,"; | |
| 849 | 801 | |
| 850 | - // build the array with the update details | |
| 851 | - $update_rows = []; | |
| 852 | - foreach ($rates_data as $rk => $rd) { | |
| 853 | - $node = $row; | |
| 854 | - $setminlos = ''; | |
| 855 | - $setmaxlos = ''; | |
| 856 | - $setctawdays = ''; | |
| 857 | - $setctdwdays = ''; | |
| 802 | + // build and store season record | |
| 803 | + $season_record = new stdClass; | |
| 804 | + $season_record->type = $season_type == "1" ? 1 : 2; | |
| 805 | + $season_record->from = $sfrom; | |
| 806 | + $season_record->to = $sto; | |
| 807 | + $season_record->diffcost = $season_diffcost; | |
| 808 | + $season_record->idrooms = $roomstr; | |
| 809 | + $season_record->spname = $season_name; | |
| 810 | + $season_record->wdays = ''; | |
| 811 | + $season_record->checkinincl = 0; | |
| 812 | + $season_record->val_pcent = 1; | |
| 813 | + $season_record->losoverride = ''; | |
| 814 | + $season_record->year = $tieyear; | |
| 815 | + $season_record->idprices = $pricestr; | |
| 858 | 816 | |
| 859 | - // pass the restrictions to the channels if specified | |
| 860 | - if (is_int($current_maxlos) && $current_maxlos > 0) { | |
| 861 | - // set max los first | |
| 862 | - $setmaxlos = $current_maxlos; | |
| 863 | - } | |
| 864 | - if (is_int($current_minlos) && $current_minlos > 0) { | |
| 865 | - // set min los after | |
| 866 | - $setminlos = $current_minlos; | |
| 867 | - // max los must have a length > 0 or min los won't be set | |
| 868 | - $setmaxlos = $setmaxlos ?: '0'; | |
| 869 | - // eavaluate whether cta/ctd week-days should be added | |
| 870 | - if ($current_cta) { | |
| 871 | - $setctawdays = 'CTA[' . implode(',', $current_cta) . ']'; | |
| 872 | - } | |
| 873 | - if ($current_ctd) { | |
| 874 | - $setctdwdays = 'CTD[' . implode(',', $current_ctd) . ']'; | |
| 875 | - } | |
| 876 | - } | |
| 817 | + $dbo->insertObject('#__vikbooking_seasons', $season_record, 'id'); | |
| 877 | 818 | |
| 878 | - // check for follow restriction flag in a derived rate plan | |
| 879 | - if ($rplan_info['is_derived'] && !((bool) ($rplan_info['derived_data']['follow_restr'] ?? 1))) { | |
| 880 | - // unset restriction values for only updating the rates | |
| 881 | - $setminlos = ''; | |
| 882 | - $setmaxlos = ''; | |
| 883 | - $setctawdays = ''; | |
| 884 | - $setctdwdays = ''; | |
| 885 | - } | |
| 819 | + /** | |
| 820 | + * Push the newly created season record to the list of preloaded records. | |
| 821 | + * | |
| 822 | + * @since 1.17.2 (J) - 1.7.2 (WP) | |
| 823 | + */ | |
| 824 | + $cached_seasons[] = (array) $season_record; | |
| 825 | + } | |
| 826 | + } | |
| 886 | 827 | |
| 887 | - // close rate plan (min or max los do not need to be set) | |
| 888 | - if ($close_rplan) { | |
| 889 | - // VikBookingConnector class in VCM requires the closure to be concatenated to maxlos | |
| 890 | - $setmaxlos .= 'closed'; | |
| 891 | - } | |
| 828 | + // calculate the involved dates | |
| 829 | + $start_ts = strtotime($from_date); | |
| 830 | + $end_ts = strtotime($to_date); | |
| 831 | + $infostart = getdate($start_ts); | |
| 832 | + $infoend = getdate($end_ts); | |
| 892 | 833 | |
| 893 | - // check bulk rates cache to see if the exact rate should be increased for the channels (the exact rate has already been set in VBO at this point of the code) | |
| 894 | - if (!$ota_pricing && ($bulk_rates_cache[$id_room][$rd['rate_id']] ?? null)) { | |
| 895 | - if ((int) $bulk_rates_cache[$id_room][$rd['rate_id']]['rmod'] > 0 && (float) $bulk_rates_cache[$id_room][$rd['rate_id']]['rmodamount'] > 0) { | |
| 896 | - if ((int) $bulk_rates_cache[$id_room][$rd['rate_id']]['rmodop'] > 0) { | |
| 897 | - // Increase rates | |
| 898 | - if ((int) $bulk_rates_cache[$id_room][$rd['rate_id']]['rmodval'] > 0) { | |
| 899 | - // Percentage charge | |
| 900 | - $rd['cost'] = $rd['cost'] * (100 + (float) $bulk_rates_cache[$id_room][$rd['rate_id']]['rmodamount']) / 100; | |
| 901 | - } else { | |
| 902 | - // Fixed charge | |
| 903 | - $rd['cost'] += (float) $bulk_rates_cache[$id_room][$rd['rate_id']]['rmodamount']; | |
| 904 | - } | |
| 905 | - } else { | |
| 906 | - // Lower rates | |
| 907 | - if ((int) $bulk_rates_cache[$id_room][$rd['rate_id']]['rmodval'] > 0) { | |
| 908 | - // Percentage discount | |
| 909 | - $disc_op = $rd['cost'] * (float) $bulk_rates_cache[$id_room][$rd['rate_id']]['rmodamount'] / 100; | |
| 910 | - $rd['cost'] -= $disc_op; | |
| 911 | - } else { | |
| 912 | - // Fixed discount | |
| 913 | - $rd['cost'] -= (float) $bulk_rates_cache[$id_room][$rd['rate_id']]['rmodamount']; | |
| 914 | - } | |
| 915 | - } | |
| 916 | - } | |
| 917 | - } | |
| 834 | + /** | |
| 835 | + * Restrictions can be set only if VCM is enabled because we use the Connector Class. | |
| 836 | + * It is allowed to set just a restriction for the website without any rate modification. | |
| 837 | + * OTAs instead would need a rate to be passed in order to eventually transmit the restrictions. | |
| 838 | + */ | |
| 839 | + $current_minlos = 0; | |
| 840 | + $current_maxlos = 0; | |
| 841 | + $current_cta = []; | |
| 842 | + $current_ctd = []; | |
| 843 | + $split_ct_nodes = []; | |
| 844 | + $vboConnector = null; | |
| 918 | 845 | |
| 919 | - // set rate inventory node(s) | |
| 920 | - if (count($split_ct_nodes) > 1 && $setminlos && ($current_cta || $current_ctd)) { | |
| 921 | - // there will be multiple OTA rate inventory nodes for better accuracy and avoid conflicts with cta/ctd rules | |
| 922 | - $node['ratesinventory'] = []; | |
| 846 | + if (method_exists('VikChannelManager', 'getVikBookingConnectorInstance')) { | |
| 847 | + // invoke the Connector for any update request | |
| 848 | + $vboConnector = VikChannelManager::getVikBookingConnectorInstance(); | |
| 849 | + // set the caller to 'VBO' to reduce the sleep time between the requests | |
| 850 | + $vboConnector->caller = 'VBO'; | |
| 851 | + } else { | |
| 852 | + // make sure the OTA update flag is off | |
| 853 | + $upd_otas = false; | |
| 854 | + } | |
| 923 | 855 | |
| 924 | - foreach ($split_ct_nodes as $split_ct_node) { | |
| 925 | - // calculate proper cta/ctd strings for this date interval | |
| 926 | - $setctawdays_range = $split_ct_node['cta'] ? 'CTA[' . implode(',', $split_ct_node['cta']) . ']' : ''; | |
| 927 | - $setctdwdays_range = $split_ct_node['ctd'] ? 'CTD[' . implode(',', $split_ct_node['ctd']) . ']' : ''; | |
| 856 | + // minimum length of stay is always necessary for creating a restriction even with other modifiers | |
| 857 | + if ($min_los > 0 && $vboConnector) { | |
| 858 | + // set the end date to the last second | |
| 859 | + $end_ts = mktime(23, 59, 59, $infoend['mon'], $infoend['mday'], $infoend['year']); | |
| 928 | 860 | |
| 929 | - // set rate inventory node for the properly calculated range of dates, cta and ctd rules | |
| 930 | - $node['ratesinventory'][] = implode('_', [ | |
| 931 | - $split_ct_node['from_dt'], | |
| 932 | - $split_ct_node['to_dt'], | |
| 933 | - $setminlos . $setctawdays_range . $setctdwdays_range, | |
| 934 | - $setmaxlos, | |
| 935 | - 1, | |
| 936 | - 2, | |
| 937 | - $rd['cost'], | |
| 938 | - 0, | |
| 939 | - ]); | |
| 940 | - } | |
| 941 | - } else { | |
| 942 | - // regular rate inventory node | |
| 943 | - $node['ratesinventory'] = [ | |
| 944 | - $from_date . '_' . $to_date . '_' . $setminlos . $setctawdays . $setctdwdays . '_' . $setmaxlos . '_1_2_' . $rd['cost'] . '_0', | |
| 945 | - ]; | |
| 946 | - } | |
| 861 | + /** | |
| 862 | + * Setting just a min los restriction may lift a previously defined CTA/CTD rule for the same dates. | |
| 863 | + * If merging restrictions is not disabled, and if no other modifiers are set (max los, cta, ctd), the | |
| 864 | + * system will automatically calculate the current modifiers in order to keep them on the OTAs. Such | |
| 865 | + * calculated restriction modifiers will not need to be created on VikBooking, but just passed to the OTAs. | |
| 866 | + * If conflicting restriction modifiers are detected, it is recommended to schedule an auto bulk action. | |
| 867 | + * | |
| 868 | + * @since 1.17.1 (J) - 1.7.1 (WP) | |
| 869 | + */ | |
| 870 | + if ($merge_restr && !$max_los && !$cta_wdays && !$ctd_wdays) { | |
| 871 | + // calculate the restriction modifiers beside the min los and get additional details | |
| 872 | + list($calc_maxlos, $calc_cta, $calc_ctd, $split_ct_nodes, $conflicts) = $this->calculateRestrictionModifiers($start_ts, $end_ts, $id_room); | |
| 947 | 873 | |
| 948 | - // set rate data | |
| 949 | - $node['pushdata'] = [ | |
| 950 | - 'pricetype' => $rd['rate_id'], | |
| 951 | - 'defrate' => $roomrates['cost'], | |
| 952 | - 'rplans' => [], | |
| 953 | - 'cur_rplans' => [], | |
| 954 | - 'rplanarimode' => [], | |
| 955 | - ]; | |
| 874 | + if ($calc_maxlos) { | |
| 875 | + // set calculated max los | |
| 876 | + $max_los = $calc_maxlos; | |
| 877 | + } | |
| 956 | 878 | |
| 957 | - // build push data for each channel rate plan according to the Bulk Rates Cache or to the OTA Pricing | |
| 958 | - if (($bulk_rates_cache[$id_room][$rd['rate_id']] ?? null)) { | |
| 959 | - // Bulk Rates Cache available for this room_id and rate_id | |
| 960 | - $node['pushdata']['rplans'] = $bulk_rates_cache[$id_room][$rd['rate_id']]['rplans']; | |
| 961 | - $node['pushdata']['cur_rplans'] = $bulk_rates_cache[$id_room][$rd['rate_id']]['cur_rplans']; | |
| 962 | - $node['pushdata']['rplanarimode'] = $bulk_rates_cache[$id_room][$rd['rate_id']]['rplanarimode']; | |
| 963 | - } | |
| 879 | + if ($calc_cta) { | |
| 880 | + // set calculated cta week days | |
| 881 | + $cta_wdays = $calc_cta; | |
| 882 | + } | |
| 964 | 883 | |
| 965 | - // check the channels mapped for this room and add what was not found in the Bulk Rates Cache, if anything | |
| 966 | - foreach ($node['channels'] as $idchannel => $ch_data) { | |
| 967 | - if (!isset($node['pushdata']['rplans'][$idchannel])) { | |
| 968 | - // this channel was not found in the Bulk Rates Cache. Read data from ota mapping pricing | |
| 969 | - $ota_mapping_pricing = json_decode($ch_data['otapricing'], true); | |
| 970 | - $ch_rplan_id = ''; | |
| 971 | - if (is_array($ota_mapping_pricing) && isset($ota_mapping_pricing['RatePlan'])) { | |
| 972 | - foreach ($ota_mapping_pricing['RatePlan'] as $rpkey => $rpv) { | |
| 973 | - // get the first key (rate plan ID) of the RatePlan array from OTA Pricing | |
| 974 | - $ch_rplan_id = $rpkey; | |
| 975 | - break; | |
| 976 | - } | |
| 977 | - } | |
| 884 | + if ($calc_ctd) { | |
| 885 | + // set calculated ctd week days | |
| 886 | + $ctd_wdays = $calc_ctd; | |
| 887 | + } | |
| 888 | + } | |
| 978 | 889 | |
| 979 | - // prevent channel from being updated if not directly involved | |
| 980 | - $is_secondary_rplan = $this->guessOTASecondaryRatePlan($idchannel, $roomrates, ($bulk_rates_cache[$id_room] ?? [])); | |
| 981 | - $is_mapped_rplan = $this->isOTARatePlanMapped($idchannel, $roomrates, ($bulk_rates_cache[$id_room] ?? [])); | |
| 982 | - $is_google_platform = defined('VikChannelManagerConfig::GOOGLEHOTEL') && $idchannel == VikChannelManagerConfig::GOOGLEHOTEL; | |
| 983 | - $is_google_platform = $is_google_platform || (defined('VikChannelManagerConfig::GOOGLEVR') && $idchannel == VikChannelManagerConfig::GOOGLEVR); | |
| 890 | + if (!$rplan_info['is_derived']) { | |
| 891 | + // build the minimum stay restriction string, eventually inclusive of CTA/CTD rules | |
| 892 | + $vbo_min_los_str = $min_los; | |
| 893 | + if ($cta_wdays) { | |
| 894 | + // append cta instructions | |
| 895 | + $vbo_min_los_str .= 'CTA[' . implode(',', $cta_wdays) . ']'; | |
| 896 | + } | |
| 897 | + if ($ctd_wdays) { | |
| 898 | + // append ctd instructions | |
| 899 | + $vbo_min_los_str .= 'CTD[' . implode(',', $ctd_wdays) . ']'; | |
| 900 | + } | |
| 984 | 901 | |
| 985 | - // prevent Airbnb from being updated if not for the main rate plan only | |
| 986 | - if (defined('VikChannelManagerConfig::AIRBNBAPI') && $idchannel == VikChannelManagerConfig::AIRBNBAPI) { | |
| 987 | - if ($rplan_info['is_derived'] || $is_secondary_rplan) { | |
| 988 | - // skip this channel from updating a derived/secondary rate plan that would not exist | |
| 989 | - $ch_rplan_id = ''; | |
| 990 | - } | |
| 991 | - } elseif (!$is_google_platform && !$is_mapped_rplan) { | |
| 992 | - // no bulk rates cache found for this channel, room and rate plan | |
| 993 | - $ch_rplan_id = ''; | |
| 994 | - } | |
| 902 | + // create the restriction in VBO (only for the parent rate since it will be a room-level restriction) | |
| 903 | + $restr_res = $vboConnector->createRestriction(date('Y-m-d H:i:s', $start_ts), date('Y-m-d H:i:s', $end_ts), [$id_room], [$vbo_min_los_str, $max_los]); | |
| 995 | 904 | |
| 996 | - // make sure an OTA rate plan ID was found | |
| 997 | - if (empty($ch_rplan_id)) { | |
| 998 | - // exclude this channel from being updated | |
| 999 | - unset($node['channels'][$idchannel]); | |
| 1000 | - continue; | |
| 1001 | - } | |
| 905 | + // update values for the response and the rest of the operations | |
| 906 | + if ($restr_res) { | |
| 907 | + $current_minlos = $min_los; | |
| 908 | + $current_maxlos = $max_los; | |
| 909 | + $current_cta = $cta_wdays; | |
| 910 | + $current_ctd = $ctd_wdays; | |
| 911 | + } else { | |
| 912 | + $current_minlos = 'e4j.error.' . $vboConnector->getError(); | |
| 913 | + } | |
| 914 | + } else { | |
| 915 | + // always update the min/max stay information | |
| 916 | + $current_minlos = $min_los; | |
| 917 | + $current_maxlos = $max_los; | |
| 918 | + $current_cta = $cta_wdays; | |
| 919 | + $current_ctd = $ctd_wdays; | |
| 920 | + } | |
| 921 | + } | |
| 1002 | 922 | |
| 1003 | - // set channel rate plan data | |
| 1004 | - $node['pushdata']['rplans'][$idchannel] = $ch_rplan_id; | |
| 1005 | - if ($idchannel == VikChannelManagerConfig::BOOKING) { | |
| 1006 | - // Default Pricing is used by default, when no data available | |
| 1007 | - $node['pushdata']['rplanarimode'][$idchannel] = 'person'; | |
| 1008 | - } | |
| 1009 | - } | |
| 1010 | - } | |
| 923 | + /** | |
| 924 | + * Ensure the room and rate plan effective min/max LOS is applied (room-rate level restrictions). | |
| 925 | + * | |
| 926 | + * @since 1.18.0 (J) - 1.8.0 (WP) | |
| 927 | + */ | |
| 928 | + if (is_int($current_minlos) && $current_minlos > 0) { | |
| 929 | + $effective_min_los = VBORoomHelper::calcEffectiveMinLOS($id_room, $now_id_price); | |
| 930 | + $effective_max_los = VBORoomHelper::calcEffectiveMaxLOS($id_room, $now_id_price); | |
| 1011 | 931 | |
| 1012 | - // push update node | |
| 1013 | - $update_rows[] = $node; | |
| 1014 | - } | |
| 932 | + // if we have a weekly rate plan, the minimum stay should always be forced regardless of room-level restrictions | |
| 933 | + if ($effective_min_los > 1 && $current_minlos < $effective_min_los) { | |
| 934 | + $current_minlos = $effective_min_los; | |
| 935 | + } | |
| 1015 | 936 | |
| 1016 | - // update rates on the various channels | |
| 1017 | - $channels_map = []; | |
| 1018 | - foreach ($update_rows as $update_row) { | |
| 1019 | - if (!$update_row['channels']) { | |
| 1020 | - // skip update for this room as no channels are involved | |
| 1021 | - continue; | |
| 1022 | - } | |
| 937 | + // check if we are dealing with a one-night rate plan | |
| 938 | + if ($effective_max_los === 1) { | |
| 939 | + // the maximum stay should always be 1 regardless of room-level restrictions, unless it's a parent rate | |
| 940 | + if (!$rplan_info['is_derived'] && $current_minlos > $effective_max_los) { | |
| 941 | + // we are requesting a higher minimum stay for this rate plan, which should rather be closed, but we assume | |
| 942 | + // the one-night rate plan is higher than any other rate plan, so it is safe to appy the requested min stay | |
| 943 | + $current_maxlos = $current_minlos; | |
| 944 | + } else { | |
| 945 | + // keep one night as minimum and maximum stay | |
| 946 | + $current_minlos = $effective_max_los; | |
| 947 | + $current_maxlos = $effective_max_los; | |
| 948 | + } | |
| 949 | + } | |
| 1023 | 950 | |
| 1024 | - // set channels updated | |
| 1025 | - foreach ($update_row['channels'] as $ch) { | |
| 1026 | - if (($channels_updated[$ch['idchannel']] ?? [])) { | |
| 1027 | - continue; | |
| 1028 | - } | |
| 1029 | - $channels_map[$ch['idchannel']] = ucfirst($ch['channel']); | |
| 1030 | - $ota_logo_url = is_object($vcm_logos) ? $vcm_logos->setProvenience($ch['channel'])->getLogoURL() : false; | |
| 1031 | - $channel_logo = $ota_logo_url !== false ? $ota_logo_url : ''; | |
| 1032 | - $channels_updated[$ch['idchannel']] = [ | |
| 1033 | - 'id' => $ch['idchannel'], | |
| 1034 | - 'name' => ucfirst($ch['channel']), | |
| 1035 | - 'logo' => $channel_logo | |
| 1036 | - ]; | |
| 1037 | - } | |
| 951 | + // ensure the minimum stay is less than or equal to the maximum stay | |
| 952 | + if (is_int($current_maxlos) && $current_maxlos > 0 && $current_minlos > $current_maxlos) { | |
| 953 | + $current_minlos = $current_maxlos; | |
| 954 | + } | |
| 955 | + } | |
| 1038 | 956 | |
| 1039 | - // prepare request data | |
| 1040 | - $channels_ids = array_keys($update_row['channels']); | |
| 1041 | - $channels_rplans = []; | |
| 1042 | - foreach ($channels_ids as $ch_id) { | |
| 1043 | - $ch_rplan = isset($update_row['pushdata']['rplans'][$ch_id]) ? $update_row['pushdata']['rplans'][$ch_id] : ''; | |
| 1044 | - $ch_rplan .= isset($update_row['pushdata']['rplanarimode'][$ch_id]) ? '='.$update_row['pushdata']['rplanarimode'][$ch_id] : ''; | |
| 1045 | - $ch_rplan .= isset($update_row['pushdata']['cur_rplans'][$ch_id]) && !empty($update_row['pushdata']['cur_rplans'][$ch_id]) ? ':'.$update_row['pushdata']['cur_rplans'][$ch_id] : ''; | |
| 1046 | - $channels_rplans[] = $ch_rplan; | |
| 1047 | - } | |
| 957 | + // check if all dates involved share the same price | |
| 958 | + $common_rate = -1; | |
| 1048 | 959 | |
| 1049 | - $channels = [ | |
| 1050 | - implode(',', $channels_ids) | |
| 1051 | - ]; | |
| 1052 | - $chrplans = [ | |
| 1053 | - implode(',', $channels_rplans) | |
| 1054 | - ]; | |
| 1055 | - $nodes = [ | |
| 1056 | - implode(';', $update_row['ratesinventory']) | |
| 1057 | - ]; | |
| 1058 | - $rooms = [$id_room]; | |
| 1059 | - $pushvars = [ | |
| 1060 | - implode(';', [ | |
| 1061 | - $update_row['pushdata']['pricetype'], | |
| 1062 | - $update_row['pushdata']['defrate'], | |
| 1063 | - ]) | |
| 1064 | - ]; | |
| 960 | + // prepare output by re-calculating the new rates in real-time | |
| 961 | + while ($infostart[0] > 0 && $infostart[0] <= $end_ts) { | |
| 962 | + $tomorrow_ts = mktime(0, 0, 0, $infostart['mon'], ($infostart['mday'] + 1), $infostart['year']); | |
| 963 | + $today_tsin = VikBooking::getDateTimestamp(date($df, $infostart[0]), $checkin_h, $checkin_m); | |
| 964 | + $today_tsout = VikBooking::getDateTimestamp(date($df, $tomorrow_ts), $checkout_h, $checkout_m); | |
| 1065 | 965 | |
| 1066 | - // send the request | |
| 1067 | - $result = $vboConnector->channelsRatesPush($channels, $chrplans, $nodes, $rooms, $pushvars); | |
| 1068 | - if ($vc_error = $vboConnector->getError(true)) { | |
| 1069 | - $channels_errors[] = $vc_error; | |
| 1070 | - continue; | |
| 1071 | - } | |
| 966 | + // apply seasonal rates by injecting the cached seasonal records | |
| 967 | + $tars = VikBooking::applySeasonsRoom([$roomrates], $today_tsin, $today_tsout, [], $cached_seasons, $cached_wdayseasons); | |
| 1072 | 968 | |
| 1073 | - // parse the channels update result and compose success, warnings, errors | |
| 1074 | - $result_pool = json_decode($result, true); | |
| 1075 | - foreach (($result_pool ?: []) as $rid => $ch_responses) { | |
| 1076 | - foreach ($ch_responses as $ch_id => $ch_res) { | |
| 1077 | - if ($ch_id == 'breakdown' || !is_numeric($ch_id)) { | |
| 1078 | - // get the rates/dates breakdown of the update request | |
| 1079 | - $bkdown = $ch_res; | |
| 1080 | - if (is_array($ch_res)) { | |
| 1081 | - $bkdown = ''; | |
| 1082 | - foreach ($ch_res as $bk => $bv) { | |
| 1083 | - $bkparts = explode('-', $bk); | |
| 1084 | - if (count($bkparts) == 6) { | |
| 1085 | - // breakdown key is usually composed of two dates in Y-m-d concatenated with another "-". | |
| 1086 | - $bkdown .= 'From ' . implode('-', array_slice($bkparts, 0, 3)) . ' - To ' . implode('-', array_slice($bkparts, 3, 3)) . ': ' . $bv . "\n"; | |
| 1087 | - } else { | |
| 1088 | - $bkdown .= $bk . ': ' . $bv . "\n"; | |
| 1089 | - } | |
| 1090 | - } | |
| 1091 | - // since the Connector does not return breakdown info about the restrictions, we concatenate the response here | |
| 1092 | - if ((int) $setminlos > 0) { | |
| 1093 | - $bkdown = rtrim($bkdown, "\n"); | |
| 1094 | - $bkdown .= ' - Min LOS: ' . $setminlos; | |
| 1095 | - if ((int) $setmaxlos > 0) { | |
| 1096 | - $bkdown .= ' - Max LOS: ' . $setmaxlos; | |
| 1097 | - } | |
| 1098 | - $bkdown_ctad_rules = []; | |
| 1099 | - if ($setctawdays && $current_cta) { | |
| 1100 | - $bkdown_ctad_rules[] = 'CTA: ' . implode(',', $this->weekDaysToShort($current_cta)); | |
| 1101 | - } | |
| 1102 | - if ($setctdwdays && $current_ctd) { | |
| 1103 | - $bkdown_ctad_rules[] = 'CTD: ' . implode(',', $this->weekDaysToShort($current_ctd)); | |
| 1104 | - } | |
| 1105 | - if ($bkdown_ctad_rules) { | |
| 1106 | - $bkdown .= ' [' . implode(' ', $bkdown_ctad_rules) . ']'; | |
| 1107 | - } | |
| 1108 | - $bkdown .= "\n"; | |
| 1109 | - } | |
| 1110 | - $bkdown = rtrim($bkdown, "\n"); | |
| 1111 | - } | |
| 1112 | - if (!isset($channels_bkdown[$ch_id])) { | |
| 1113 | - $channels_bkdown[$ch_id] = $bkdown; | |
| 1114 | - } else { | |
| 1115 | - $channels_bkdown[$ch_id] .= "\n".$bkdown; | |
| 1116 | - } | |
| 1117 | - continue; | |
| 1118 | - } | |
| 1119 | - $ch_id = (int)$ch_id; | |
| 1120 | - if (substr($ch_res, 0, 6) == 'e4j.OK') { | |
| 1121 | - // success | |
| 1122 | - if (!isset($channels_success[$ch_id])) { | |
| 1123 | - $channels_success[$ch_id] = $channels_map[$ch_id]; | |
| 1124 | - } | |
| 1125 | - } elseif (substr($ch_res, 0, 11) == 'e4j.warning') { | |
| 1126 | - // warning | |
| 1127 | - if (!isset($channels_warnings[$ch_id])) { | |
| 1128 | - $channels_warnings[$ch_id] = $channels_map[$ch_id].': '.str_replace('e4j.warning.', '', $ch_res); | |
| 1129 | - } else { | |
| 1130 | - $channels_warnings[$ch_id] .= "\n".str_replace('e4j.warning.', '', $ch_res); | |
| 1131 | - } | |
| 1132 | - // add the channel also to the successful list in case of Warning | |
| 1133 | - if (!isset($channels_success[$ch_id])) { | |
| 1134 | - $channels_success[$ch_id] = $channels_map[$ch_id]; | |
| 1135 | - } | |
| 1136 | - } elseif (substr($ch_res, 0, 9) == 'e4j.error') { | |
| 1137 | - // error | |
| 1138 | - if (!isset($channels_errors[$ch_id])) { | |
| 1139 | - $channels_errors[$ch_id] = $channels_map[$ch_id].': '.str_replace('e4j.error.', '', $ch_res); | |
| 1140 | - } else { | |
| 1141 | - $channels_errors[$ch_id] .= "\n".str_replace('e4j.error.', '', $ch_res); | |
| 1142 | - } | |
| 1143 | - } | |
| 1144 | - } | |
| 1145 | - } | |
| 1146 | - } | |
| 1147 | - } | |
| 969 | + // apply rounding to 2 decimals at most | |
| 970 | + $tars[0]['cost'] = round($tars[0]['cost'], 2); | |
| 1148 | 971 | |
| 1149 | - if ($channels_updated) { | |
| 1150 | - /** | |
| 1151 | - * We now support chained updates due to derived rate plans. | |
| 1152 | - * The "vcm" response property is now an array of objects with equal structure. | |
| 1153 | - * | |
| 1154 | - * @since 1.16.10 (J) - 1.6.10 (WP) | |
| 1155 | - */ | |
| 1156 | - if (!isset($newly_rates['vcm'])) { | |
| 1157 | - $newly_rates['vcm'] = []; | |
| 1158 | - } | |
| 972 | + if ($common_rate < 0) { | |
| 973 | + // save first-day common rate | |
| 974 | + $common_rate = $tars[0]['cost']; | |
| 975 | + } else { | |
| 976 | + // check if we've got a different rate for this day | |
| 977 | + if ($common_rate != $tars[0]['cost']) { | |
| 978 | + // freeze all controls, because this day has got a different cost | |
| 979 | + $common_rate = 0; | |
| 980 | + } | |
| 981 | + } | |
| 1159 | 982 | |
| 1160 | - // build channels response data for the current rate plan | |
| 1161 | - $channels_response_data = [ | |
| 1162 | - 'rplan_id' => $now_id_price, | |
| 1163 | - 'rplan_name' => $rate_plan_name, | |
| 1164 | - 'is_derived' => $rplan_info['is_derived'], | |
| 1165 | - 'channels_updated' => $channels_updated, | |
| 1166 | - ]; | |
| 983 | + $indkey = $infostart['mday'] . '-' . $infostart['mon'] . '-' . $infostart['year'] . '-' . $now_id_price; | |
| 984 | + $newly_rates[$indkey] = $tars[0]; | |
| 985 | + if (is_int($current_minlos) && $current_minlos > 0) { | |
| 986 | + $newly_rates[$indkey]['newminlos'] = $current_minlos; | |
| 987 | + } | |
| 1167 | 988 | |
| 1168 | - // set these property only if not empty | |
| 1169 | - if ($channels_bkdown) { | |
| 1170 | - $channels_response_data['channels_bkdown'] = $channels_bkdown['breakdown']; | |
| 1171 | - } | |
| 1172 | - if ($channels_success) { | |
| 1173 | - $channels_response_data['channels_success'] = $channels_success; | |
| 1174 | - } | |
| 1175 | - if ($channels_warnings) { | |
| 1176 | - $channels_response_data['channels_warnings'] = $channels_warnings; | |
| 1177 | - // cache channel warnings | |
| 1178 | - $this->channel_warnings = $channels_warnings; | |
| 1179 | - } | |
| 1180 | - if ($channels_errors) { | |
| 1181 | - $channels_response_data['channels_errors'] = $channels_errors; | |
| 1182 | - // cache channel errors | |
| 1183 | - $this->channel_errors = $channels_errors; | |
| 1184 | - } | |
| 989 | + $infostart = getdate($tomorrow_ts); | |
| 990 | + } | |
| 1185 | 991 | |
| 1186 | - // push channels response data to the pool | |
| 1187 | - $newly_rates['vcm'][] = $channels_response_data; | |
| 992 | + // free memory up | |
| 993 | + unset($cached_seasons, $cached_wdayseasons); | |
| 1188 | 994 | |
| 1189 | - // cache the channels updated list by eventually merging what was already set | |
| 1190 | - foreach ($channels_updated as $idch => $chinfo) { | |
| 1191 | - $this->channels_updated_list[$idch] = $chinfo; | |
| 1192 | - } | |
| 995 | + /** | |
| 996 | + * Store a record in the rates flow for this rate modification on VBO. | |
| 997 | + */ | |
| 998 | + $rflow_handler = VikBooking::getRatesFlowInstance($anew = true); | |
| 999 | + if ($rflow_handler !== null) { | |
| 1000 | + $rflow_record = $rflow_handler->getRecord() | |
| 1001 | + ->setCreatedBy($this->get('_created_by', 'VBO')) | |
| 1002 | + ->setDates($from_date, $to_date) | |
| 1003 | + ->setVBORoomID($id_room) | |
| 1004 | + ->setVBORatePlanID($now_id_price); | |
| 1193 | 1005 | |
| 1194 | - // check for possible conflicts | |
| 1195 | - if (!$rplan_info['is_derived'] && !$split_ct_nodes && ($conflicts ?? false) === true) { | |
| 1196 | - // trigger an automatic bulk action for uploading the rates to ensure a full refresh | |
| 1197 | - VikChannelManager::autoBulkActions([ | |
| 1198 | - 'from_date' => $from_date, | |
| 1199 | - 'to_date' => $to_date, | |
| 1200 | - 'forced_rooms' => [$id_room], | |
| 1201 | - 'update' => 'rates', | |
| 1202 | - ]); | |
| 1203 | - } | |
| 1204 | - } | |
| 1205 | - } | |
| 1206 | - } | |
| 1006 | + if ($rate > 0) { | |
| 1007 | + // a new rate was set | |
| 1008 | + $rflow_record->setNightlyFee($rate); | |
| 1009 | + } | |
| 1207 | 1010 | |
| 1208 | - return $newly_rates; | |
| 1209 | - } | |
| 1011 | + if (is_int($current_minlos) && $current_minlos > 0) { | |
| 1012 | + // push restriction extra data | |
| 1013 | + $rflow_record->setRestrictions([ | |
| 1014 | + 'minLOS' => $current_minlos, | |
| 1015 | + 'maxLOS' => $current_maxlos, | |
| 1016 | + 'cta' => (bool) $current_cta, | |
| 1017 | + 'ctd' => (bool) $current_ctd, | |
| 1018 | + ]); | |
| 1019 | + } | |
| 1210 | 1020 | |
| 1211 | - /** | |
| 1212 | - * Returns the information about the channels updated with | |
| 1213 | - * the last rates/restrictions modification request, if any. | |
| 1214 | - * | |
| 1215 | - * @return array | |
| 1216 | - */ | |
| 1217 | - public function getChannelsUpdated() | |
| 1218 | - { | |
| 1219 | - $channel_details = []; | |
| 1021 | + if (method_exists($rflow_record, 'setBaseFee')) { | |
| 1022 | + $rflow_record->setBaseFee($roomrates['cost']); | |
| 1023 | + } | |
| 1220 | 1024 | |
| 1221 | - $vcm_logos = VikBooking::getVcmChannelsLogo('', true); | |
| 1025 | + // push rates flow record | |
| 1026 | + $rflow_handler->pushRecord($rflow_record); | |
| 1222 | 1027 | |
| 1223 | - foreach ($this->channels_updated_list as $idchannel => $channel_data) { | |
| 1224 | - $small_logo_url = $vcm_logos ? $vcm_logos->setProvenience(strtolower($channel_data['name']))->getSmallLogoURL() : ''; | |
| 1225 | - $tiny_logo_url = $vcm_logos ? $vcm_logos->setProvenience(strtolower($channel_data['name']))->getTinyLogoURL() : ''; | |
| 1028 | + // store rates flow record | |
| 1029 | + $rflow_handler->storeRecords(); | |
| 1030 | + } | |
| 1226 | 1031 | |
| 1227 | - // set channel details | |
| 1228 | - $channel_details[] = [ | |
| 1229 | - 'id' => $channel_data['id'], | |
| 1230 | - 'name' => $channel_data['name'], | |
| 1231 | - 'logo' => $channel_data['logo'], | |
| 1232 | - 'small_logo' => $small_logo_url, | |
| 1233 | - 'tiny_logo' => $tiny_logo_url, | |
| 1234 | - ]; | |
| 1235 | - } | |
| 1032 | + // check if restrictions can be transmitted to OTAs in case of no rate given | |
| 1033 | + if ($upd_otas && $rate <= 0 && is_int($current_minlos) && $current_minlos > 0 && $common_rate > 0) { | |
| 1034 | + // ensure OTAs will get the minimum stay by using the rate shared by all dates involved | |
| 1035 | + $rate = $common_rate; | |
| 1036 | + } | |
| 1236 | 1037 | |
| 1237 | - return $channel_details; | |
| 1238 | - } | |
| 1038 | + /** | |
| 1039 | + * Channels will only be updated if a rate greater than zero was passed, | |
| 1040 | + * and of course if the flag to the update the OTAs is enabled. Restrictions | |
| 1041 | + * alone could not be pushed to the OTAs, or rate threshold errors would occur. | |
| 1042 | + */ | |
| 1043 | + if ($upd_otas && $rate > 0) { | |
| 1044 | + // launch channel manager (from VBO, unlikely through the App, we update one rate plan per request) | |
| 1045 | + $vcm_logos = VikBooking::getVcmChannelsLogo('', true); | |
| 1046 | + $channels_updated = []; | |
| 1047 | + $channels_bkdown = []; | |
| 1048 | + $channels_success = []; | |
| 1049 | + $channels_warnings = []; | |
| 1050 | + $channels_errors = []; | |
| 1239 | 1051 | |
| 1240 | - /** | |
| 1241 | - * Returns the associative list of channel warning messages. | |
| 1242 | - * | |
| 1243 | - * @return array | |
| 1244 | - */ | |
| 1245 | - public function getChannelWarnings() | |
| 1246 | - { | |
| 1247 | - return $this->channel_warnings; | |
| 1248 | - } | |
| 1052 | + // load room details | |
| 1053 | + $q = "SELECT `id`,`name`,`units` FROM `#__vikbooking_rooms` WHERE `id`=" . $id_room . ";"; | |
| 1054 | + $dbo->setQuery($q); | |
| 1055 | + $row = $dbo->loadAssoc(); | |
| 1056 | + if ($row) { | |
| 1057 | + $row['channels'] = []; | |
| 1058 | + // get the mapped channels for this room | |
| 1059 | + $q = "SELECT * FROM `#__vikchannelmanager_roomsxref` WHERE `idroomvb`=" . $id_room . ";"; | |
| 1060 | + $dbo->setQuery($q); | |
| 1061 | + foreach ($dbo->loadAssocList() as $ch_data) { | |
| 1062 | + $row['channels'][$ch_data['idchannel']] = $ch_data; | |
| 1063 | + } | |
| 1064 | + } | |
| 1249 | 1065 | |
| 1250 | - /** | |
| 1251 | - * Returns the associative list of channel error messages. | |
| 1252 | - * | |
| 1253 | - * @return array | |
| 1254 | - */ | |
| 1255 | - public function getChannelErrors() | |
| 1256 | - { | |
| 1257 | - return $this->channel_errors; | |
| 1258 | - } | |
| 1066 | + if ($row && ($row['channels'] ?? [])) { | |
| 1067 | + // this room is actually mapped to some channels supporting AV requests | |
| 1068 | + // load the 'Bulk Action - Rates Upload' cache | |
| 1069 | + $bulk_rates_cache = VikChannelManager::getBulkRatesCache(); | |
| 1259 | 1070 | |
| 1260 | - /** | |
| 1261 | - * Calculates the restriction modifiers for a specific range of dates and room. | |
| 1262 | - * | |
| 1263 | - * @param int $start_ts The date range start date timestamp. | |
| 1264 | - * @param int $end_ts The date range end date timestamp. | |
| 1265 | - * @param int $room_id The VikBooking room ID. | |
| 1266 | - * | |
| 1267 | - * @return array Numeric list of modifiers (Max LOS, CTA, CTD, split nodes, conflicts). | |
| 1268 | - * | |
| 1269 | - * @since 1.17.1 (J) - 1.7.1 (WP) | |
| 1270 | - */ | |
| 1271 | - public function calculateRestrictionModifiers($start_ts, $end_ts, $room_id) | |
| 1272 | - { | |
| 1273 | - // build cache signature | |
| 1274 | - $from_dt = date('Y-m-d', $start_ts); | |
| 1275 | - $to_dt = date('Y-m-d', $end_ts); | |
| 1276 | - $signature = $from_dt . '_' . $to_dt . '_' . $room_id; | |
| 1071 | + // check for custom ota pricing overrides | |
| 1072 | + if ($ota_pricing) { | |
| 1073 | + // build ota pricing overrides for each channel | |
| 1074 | + $ota_pricing_overrides = []; | |
| 1277 | 1075 | |
| 1278 | - if (!isset($this->cached_restrictions[$signature])) { | |
| 1279 | - // load and cache restrictions | |
| 1280 | - $this->cached_restrictions[$signature] = VikBooking::loadRestrictions(true, [$room_id]); | |
| 1281 | - } | |
| 1076 | + foreach ($ota_pricing as $ota_id => $pricing_command) { | |
| 1077 | + if (!preg_match("/^(\+|\-)[0-9]+(\.|\,)?([0-9]+)?(\%|\*)$/", $pricing_command)) { | |
| 1078 | + // invalid pricing instructions | |
| 1079 | + continue; | |
| 1080 | + } | |
| 1282 | 1081 | |
| 1283 | - // build default return values | |
| 1284 | - $calc_maxlos = 0; | |
| 1285 | - $calc_cta = []; | |
| 1286 | - $calc_ctd = []; | |
| 1287 | - $split_ct_nodes = []; | |
| 1288 | - $conflicts = false; | |
| 1082 | + // get pricing command | |
| 1083 | + $rmodop = substr($pricing_command, 0, 1); | |
| 1084 | + $rmodval = substr($pricing_command, -1, 1); | |
| 1085 | + $rmodamount = (float) str_replace([$rmodop, $rmodval], '', $pricing_command); | |
| 1289 | 1086 | |
| 1290 | - if (!($this->cached_restrictions[$signature] ?? [])) { | |
| 1291 | - // do not proceed as nothing would be found | |
| 1292 | - return [ | |
| 1293 | - $calc_maxlos, | |
| 1294 | - $calc_cta, | |
| 1295 | - $calc_ctd, | |
| 1296 | - $split_ct_nodes, | |
| 1297 | - $conflicts, | |
| 1298 | - ]; | |
| 1299 | - } | |
| 1087 | + if ($rmodamount <= 0) { | |
| 1088 | + // invalid rate amount factor | |
| 1089 | + continue; | |
| 1090 | + } | |
| 1300 | 1091 | |
| 1301 | - // list of week days involved | |
| 1302 | - $wdays_involved = []; | |
| 1092 | + // build ota pricing instructions | |
| 1093 | + $ota_pricing_overrides[$ota_id] = [ | |
| 1094 | + // modify rates | |
| 1095 | + 1, | |
| 1096 | + // increase or decrease | |
| 1097 | + ($rmodop == '+' ? 1 : 0), | |
| 1098 | + // amount | |
| 1099 | + $rmodamount, | |
| 1100 | + // percent or absolute | |
| 1101 | + ($rmodval == '%' ? 1 : 0), | |
| 1102 | + ]; | |
| 1103 | + } | |
| 1303 | 1104 | |
| 1304 | - // loop through the requested range of dates | |
| 1305 | - $infostart = getdate($start_ts); | |
| 1306 | - $infofirst = $infostart; | |
| 1307 | - while ($infostart[0] > 0 && $infostart[0] <= $end_ts) { | |
| 1308 | - // calculate timestamps | |
| 1309 | - $tomorrow_ts = mktime(0, 0, 0, $infostart['mon'], ($infostart['mday'] + 1), $infostart['year']); | |
| 1310 | - $today_mid_ts = mktime(0, 0, 0, $infostart['mon'], $infostart['mday'], $infostart['year']); | |
| 1105 | + if ($ota_pricing_overrides && method_exists($vboConnector, 'setOTAPricingOverrides')) { | |
| 1106 | + /** | |
| 1107 | + * Set OTA pricing instruction overrides. | |
| 1108 | + * | |
| 1109 | + * @since 1.17.2 (J) - 1.7.2 (WP) | |
| 1110 | + * | |
| 1111 | + * @requires VCM >= 1.9.4 | |
| 1112 | + */ | |
| 1113 | + $vboConnector->setOTAPricingOverrides($ota_pricing_overrides); | |
| 1114 | + } | |
| 1115 | + } | |
| 1311 | 1116 | |
| 1312 | - // set week-day involved | |
| 1313 | - $wdays_involved[] = $infostart['wday']; | |
| 1117 | + // we update one rate plan per time, even though we could update all of them with a similar request | |
| 1118 | + $rates_data = [ | |
| 1119 | + [ | |
| 1120 | + 'rate_id' => $now_id_price, | |
| 1121 | + 'cost' => $rate, | |
| 1122 | + ] | |
| 1123 | + ]; | |
| 1314 | 1124 | |
| 1315 | - // calculate restrictions | |
| 1316 | - $restrictions = VikBooking::parseSeasonRestrictions($today_mid_ts, $tomorrow_ts, 1, $this->cached_restrictions[$signature] ?? []); | |
| 1125 | + // build the array with the update details | |
| 1126 | + $update_rows = []; | |
| 1127 | + foreach ($rates_data as $rk => $rd) { | |
| 1128 | + $node = $row; | |
| 1129 | + $setminlos = ''; | |
| 1130 | + $setmaxlos = ''; | |
| 1131 | + $setctawdays = ''; | |
| 1132 | + $setctdwdays = ''; | |
| 1317 | 1133 | |
| 1318 | - // check for max LOS | |
| 1319 | - if ($restrictions['maxlos'] ?? null) { | |
| 1320 | - if (!$calc_maxlos) { | |
| 1321 | - $calc_maxlos = (int) $restrictions['maxlos']; | |
| 1322 | - } | |
| 1323 | - } | |
| 1134 | + // pass the restrictions to the channels if specified | |
| 1135 | + if (is_int($current_maxlos) && $current_maxlos > 0) { | |
| 1136 | + // set max los first | |
| 1137 | + $setmaxlos = $current_maxlos; | |
| 1138 | + } | |
| 1139 | + if (is_int($current_minlos) && $current_minlos > 0) { | |
| 1140 | + // set min los after | |
| 1141 | + $setminlos = $current_minlos; | |
| 1142 | + // max los must have a length > 0 or min los won't be set | |
| 1143 | + $setmaxlos = $setmaxlos ?: '0'; | |
| 1144 | + // eavaluate whether cta/ctd week-days should be added | |
| 1145 | + if ($current_cta) { | |
| 1146 | + $setctawdays = 'CTA[' . implode(',', $current_cta) . ']'; | |
| 1147 | + } | |
| 1148 | + if ($current_ctd) { | |
| 1149 | + $setctdwdays = 'CTD[' . implode(',', $current_ctd) . ']'; | |
| 1150 | + } | |
| 1151 | + } | |
| 1324 | 1152 | |
| 1325 | - // check for CTA week-days | |
| 1326 | - if ($restrictions['cta'] ?? []) { | |
| 1327 | - if (!$calc_cta) { | |
| 1328 | - $calc_cta = (array) $restrictions['cta']; | |
| 1329 | - $conflicts = $conflicts || !in_array($infostart['wday'], (array) $restrictions['cta']); | |
| 1330 | - } else { | |
| 1331 | - $conflicts = $conflicts || $calc_cta != (array) $restrictions['cta']; | |
| 1332 | - } | |
| 1333 | - } elseif ($calc_cta) { | |
| 1334 | - $conflicts = true; | |
| 1335 | - } | |
| 1153 | + // check for follow restriction flag in a derived rate plan | |
| 1154 | + if ($rplan_info['is_derived'] && !((bool) ($rplan_info['derived_data']['follow_restr'] ?? 1))) { | |
| 1155 | + // unset restriction values for only updating the rates | |
| 1156 | + $setminlos = ''; | |
| 1157 | + $setmaxlos = ''; | |
| 1158 | + $setctawdays = ''; | |
| 1159 | + $setctdwdays = ''; | |
| 1160 | + } | |
| 1336 | 1161 | |
| 1337 | - // check for CTD week-days | |
| 1338 | - if ($restrictions['ctd'] ?? []) { | |
| 1339 | - if (!$calc_ctd) { | |
| 1340 | - $calc_ctd = (array) $restrictions['ctd']; | |
| 1341 | - $conflicts = $conflicts || !in_array($infostart['wday'], (array) $restrictions['ctd']); | |
| 1342 | - } else { | |
| 1343 | - $conflicts = $conflicts || $calc_ctd != (array) $restrictions['ctd']; | |
| 1344 | - } | |
| 1345 | - } elseif ($calc_ctd) { | |
| 1346 | - $conflicts = true; | |
| 1347 | - } | |
| 1162 | + // close rate plan (min or max los do not need to be set) | |
| 1163 | + if ($close_rplan) { | |
| 1164 | + // VikBookingConnector class in VCM requires the closure to be concatenated to maxlos | |
| 1165 | + $setmaxlos .= 'closed'; | |
| 1166 | + } | |
| 1348 | 1167 | |
| 1349 | - // go to next day | |
| 1350 | - $infostart = getdate($tomorrow_ts); | |
| 1351 | - } | |
| 1168 | + // check bulk rates cache to see if the exact rate should be increased for the channels (the exact rate has already been set in VBO at this point of the code) | |
| 1169 | + if (!$ota_pricing && ($bulk_rates_cache[$id_room][$rd['rate_id']] ?? null)) { | |
| 1170 | + if ((int) $bulk_rates_cache[$id_room][$rd['rate_id']]['rmod'] > 0 && (float) $bulk_rates_cache[$id_room][$rd['rate_id']]['rmodamount'] > 0) { | |
| 1171 | + if ((int) $bulk_rates_cache[$id_room][$rd['rate_id']]['rmodop'] > 0) { | |
| 1172 | + // Increase rates | |
| 1173 | + if ((int) $bulk_rates_cache[$id_room][$rd['rate_id']]['rmodval'] > 0) { | |
| 1174 | + // Percentage charge | |
| 1175 | + $rd['cost'] = $rd['cost'] * (100 + (float) $bulk_rates_cache[$id_room][$rd['rate_id']]['rmodamount']) / 100; | |
| 1176 | + } else { | |
| 1177 | + // Fixed charge | |
| 1178 | + $rd['cost'] += (float) $bulk_rates_cache[$id_room][$rd['rate_id']]['rmodamount']; | |
| 1179 | + } | |
| 1180 | + } else { | |
| 1181 | + // Lower rates | |
| 1182 | + if ((int) $bulk_rates_cache[$id_room][$rd['rate_id']]['rmodval'] > 0) { | |
| 1183 | + // Percentage discount | |
| 1184 | + $disc_op = $rd['cost'] * (float) $bulk_rates_cache[$id_room][$rd['rate_id']]['rmodamount'] / 100; | |
| 1185 | + $rd['cost'] -= $disc_op; | |
| 1186 | + } else { | |
| 1187 | + // Fixed discount | |
| 1188 | + $rd['cost'] -= (float) $bulk_rates_cache[$id_room][$rd['rate_id']]['rmodamount']; | |
| 1189 | + } | |
| 1190 | + } | |
| 1191 | + } | |
| 1192 | + } | |
| 1352 | 1193 | |
| 1353 | - if ($calc_cta) { | |
| 1354 | - // ensure it's a list of integers representing week-days | |
| 1355 | - $calc_cta = array_map(function($w) { | |
| 1356 | - return (int) str_replace('-', '', (string) $w); | |
| 1357 | - }, $calc_cta); | |
| 1194 | + // set rate inventory node(s) | |
| 1195 | + if (count($split_ct_nodes) > 1 && $setminlos && ($current_cta || $current_ctd)) { | |
| 1196 | + // there will be multiple OTA rate inventory nodes for better accuracy and avoid conflicts with cta/ctd rules | |
| 1197 | + $node['ratesinventory'] = []; | |
| 1358 | 1198 | |
| 1359 | - if (count($wdays_involved) < 7) { | |
| 1360 | - // filter the CTA week days according to the nights involved | |
| 1361 | - $calc_cta = array_filter($calc_cta, function($w) use ($wdays_involved) { | |
| 1362 | - return in_array($w, $wdays_involved); | |
| 1363 | - }); | |
| 1364 | - } | |
| 1365 | - } | |
| 1199 | + foreach ($split_ct_nodes as $split_ct_node) { | |
| 1200 | + // calculate proper cta/ctd strings for this date interval | |
| 1201 | + $setctawdays_range = $split_ct_node['cta'] ? 'CTA[' . implode(',', $split_ct_node['cta']) . ']' : ''; | |
| 1202 | + $setctdwdays_range = $split_ct_node['ctd'] ? 'CTD[' . implode(',', $split_ct_node['ctd']) . ']' : ''; | |
| 1366 | 1203 | |
| 1367 | - if ($calc_ctd) { | |
| 1368 | - // ensure it's a list of integers representing week-days | |
| 1369 | - $calc_ctd = array_map(function($w) { | |
| 1370 | - return (int) str_replace('-', '', (string) $w); | |
| 1371 | - }, $calc_ctd); | |
| 1204 | + // set rate inventory node for the properly calculated range of dates, cta and ctd rules | |
| 1205 | + $node['ratesinventory'][] = implode('_', [ | |
| 1206 | + $split_ct_node['from_dt'], | |
| 1207 | + $split_ct_node['to_dt'], | |
| 1208 | + $setminlos . $setctawdays_range . $setctdwdays_range, | |
| 1209 | + $setmaxlos, | |
| 1210 | + 1, | |
| 1211 | + 2, | |
| 1212 | + $rd['cost'], | |
| 1213 | + 0, | |
| 1214 | + ]); | |
| 1215 | + } | |
| 1216 | + } else { | |
| 1217 | + // regular rate inventory node | |
| 1218 | + $node['ratesinventory'] = [ | |
| 1219 | + $from_date . '_' . $to_date . '_' . $setminlos . $setctawdays . $setctdwdays . '_' . $setmaxlos . '_1_2_' . $rd['cost'] . '_0', | |
| 1220 | + ]; | |
| 1221 | + } | |
| 1372 | 1222 | |
| 1373 | - if (count($wdays_involved) < 7) { | |
| 1374 | - // filter the CTD week days according to the nights involved | |
| 1375 | - $calc_ctd = array_filter($calc_ctd, function($w) use ($wdays_involved) { | |
| 1376 | - return in_array($w, $wdays_involved); | |
| 1377 | - }); | |
| 1378 | - } | |
| 1379 | - } | |
| 1223 | + // set rate data | |
| 1224 | + $node['pushdata'] = [ | |
| 1225 | + 'pricetype' => $rd['rate_id'], | |
| 1226 | + 'defrate' => $roomrates['cost'], | |
| 1227 | + 'rplans' => [], | |
| 1228 | + 'cur_rplans' => [], | |
| 1229 | + 'rplanarimode' => [], | |
| 1230 | + ]; | |
| 1380 | 1231 | |
| 1381 | - if ($conflicts && count($wdays_involved) < 7 && ($calc_cta || $calc_ctd)) { | |
| 1382 | - // conflicting cta/ctd restrictions for a range of dates less than a week may be split into multiple OTA nodes | |
| 1383 | - $day_ctad_list = []; | |
| 1384 | - for ($w = 0; $w < count($wdays_involved); $w++) { | |
| 1385 | - // build day individual cta/ctd restrictions | |
| 1386 | - $split_ts = mktime(0, 0, 0, $infofirst['mon'], $infofirst['mday'] + $w, $infofirst['year']); | |
| 1387 | - $day_ctad_list[] = [ | |
| 1388 | - 'ts' => $split_ts, | |
| 1389 | - 'day' => date('Y-m-d', $split_ts), | |
| 1390 | - 'cta' => in_array($wdays_involved[$w], $calc_cta) ? $calc_cta : [], | |
| 1391 | - 'ctd' => in_array($wdays_involved[$w], $calc_ctd) ? $calc_ctd : [], | |
| 1392 | - ]; | |
| 1393 | - } | |
| 1232 | + // build push data for each channel rate plan according to the Bulk Rates Cache or to the OTA Pricing | |
| 1233 | + if (($bulk_rates_cache[$id_room][$rd['rate_id']] ?? null)) { | |
| 1234 | + // Bulk Rates Cache available for this room_id and rate_id | |
| 1235 | + $node['pushdata']['rplans'] = $bulk_rates_cache[$id_room][$rd['rate_id']]['rplans']; | |
| 1236 | + $node['pushdata']['cur_rplans'] = $bulk_rates_cache[$id_room][$rd['rate_id']]['cur_rplans']; | |
| 1237 | + $node['pushdata']['rplanarimode'] = $bulk_rates_cache[$id_room][$rd['rate_id']]['rplanarimode']; | |
| 1238 | + } | |
| 1394 | 1239 | |
| 1395 | - // attempt to merge the split cta/ctd nodes for consecutive dates with equal rules | |
| 1396 | - $split_from_ts = 0; | |
| 1397 | - $split_to_ts = 0; | |
| 1398 | - $split_cta = []; | |
| 1399 | - $split_ctd = []; | |
| 1400 | - foreach ($day_ctad_list as $ct_node_restr) { | |
| 1401 | - if (!$split_from_ts) { | |
| 1402 | - // initialize values | |
| 1403 | - $split_from_ts = $ct_node_restr['ts']; | |
| 1404 | - $split_to_ts = $ct_node_restr['ts']; | |
| 1405 | - $split_cta = $ct_node_restr['cta']; | |
| 1406 | - $split_ctd = $ct_node_restr['ctd']; | |
| 1407 | - continue; | |
| 1408 | - } | |
| 1409 | - if ($split_cta != $ct_node_restr['cta'] || $split_ctd != $ct_node_restr['ctd']) { | |
| 1410 | - // close previous interval | |
| 1411 | - $split_ct_nodes[] = [ | |
| 1412 | - 'from_ts' => $split_from_ts, | |
| 1413 | - 'to_ts' => $split_to_ts, | |
| 1414 | - 'cta' => $split_cta, | |
| 1415 | - 'ctd' => $split_ctd, | |
| 1416 | - ]; | |
| 1417 | - // start new values | |
| 1418 | - $split_from_ts = $ct_node_restr['ts']; | |
| 1419 | - $split_to_ts = $ct_node_restr['ts']; | |
| 1420 | - $split_cta = $ct_node_restr['cta']; | |
| 1421 | - $split_ctd = $ct_node_restr['ctd']; | |
| 1422 | - } else { | |
| 1423 | - // increase till day timestamp | |
| 1424 | - $split_to_ts = $ct_node_restr['ts']; | |
| 1425 | - } | |
| 1426 | - } | |
| 1427 | - if (!$split_ct_nodes || $split_ct_nodes[count($split_ct_nodes) - 1]['to_ts'] != $split_from_ts) { | |
| 1428 | - // close last or only interval | |
| 1429 | - $split_ct_nodes[] = [ | |
| 1430 | - 'from_ts' => $split_from_ts, | |
| 1431 | - 'to_ts' => $split_to_ts, | |
| 1432 | - 'cta' => $split_cta, | |
| 1433 | - 'ctd' => $split_ctd, | |
| 1434 | - ]; | |
| 1435 | - } | |
| 1240 | + // check the channels mapped for this room and add what was not found in the Bulk Rates Cache, if anything | |
| 1241 | + foreach ($node['channels'] as $idchannel => $ch_data) { | |
| 1242 | + if (!isset($node['pushdata']['rplans'][$idchannel])) { | |
| 1243 | + // this channel was not found in the Bulk Rates Cache | |
| 1244 | + $ota_mapping_pricing = (array) json_decode($ch_data['otapricing'], true); | |
| 1436 | 1245 | |
| 1437 | - // normalize timestamps | |
| 1438 | - foreach ($split_ct_nodes as &$split_ct_node) { | |
| 1439 | - $split_ct_node['from_dt'] = date('Y-m-d', $split_ct_node['from_ts']); | |
| 1440 | - $split_ct_node['to_dt'] = date('Y-m-d', $split_ct_node['to_ts']); | |
| 1441 | - } | |
| 1246 | + if (defined('VikChannelManagerConfig::EXPEDIA') && $idchannel == VikChannelManagerConfig::EXPEDIA) { | |
| 1247 | + // make sure to sort the Expedia rate plans accordingly | |
| 1248 | + $ota_mapping_pricing = VikChannelManager::sortExpediaChannelPricing($ota_mapping_pricing); | |
| 1249 | + } | |
| 1442 | 1250 | |
| 1443 | - // unset last reference | |
| 1444 | - unset($split_ct_node); | |
| 1445 | - } | |
| 1251 | + // read data from ota mapping pricing | |
| 1252 | + $ch_rplan_id = ''; | |
| 1253 | + if (isset($ota_mapping_pricing['RatePlan'])) { | |
| 1254 | + foreach ($ota_mapping_pricing['RatePlan'] as $rpkey => $rpv) { | |
| 1255 | + // get the first key (rate plan ID) of the RatePlan array from OTA Pricing | |
| 1256 | + $ch_rplan_id = $rpkey; | |
| 1257 | + break; | |
| 1258 | + } | |
| 1259 | + } | |
| 1446 | 1260 | |
| 1447 | - // double check for possible conflicts | |
| 1448 | - $conflicts = count($wdays_involved) < 7 ? false : $conflicts; | |
| 1261 | + // build a list of OTAs NOT supporting rate plans | |
| 1262 | + $ota_single_rplan = []; | |
| 1263 | + if (defined('VikChannelManagerConfig::AIRBNBAPI')) { | |
| 1264 | + $ota_single_rplan[] = VikChannelManagerConfig::AIRBNBAPI; | |
| 1265 | + } | |
| 1266 | + if (defined('VikChannelManagerConfig::VRBOAPI')) { | |
| 1267 | + $ota_single_rplan[] = VikChannelManagerConfig::VRBOAPI; | |
| 1268 | + } | |
| 1449 | 1269 | |
| 1450 | - return [ | |
| 1451 | - // max LOS | |
| 1452 | - $calc_maxlos, | |
| 1453 | - // cta week-days | |
| 1454 | - $calc_cta, | |
| 1455 | - // ctd week-days | |
| 1456 | - $calc_ctd, | |
| 1457 | - // split cta/ctd nodes (range of dates) | |
| 1458 | - $split_ct_nodes, | |
| 1459 | - // whether some dates have conflicting modifiers | |
| 1460 | - $conflicts, | |
| 1461 | - ]; | |
| 1462 | - } | |
| 1270 | + // prevent channel from being updated if not directly involved | |
| 1271 | + $vbo_single_rplan = count($all_rate_plans) === 1; | |
| 1272 | + $is_secondary_rplan = $this->guessOTASecondaryRatePlan($idchannel, $roomrates, ($bulk_rates_cache[$id_room] ?? [])); | |
| 1273 | + $is_google_platform = defined('VikChannelManagerConfig::GOOGLEHOTEL') && $idchannel == VikChannelManagerConfig::GOOGLEHOTEL; | |
| 1274 | + $is_google_platform = $is_google_platform || (defined('VikChannelManagerConfig::GOOGLEVR') && $idchannel == VikChannelManagerConfig::GOOGLEVR); | |
| 1463 | 1275 | |
| 1464 | - /** | |
| 1465 | - * Detects if we are updating a secondary rate plan, probably not supported by the OTA. | |
| 1466 | - * Useful to prevent non-refundable rate plans to be transmitted to channels like Airbnb. | |
| 1467 | - * | |
| 1468 | - * @param int $idchannel The channel unique key. | |
| 1469 | - * @param array $room_rates The rate plan record details. | |
| 1470 | - * @param array $room_cahe The Bulk Rates Cache for the current room-type. | |
| 1471 | - * | |
| 1472 | - * @return bool True if a secondary rate plan was detected. | |
| 1473 | - */ | |
| 1474 | - protected function guessOTASecondaryRatePlan($idchannel, array $room_rates, array $room_cache) | |
| 1475 | - { | |
| 1476 | - $room_type_id = $room_rates['idroom'] ?? 0; | |
| 1477 | - $rate_plan_id = $room_rates['idprice'] ?? 0; | |
| 1478 | - $rate_plan_name = $room_rates['name'] ?? 'Standard'; | |
| 1276 | + /** | |
| 1277 | + * No bulk rates cache found for this channel, room and rate plan. | |
| 1278 | + * We prevent channels like Airbnb from being updated if not for the | |
| 1279 | + * main rate plan only, otherwise we attempt to process the request. | |
| 1280 | + * | |
| 1281 | + * @since 1.17.6 (J) - 1.7.6 (WP) | |
| 1282 | + */ | |
| 1283 | + if (in_array($idchannel, $ota_single_rplan)) { | |
| 1284 | + if (($rplan_info['is_derived'] || $is_secondary_rplan) && !$vbo_single_rplan) { | |
| 1285 | + // skip this channel from updating a derived/secondary rate plan that would not exist | |
| 1286 | + $ch_rplan_id = ''; | |
| 1287 | + } | |
| 1288 | + } elseif (!$is_google_platform && $rplan_info['is_derived'] && !$vbo_single_rplan) { | |
| 1289 | + // derived rate plans will always require bulk rates cache information, unless it's Google | |
| 1290 | + $ch_rplan_id = ''; | |
| 1291 | + } | |
| 1479 | 1292 | |
| 1480 | - if (!$room_cache) { | |
| 1481 | - // unable to perform a detection | |
| 1482 | - return false; | |
| 1483 | - } | |
| 1293 | + // make sure an OTA rate plan ID was found | |
| 1294 | + if (empty($ch_rplan_id)) { | |
| 1295 | + // exclude this channel from being updated | |
| 1296 | + unset($node['channels'][$idchannel]); | |
| 1297 | + continue; | |
| 1298 | + } | |
| 1484 | 1299 | |
| 1485 | - // check how many rate plans are linked to the given channel identifier | |
| 1486 | - $cached_ota_rate_plans = []; | |
| 1300 | + // set channel rate plan data | |
| 1301 | + $node['pushdata']['rplans'][$idchannel] = $ch_rplan_id; | |
| 1302 | + if ($idchannel == VikChannelManagerConfig::BOOKING) { | |
| 1303 | + // Default Pricing is used by default, when no data available | |
| 1304 | + $node['pushdata']['rplanarimode'][$idchannel] = 'person'; | |
| 1305 | + } | |
| 1306 | + } | |
| 1307 | + } | |
| 1487 | 1308 | |
| 1488 | - foreach ($room_cache as $price_id => $plan_cache) { | |
| 1489 | - if (is_array($plan_cache) && ($plan_cache['rplans'][$idchannel] ?? null)) { | |
| 1490 | - $cached_ota_rate_plans[] = $price_id; | |
| 1491 | - } | |
| 1492 | - } | |
| 1309 | + // push update node | |
| 1310 | + $update_rows[] = $node; | |
| 1311 | + } | |
| 1493 | 1312 | |
| 1494 | - if (!$cached_ota_rate_plans) { | |
| 1495 | - // unable to perform a detection due to missing bulk rates cache data | |
| 1496 | - return false; | |
| 1497 | - } | |
| 1313 | + // update rates on the various channels | |
| 1314 | + $channels_map = []; | |
| 1315 | + foreach ($update_rows as $update_row) { | |
| 1316 | + if (!$update_row['channels']) { | |
| 1317 | + // skip update for this room as no channels are involved | |
| 1318 | + continue; | |
| 1319 | + } | |
| 1498 | 1320 | |
| 1499 | - if (in_array($rate_plan_id, $cached_ota_rate_plans)) { | |
| 1500 | - // this rate plan was updated through a bulk action, so it's reliable | |
| 1501 | - return false; | |
| 1502 | - } | |
| 1321 | + // set channels updated | |
| 1322 | + foreach ($update_row['channels'] as $ch) { | |
| 1323 | + if (($channels_updated[$ch['idchannel']] ?? [])) { | |
| 1324 | + continue; | |
| 1325 | + } | |
| 1326 | + $channels_map[$ch['idchannel']] = ucfirst($ch['channel']); | |
| 1327 | + $ota_logo_url = is_object($vcm_logos) ? $vcm_logos->setProvenience($ch['channel'])->getLogoURL() : false; | |
| 1328 | + $channel_logo = $ota_logo_url !== false ? $ota_logo_url : ''; | |
| 1329 | + $channels_updated[$ch['idchannel']] = [ | |
| 1330 | + 'id' => $ch['idchannel'], | |
| 1331 | + 'name' => ucfirst($ch['channel']), | |
| 1332 | + 'logo' => $channel_logo | |
| 1333 | + ]; | |
| 1334 | + } | |
| 1503 | 1335 | |
| 1504 | - // access the room rate plan relations | |
| 1505 | - if (!($this->room_rate_plans[$room_type_id] ?? [])) { | |
| 1506 | - $this->room_rate_plans[$room_type_id] = VBORoomHelper::getInstance()->getRatePlans($room_type_id); | |
| 1507 | - } | |
| 1336 | + // prepare request data | |
| 1337 | + $channels_ids = array_keys($update_row['channels']); | |
| 1338 | + $channels_rplans = []; | |
| 1339 | + foreach ($channels_ids as $ch_id) { | |
| 1340 | + $ch_rplan = isset($update_row['pushdata']['rplans'][$ch_id]) ? $update_row['pushdata']['rplans'][$ch_id] : ''; | |
| 1341 | + $ch_rplan .= isset($update_row['pushdata']['rplanarimode'][$ch_id]) ? '='.$update_row['pushdata']['rplanarimode'][$ch_id] : ''; | |
| 1342 | + $ch_rplan .= isset($update_row['pushdata']['cur_rplans'][$ch_id]) && !empty($update_row['pushdata']['cur_rplans'][$ch_id]) ? ':'.$update_row['pushdata']['cur_rplans'][$ch_id] : ''; | |
| 1343 | + $channels_rplans[] = $ch_rplan; | |
| 1344 | + } | |
| 1508 | 1345 | |
| 1509 | - if (count(($this->room_rate_plans[$room_type_id] ?: [])) < 2) { | |
| 1510 | - // this room-type has got just one rate plan assigned, so it must be a parent rate | |
| 1511 | - return false; | |
| 1512 | - } | |
| 1346 | + $channels = [ | |
| 1347 | + implode(',', $channels_ids) | |
| 1348 | + ]; | |
| 1349 | + $chrplans = [ | |
| 1350 | + implode(',', $channels_rplans) | |
| 1351 | + ]; | |
| 1352 | + $nodes = [ | |
| 1353 | + implode(';', $update_row['ratesinventory']) | |
| 1354 | + ]; | |
| 1355 | + $rooms = [$id_room]; | |
| 1356 | + $pushvars = [ | |
| 1357 | + implode(';', [ | |
| 1358 | + $update_row['pushdata']['pricetype'], | |
| 1359 | + $update_row['pushdata']['defrate'], | |
| 1360 | + ]) | |
| 1361 | + ]; | |
| 1513 | 1362 | |
| 1514 | - if (stripos($rate_plan_name, 'Standard') !== false) { | |
| 1515 | - // we assume this a main rate plan | |
| 1516 | - return false; | |
| 1517 | - } | |
| 1363 | + // send the request | |
| 1364 | + $result = $vboConnector->channelsRatesPush($channels, $chrplans, $nodes, $rooms, $pushvars); | |
| 1365 | + if ($vc_error = $vboConnector->getError(true)) { | |
| 1366 | + $channels_errors[] = $vc_error; | |
| 1367 | + continue; | |
| 1368 | + } | |
| 1518 | 1369 | |
| 1519 | - // this is probably a secondary rate plan for this OTA | |
| 1520 | - return true; | |
| 1521 | - } | |
| 1370 | + // parse the channels update result and compose success, warnings, errors | |
| 1371 | + $result_pool = json_decode($result, true); | |
| 1372 | + foreach (($result_pool ?: []) as $rid => $ch_responses) { | |
| 1373 | + foreach ($ch_responses as $ch_id => $ch_res) { | |
| 1374 | + if ($ch_id == 'breakdown' || !is_numeric($ch_id)) { | |
| 1375 | + // get the rates/dates breakdown of the update request | |
| 1376 | + $bkdown = $ch_res; | |
| 1377 | + if (is_array($ch_res)) { | |
| 1378 | + $bkdown = ''; | |
| 1379 | + foreach ($ch_res as $bk => $bv) { | |
| 1380 | + $bkparts = explode('-', $bk); | |
| 1381 | + if (count($bkparts) == 6) { | |
| 1382 | + // breakdown key is usually composed of two dates in Y-m-d concatenated with another "-". | |
| 1383 | + $bkdown .= 'From ' . implode('-', array_slice($bkparts, 0, 3)) . ' - To ' . implode('-', array_slice($bkparts, 3, 3)) . ': ' . $bv . "\n"; | |
| 1384 | + } else { | |
| 1385 | + $bkdown .= $bk . ': ' . $bv . "\n"; | |
| 1386 | + } | |
| 1387 | + } | |
| 1388 | + // since the Connector does not return breakdown info about the restrictions, we concatenate the response here | |
| 1389 | + if ((int) $setminlos > 0) { | |
| 1390 | + $bkdown = rtrim($bkdown, "\n"); | |
| 1391 | + $bkdown .= ' - Min LOS: ' . $setminlos; | |
| 1392 | + if ((int) $setmaxlos > 0) { | |
| 1393 | + $bkdown .= ' - Max LOS: ' . $setmaxlos; | |
| 1394 | + } | |
| 1395 | + $bkdown_ctad_rules = []; | |
| 1396 | + if ($setctawdays && $current_cta) { | |
| 1397 | + $bkdown_ctad_rules[] = 'CTA: ' . implode(',', $this->weekDaysToShort($current_cta)); | |
| 1398 | + } | |
| 1399 | + if ($setctdwdays && $current_ctd) { | |
| 1400 | + $bkdown_ctad_rules[] = 'CTD: ' . implode(',', $this->weekDaysToShort($current_ctd)); | |
| 1401 | + } | |
| 1402 | + if ($bkdown_ctad_rules) { | |
| 1403 | + $bkdown .= ' [' . implode(' ', $bkdown_ctad_rules) . ']'; | |
| 1404 | + } | |
| 1405 | + $bkdown .= "\n"; | |
| 1406 | + } | |
| 1407 | + $bkdown = rtrim($bkdown, "\n"); | |
| 1408 | + } | |
| 1409 | + if (!isset($channels_bkdown[$ch_id])) { | |
| 1410 | + $channels_bkdown[$ch_id] = $bkdown; | |
| 1411 | + } else { | |
| 1412 | + $channels_bkdown[$ch_id] .= "\n".$bkdown; | |
| 1413 | + } | |
| 1414 | + continue; | |
| 1415 | + } | |
| 1416 | + $ch_id = (int)$ch_id; | |
| 1417 | + if (substr($ch_res, 0, 6) == 'e4j.OK') { | |
| 1418 | + // success | |
| 1419 | + if (!isset($channels_success[$ch_id])) { | |
| 1420 | + $channels_success[$ch_id] = $channels_map[$ch_id]; | |
| 1421 | + } | |
| 1422 | + } elseif (substr($ch_res, 0, 11) == 'e4j.warning') { | |
| 1423 | + // warning | |
| 1424 | + if (!isset($channels_warnings[$ch_id])) { | |
| 1425 | + $channels_warnings[$ch_id] = $channels_map[$ch_id].': '.str_replace('e4j.warning.', '', $ch_res); | |
| 1426 | + } else { | |
| 1427 | + $channels_warnings[$ch_id] .= "\n".str_replace('e4j.warning.', '', $ch_res); | |
| 1428 | + } | |
| 1429 | + // add the channel also to the successful list in case of Warning | |
| 1430 | + if (!isset($channels_success[$ch_id])) { | |
| 1431 | + $channels_success[$ch_id] = $channels_map[$ch_id]; | |
| 1432 | + } | |
| 1433 | + } elseif (substr($ch_res, 0, 9) == 'e4j.error') { | |
| 1434 | + // error | |
| 1435 | + if (!isset($channels_errors[$ch_id])) { | |
| 1436 | + $channels_errors[$ch_id] = $channels_map[$ch_id].': '.str_replace('e4j.error.', '', $ch_res); | |
| 1437 | + } else { | |
| 1438 | + $channels_errors[$ch_id] .= "\n".str_replace('e4j.error.', '', $ch_res); | |
| 1439 | + } | |
| 1440 | + } | |
| 1441 | + } | |
| 1442 | + } | |
| 1443 | + } | |
| 1444 | + } | |
| 1522 | 1445 | |
| 1523 | - /** | |
| 1524 | - * Detects if we are updating a non-mapped rate plan, probably not supported by the OTA. Useful to | |
| 1525 | - * prevent non-refundable rate plans to be transmitted to OTAs that would not directly support it. | |
| 1526 | - * | |
| 1527 | - * @param int $idchannel The channel unique key. | |
| 1528 | - * @param array $room_rates The rate plan record details. | |
| 1529 | - * @param array $room_cahe The Bulk Rates Cache for the current room-type. | |
| 1530 | - * | |
| 1531 | - * @return bool True if the rate plan was mapped in the Bulk Rates Cache. | |
| 1532 | - * | |
| 1533 | - * @since 1.17.2 (J) - 1.7.2 (WP) | |
| 1534 | - */ | |
| 1535 | - protected function isOTARatePlanMapped($idchannel, array $room_rates, array $room_cache) | |
| 1536 | - { | |
| 1537 | - $room_type_id = $room_rates['idroom'] ?? 0; | |
| 1538 | - $rate_plan_id = $room_rates['idprice'] ?? 0; | |
| 1446 | + if ($channels_updated) { | |
| 1447 | + /** | |
| 1448 | + * We now support chained updates due to derived rate plans. | |
| 1449 | + * The "vcm" response property is now an array of objects with equal structure. | |
| 1450 | + * | |
| 1451 | + * @since 1.16.10 (J) - 1.6.10 (WP) | |
| 1452 | + */ | |
| 1453 | + if (!isset($newly_rates['vcm'])) { | |
| 1454 | + $newly_rates['vcm'] = []; | |
| 1455 | + } | |
| 1539 | 1456 | |
| 1540 | - // access the room rate plan relations | |
| 1541 | - if (!($this->room_rate_plans[$room_type_id] ?? [])) { | |
| 1542 | - $this->room_rate_plans[$room_type_id] = VBORoomHelper::getInstance()->getRatePlans($room_type_id); | |
| 1543 | - } | |
| 1457 | + // build channels response data for the current rate plan | |
| 1458 | + $channels_response_data = [ | |
| 1459 | + 'rplan_id' => $now_id_price, | |
| 1460 | + 'rplan_name' => $rate_plan_name, | |
| 1461 | + 'is_derived' => $rplan_info['is_derived'], | |
| 1462 | + 'channels_updated' => $channels_updated, | |
| 1463 | + ]; | |
| 1544 | 1464 | |
| 1545 | - if (count(($this->room_rate_plans[$room_type_id] ?: [])) < 2) { | |
| 1546 | - // this room-type has got just one rate plan assigned, so we consider it as mapped | |
| 1547 | - return true; | |
| 1548 | - } | |
| 1465 | + // set these property only if not empty | |
| 1466 | + if ($channels_bkdown) { | |
| 1467 | + $channels_response_data['channels_bkdown'] = $channels_bkdown['breakdown']; | |
| 1468 | + } | |
| 1469 | + if ($channels_success) { | |
| 1470 | + $channels_response_data['channels_success'] = $channels_success; | |
| 1471 | + } | |
| 1472 | + if ($channels_warnings) { | |
| 1473 | + $channels_response_data['channels_warnings'] = $channels_warnings; | |
| 1474 | + // cache channel warnings | |
| 1475 | + $this->channel_warnings = $channels_warnings; | |
| 1476 | + } | |
| 1477 | + if ($channels_errors) { | |
| 1478 | + $channels_response_data['channels_errors'] = $channels_errors; | |
| 1479 | + // cache channel errors | |
| 1480 | + $this->channel_errors = $channels_errors; | |
| 1481 | + } | |
| 1549 | 1482 | |
| 1550 | - if (!$room_cache) { | |
| 1551 | - // unable to perform a detection | |
| 1552 | - return false; | |
| 1553 | - } | |
| 1483 | + // push channels response data to the pool | |
| 1484 | + $newly_rates['vcm'][] = $channels_response_data; | |
| 1554 | 1485 | |
| 1555 | - // check how many rate plans are linked to the given channel identifier | |
| 1556 | - $cached_ota_rate_plans = []; | |
| 1486 | + // cache the channels updated list by eventually merging what was already set | |
| 1487 | + foreach ($channels_updated as $idch => $chinfo) { | |
| 1488 | + $this->channels_updated_list[$idch] = $chinfo; | |
| 1489 | + } | |
| 1557 | 1490 | |
| 1558 | - foreach ($room_cache as $price_id => $plan_cache) { | |
| 1559 | - if (is_array($plan_cache) && ($plan_cache['rplans'][$idchannel] ?? null)) { | |
| 1560 | - $cached_ota_rate_plans[] = $price_id; | |
| 1561 | - } | |
| 1562 | - } | |
| 1491 | + // check for possible conflicts | |
| 1492 | + if (!$rplan_info['is_derived'] && !$split_ct_nodes && ($conflicts ?? false) === true) { | |
| 1493 | + // trigger an automatic bulk action for uploading the rates to ensure a full refresh | |
| 1494 | + VikChannelManager::autoBulkActions([ | |
| 1495 | + 'from_date' => $from_date, | |
| 1496 | + 'to_date' => $to_date, | |
| 1497 | + 'forced_rooms' => [$id_room], | |
| 1498 | + 'update' => 'rates', | |
| 1499 | + ]); | |
| 1500 | + } | |
| 1501 | + } | |
| 1502 | + } | |
| 1503 | + } | |
| 1563 | 1504 | |
| 1564 | - if (!$cached_ota_rate_plans) { | |
| 1565 | - // unable to perform a detection due to missing bulk rates cache data | |
| 1566 | - return false; | |
| 1567 | - } | |
| 1505 | + return $newly_rates; | |
| 1506 | + } | |
| 1568 | 1507 | |
| 1569 | - // return whether this rate plan was updated through a bulk action | |
| 1570 | - return in_array($rate_plan_id, $cached_ota_rate_plans); | |
| 1571 | - } | |
| 1508 | + /** | |
| 1509 | + * Gets the rates for the lowest number of nights for the given listing and one, or all, rate plan(s). | |
| 1510 | + * | |
| 1511 | + * @param int $id_room The listing ID. | |
| 1512 | + * @param ?int $id_price Optional rate plan, or listing rates for any rate plan will be fetched. | |
| 1513 | + * @param bool $use_cache Whether to allow caching or not, enabled by default. | |
| 1514 | + * | |
| 1515 | + * @return array | |
| 1516 | + * | |
| 1517 | + * @since 1.18.5 (J) - 1.8.5 (WP) | |
| 1518 | + */ | |
| 1519 | + public function getBaseRoomRates(int $id_room, ?int $id_price = null, bool $use_cache = true) | |
| 1520 | + { | |
| 1521 | + // calculate signature cache | |
| 1522 | + $cache_signature = sprintf('%d_%d', $id_room, ($id_price ?: 0)); | |
| 1572 | 1523 | |
| 1573 | - /** | |
| 1574 | - * Used for breakdown purposes, converts a list of week-day indexes. | |
| 1575 | - * | |
| 1576 | - * @param array $wdays List of zero-based week day indexes. | |
| 1577 | - * | |
| 1578 | - * @return array List of readable week days. | |
| 1579 | - * | |
| 1580 | - * @since 1.17.1 (J) - 1.7.1 (WP) | |
| 1581 | - */ | |
| 1582 | - protected function weekDaysToShort(array $wdays) | |
| 1583 | - { | |
| 1584 | - $map = [ | |
| 1585 | - 'Sun', | |
| 1586 | - 'Mon', | |
| 1587 | - 'Tue', | |
| 1588 | - 'Wed', | |
| 1589 | - 'Thu', | |
| 1590 | - 'Fri', | |
| 1591 | - 'Sat', | |
| 1592 | - ]; | |
| 1524 | + if ($use_cache && isset(self::$cached_base_rates[$cache_signature])) { | |
| 1525 | + // return the previously cached value to avoid duplicate queries | |
| 1526 | + return self::$cached_base_rates[$cache_signature]; | |
| 1527 | + } | |
| 1593 | 1528 | |
| 1594 | - return array_map(function($wday_index) use ($map) { | |
| 1595 | - $wday_index = (int) $wday_index; | |
| 1596 | - return $map[$wday_index] ?? $wday_index; | |
| 1597 | - }, $wdays); | |
| 1598 | - } | |
| 1529 | + $dbo = JFactory::getDbo(); | |
| 1530 | + | |
| 1531 | + // inner-join query | |
| 1532 | + $qinner = $dbo->getQuery(true) | |
| 1533 | + ->select('MIN(' . $dbo->qn('days') . ') AS ' . $dbo->qn('min_days')) | |
| 1534 | + ->from($dbo->qn('#__vikbooking_dispcost')) | |
| 1535 | + ->where($dbo->qn('idroom') . ' = ' . $id_room) | |
| 1536 | + ->group($dbo->qn('idroom')); | |
| 1537 | + if ($id_price) { | |
| 1538 | + $qinner->where($dbo->qn('idprice') . ' = ' . $id_price); | |
| 1539 | + } | |
| 1540 | + | |
| 1541 | + // main query | |
| 1542 | + $q = $dbo->getQuery(true) | |
| 1543 | + ->select([ | |
| 1544 | + $dbo->qn('r.id'), | |
| 1545 | + $dbo->qn('r.idroom'), | |
| 1546 | + $dbo->qn('r.days'), | |
| 1547 | + $dbo->qn('r.idprice'), | |
| 1548 | + $dbo->qn('r.cost'), | |
| 1549 | + $dbo->qn('p.name'), | |
| 1550 | + ]) | |
| 1551 | + ->from($dbo->qn('#__vikbooking_dispcost', 'r')) | |
| 1552 | + ->innerJoin('(' . $qinner . ') AS ' . $dbo->qn('r2') . ' ON ' . $dbo->qn('r.days') . ' = ' . $dbo->qn('r2.min_days')) | |
| 1553 | + ->leftJoin( | |
| 1554 | + $dbo->qn('#__vikbooking_prices', 'p') . ' ON ' . $dbo->qn('p.id') . ' = ' . $dbo->qn('r.idprice') . | |
| 1555 | + ($id_price ? ' AND ' . $dbo->qn('p.id') . ' = ' . $id_price : '') | |
| 1556 | + ) | |
| 1557 | + ->where($dbo->qn('r.idroom') . ' = ' . $id_room) | |
| 1558 | + ->group([ | |
| 1559 | + $dbo->qn('r.id'), | |
| 1560 | + $dbo->qn('r.idroom'), | |
| 1561 | + $dbo->qn('r.days'), | |
| 1562 | + $dbo->qn('r.idprice'), | |
| 1563 | + $dbo->qn('r.cost'), | |
| 1564 | + $dbo->qn('p.name'), | |
| 1565 | + ]) | |
| 1566 | + ->order($dbo->qn('r.days') . ' ASC') | |
| 1567 | + ->order($dbo->qn('r.cost') . ' ASC'); | |
| 1568 | + if ($id_price) { | |
| 1569 | + $q->where($dbo->qn('r.idprice') . ' = ' . $id_price); | |
| 1570 | + } | |
| 1571 | + | |
| 1572 | + // read the rates for the lowest number of nights, either for | |
| 1573 | + // a specific rate plan ID, or for all configured rate plans | |
| 1574 | + $dbo->setQuery($q); | |
| 1575 | + $roomrates = $dbo->loadAssocList(); | |
| 1576 | + | |
| 1577 | + foreach ($roomrates as $rrk => $rrv) { | |
| 1578 | + $roomrates[$rrk]['cost'] = round(($rrv['cost'] / $rrv['days']), 2); | |
| 1579 | + $roomrates[$rrk]['days'] = 1; | |
| 1580 | + } | |
| 1581 | + | |
| 1582 | + if ($use_cache) { | |
| 1583 | + // cache value | |
| 1584 | + self::$cached_base_rates[$cache_signature] = $roomrates; | |
| 1585 | + } | |
| 1586 | + | |
| 1587 | + return $roomrates; | |
| 1588 | + } | |
| 1589 | + | |
| 1590 | + /** | |
| 1591 | + * Returns the information about the channels updated with | |
| 1592 | + * the last rates/restrictions modification request, if any. | |
| 1593 | + * | |
| 1594 | + * @return array | |
| 1595 | + */ | |
| 1596 | + public function getChannelsUpdated() | |
| 1597 | + { | |
| 1598 | + $channel_details = []; | |
| 1599 | + | |
| 1600 | + $vcm_logos = VikBooking::getVcmChannelsLogo('', true); | |
| 1601 | + | |
| 1602 | + foreach ($this->channels_updated_list as $idchannel => $channel_data) { | |
| 1603 | + $small_logo_url = $vcm_logos ? $vcm_logos->setProvenience(strtolower($channel_data['name']))->getSmallLogoURL() : ''; | |
| 1604 | + $tiny_logo_url = $vcm_logos ? $vcm_logos->setProvenience(strtolower($channel_data['name']))->getTinyLogoURL() : ''; | |
| 1605 | + | |
| 1606 | + // set channel details | |
| 1607 | + $channel_details[] = [ | |
| 1608 | + 'id' => $channel_data['id'], | |
| 1609 | + 'name' => $channel_data['name'], | |
| 1610 | + 'logo' => $channel_data['logo'], | |
| 1611 | + 'small_logo' => $small_logo_url, | |
| 1612 | + 'tiny_logo' => $tiny_logo_url, | |
| 1613 | + ]; | |
| 1614 | + } | |
| 1615 | + | |
| 1616 | + return $channel_details; | |
| 1617 | + } | |
| 1618 | + | |
| 1619 | + /** | |
| 1620 | + * Returns the associative list of channel warning messages. | |
| 1621 | + * | |
| 1622 | + * @return array | |
| 1623 | + */ | |
| 1624 | + public function getChannelWarnings() | |
| 1625 | + { | |
| 1626 | + return $this->channel_warnings; | |
| 1627 | + } | |
| 1628 | + | |
| 1629 | + /** | |
| 1630 | + * Returns the associative list of channel error messages. | |
| 1631 | + * | |
| 1632 | + * @return array | |
| 1633 | + */ | |
| 1634 | + public function getChannelErrors() | |
| 1635 | + { | |
| 1636 | + return $this->channel_errors; | |
| 1637 | + } | |
| 1638 | + | |
| 1639 | + /** | |
| 1640 | + * Calculates the restriction modifiers for a specific range of dates and room. | |
| 1641 | + * | |
| 1642 | + * @param int $start_ts The date range start date timestamp. | |
| 1643 | + * @param int $end_ts The date range end date timestamp. | |
| 1644 | + * @param int $room_id The VikBooking room ID. | |
| 1645 | + * | |
| 1646 | + * @return array Numeric list of modifiers (Max LOS, CTA, CTD, split nodes, conflicts). | |
| 1647 | + * | |
| 1648 | + * @since 1.17.1 (J) - 1.7.1 (WP) | |
| 1649 | + */ | |
| 1650 | + public function calculateRestrictionModifiers($start_ts, $end_ts, $room_id) | |
| 1651 | + { | |
| 1652 | + // build cache signature | |
| 1653 | + $from_dt = date('Y-m-d', $start_ts); | |
| 1654 | + $to_dt = date('Y-m-d', $end_ts); | |
| 1655 | + $signature = $from_dt . '_' . $to_dt . '_' . $room_id; | |
| 1656 | + | |
| 1657 | + if (!isset($this->cached_restrictions[$signature])) { | |
| 1658 | + // load and cache restrictions | |
| 1659 | + $this->cached_restrictions[$signature] = VikBooking::loadRestrictions(true, [$room_id]); | |
| 1660 | + } | |
| 1661 | + | |
| 1662 | + // build default return values | |
| 1663 | + $calc_maxlos = 0; | |
| 1664 | + $calc_cta = []; | |
| 1665 | + $calc_ctd = []; | |
| 1666 | + $split_ct_nodes = []; | |
| 1667 | + $conflicts = false; | |
| 1668 | + | |
| 1669 | + if (!($this->cached_restrictions[$signature] ?? [])) { | |
| 1670 | + // do not proceed as nothing would be found | |
| 1671 | + return [ | |
| 1672 | + $calc_maxlos, | |
| 1673 | + $calc_cta, | |
| 1674 | + $calc_ctd, | |
| 1675 | + $split_ct_nodes, | |
| 1676 | + $conflicts, | |
| 1677 | + ]; | |
| 1678 | + } | |
| 1679 | + | |
| 1680 | + // list of week days involved | |
| 1681 | + $wdays_involved = []; | |
| 1682 | + | |
| 1683 | + // loop through the requested range of dates | |
| 1684 | + $infostart = getdate($start_ts); | |
| 1685 | + $infofirst = $infostart; | |
| 1686 | + while ($infostart[0] > 0 && $infostart[0] <= $end_ts) { | |
| 1687 | + // calculate timestamps | |
| 1688 | + $tomorrow_ts = mktime(0, 0, 0, $infostart['mon'], ($infostart['mday'] + 1), $infostart['year']); | |
| 1689 | + $today_mid_ts = mktime(0, 0, 0, $infostart['mon'], $infostart['mday'], $infostart['year']); | |
| 1690 | + | |
| 1691 | + // set week-day involved | |
| 1692 | + $wdays_involved[] = $infostart['wday']; | |
| 1693 | + | |
| 1694 | + // calculate restrictions | |
| 1695 | + $restrictions = VikBooking::parseSeasonRestrictions($today_mid_ts, $tomorrow_ts, 1, $this->cached_restrictions[$signature] ?? []); | |
| 1696 | + | |
| 1697 | + // check for max LOS | |
| 1698 | + if ($restrictions['maxlos'] ?? null) { | |
| 1699 | + if (!$calc_maxlos) { | |
| 1700 | + $calc_maxlos = (int) $restrictions['maxlos']; | |
| 1701 | + } | |
| 1702 | + } | |
| 1703 | + | |
| 1704 | + // check for CTA week-days | |
| 1705 | + if ($restrictions['cta'] ?? []) { | |
| 1706 | + if (!$calc_cta) { | |
| 1707 | + $calc_cta = (array) $restrictions['cta']; | |
| 1708 | + $conflicts = $conflicts || !in_array($infostart['wday'], (array) $restrictions['cta']); | |
| 1709 | + } else { | |
| 1710 | + $conflicts = $conflicts || $calc_cta != (array) $restrictions['cta']; | |
| 1711 | + } | |
| 1712 | + } elseif ($calc_cta) { | |
| 1713 | + $conflicts = true; | |
| 1714 | + } | |
| 1715 | + | |
| 1716 | + // check for CTD week-days | |
| 1717 | + if ($restrictions['ctd'] ?? []) { | |
| 1718 | + if (!$calc_ctd) { | |
| 1719 | + $calc_ctd = (array) $restrictions['ctd']; | |
| 1720 | + $conflicts = $conflicts || !in_array($infostart['wday'], (array) $restrictions['ctd']); | |
| 1721 | + } else { | |
| 1722 | + $conflicts = $conflicts || $calc_ctd != (array) $restrictions['ctd']; | |
| 1723 | + } | |
| 1724 | + } elseif ($calc_ctd) { | |
| 1725 | + $conflicts = true; | |
| 1726 | + } | |
| 1727 | + | |
| 1728 | + // go to next day | |
| 1729 | + $infostart = getdate($tomorrow_ts); | |
| 1730 | + } | |
| 1731 | + | |
| 1732 | + if ($calc_cta) { | |
| 1733 | + // ensure it's a list of integers representing week-days | |
| 1734 | + $calc_cta = array_map(function($w) { | |
| 1735 | + return (int) str_replace('-', '', (string) $w); | |
| 1736 | + }, $calc_cta); | |
| 1737 | + | |
| 1738 | + if (count($wdays_involved) < 7) { | |
| 1739 | + // filter the CTA week days according to the nights involved | |
| 1740 | + $calc_cta = array_filter($calc_cta, function($w) use ($wdays_involved) { | |
| 1741 | + return in_array($w, $wdays_involved); | |
| 1742 | + }); | |
| 1743 | + } | |
| 1744 | + } | |
| 1745 | + | |
| 1746 | + if ($calc_ctd) { | |
| 1747 | + // ensure it's a list of integers representing week-days | |
| 1748 | + $calc_ctd = array_map(function($w) { | |
| 1749 | + return (int) str_replace('-', '', (string) $w); | |
| 1750 | + }, $calc_ctd); | |
| 1751 | + | |
| 1752 | + if (count($wdays_involved) < 7) { | |
| 1753 | + // filter the CTD week days according to the nights involved | |
| 1754 | + $calc_ctd = array_filter($calc_ctd, function($w) use ($wdays_involved) { | |
| 1755 | + return in_array($w, $wdays_involved); | |
| 1756 | + }); | |
| 1757 | + } | |
| 1758 | + } | |
| 1759 | + | |
| 1760 | + if ($conflicts && count($wdays_involved) < 7 && ($calc_cta || $calc_ctd)) { | |
| 1761 | + // conflicting cta/ctd restrictions for a range of dates less than a week may be split into multiple OTA nodes | |
| 1762 | + $day_ctad_list = []; | |
| 1763 | + for ($w = 0; $w < count($wdays_involved); $w++) { | |
| 1764 | + // build day individual cta/ctd restrictions | |
| 1765 | + $split_ts = mktime(0, 0, 0, $infofirst['mon'], $infofirst['mday'] + $w, $infofirst['year']); | |
| 1766 | + $day_ctad_list[] = [ | |
| 1767 | + 'ts' => $split_ts, | |
| 1768 | + 'day' => date('Y-m-d', $split_ts), | |
| 1769 | + 'cta' => in_array($wdays_involved[$w], $calc_cta) ? $calc_cta : [], | |
| 1770 | + 'ctd' => in_array($wdays_involved[$w], $calc_ctd) ? $calc_ctd : [], | |
| 1771 | + ]; | |
| 1772 | + } | |
| 1773 | + | |
| 1774 | + // attempt to merge the split cta/ctd nodes for consecutive dates with equal rules | |
| 1775 | + $split_from_ts = 0; | |
| 1776 | + $split_to_ts = 0; | |
| 1777 | + $split_cta = []; | |
| 1778 | + $split_ctd = []; | |
| 1779 | + foreach ($day_ctad_list as $ct_node_restr) { | |
| 1780 | + if (!$split_from_ts) { | |
| 1781 | + // initialize values | |
| 1782 | + $split_from_ts = $ct_node_restr['ts']; | |
| 1783 | + $split_to_ts = $ct_node_restr['ts']; | |
| 1784 | + $split_cta = $ct_node_restr['cta']; | |
| 1785 | + $split_ctd = $ct_node_restr['ctd']; | |
| 1786 | + continue; | |
| 1787 | + } | |
| 1788 | + if ($split_cta != $ct_node_restr['cta'] || $split_ctd != $ct_node_restr['ctd']) { | |
| 1789 | + // close previous interval | |
| 1790 | + $split_ct_nodes[] = [ | |
| 1791 | + 'from_ts' => $split_from_ts, | |
| 1792 | + 'to_ts' => $split_to_ts, | |
| 1793 | + 'cta' => $split_cta, | |
| 1794 | + 'ctd' => $split_ctd, | |
| 1795 | + ]; | |
| 1796 | + // start new values | |
| 1797 | + $split_from_ts = $ct_node_restr['ts']; | |
| 1798 | + $split_to_ts = $ct_node_restr['ts']; | |
| 1799 | + $split_cta = $ct_node_restr['cta']; | |
| 1800 | + $split_ctd = $ct_node_restr['ctd']; | |
| 1801 | + } else { | |
| 1802 | + // increase till day timestamp | |
| 1803 | + $split_to_ts = $ct_node_restr['ts']; | |
| 1804 | + } | |
| 1805 | + } | |
| 1806 | + if (!$split_ct_nodes || $split_ct_nodes[count($split_ct_nodes) - 1]['to_ts'] != $split_from_ts) { | |
| 1807 | + // close last or only interval | |
| 1808 | + $split_ct_nodes[] = [ | |
| 1809 | + 'from_ts' => $split_from_ts, | |
| 1810 | + 'to_ts' => $split_to_ts, | |
| 1811 | + 'cta' => $split_cta, | |
| 1812 | + 'ctd' => $split_ctd, | |
| 1813 | + ]; | |
| 1814 | + } | |
| 1815 | + | |
| 1816 | + // normalize timestamps | |
| 1817 | + foreach ($split_ct_nodes as &$split_ct_node) { | |
| 1818 | + $split_ct_node['from_dt'] = date('Y-m-d', $split_ct_node['from_ts']); | |
| 1819 | + $split_ct_node['to_dt'] = date('Y-m-d', $split_ct_node['to_ts']); | |
| 1820 | + } | |
| 1821 | + | |
| 1822 | + // unset last reference | |
| 1823 | + unset($split_ct_node); | |
| 1824 | + } | |
| 1825 | + | |
| 1826 | + // double check for possible conflicts | |
| 1827 | + $conflicts = count($wdays_involved) < 7 ? false : $conflicts; | |
| 1828 | + | |
| 1829 | + return [ | |
| 1830 | + // max LOS | |
| 1831 | + $calc_maxlos, | |
| 1832 | + // cta week-days | |
| 1833 | + $calc_cta, | |
| 1834 | + // ctd week-days | |
| 1835 | + $calc_ctd, | |
| 1836 | + // split cta/ctd nodes (range of dates) | |
| 1837 | + $split_ct_nodes, | |
| 1838 | + // whether some dates have conflicting modifiers | |
| 1839 | + $conflicts, | |
| 1840 | + ]; | |
| 1841 | + } | |
| 1842 | + | |
| 1843 | + /** | |
| 1844 | + * Detects if we are updating a secondary rate plan, probably not supported by the OTA. | |
| 1845 | + * Useful to prevent non-refundable rate plans to be transmitted to channels like Airbnb. | |
| 1846 | + * | |
| 1847 | + * @param int $idchannel The channel unique key. | |
| 1848 | + * @param array $room_rates The rate plan record details. | |
| 1849 | + * @param array $room_cahe The Bulk Rates Cache for the current room-type. | |
| 1850 | + * | |
| 1851 | + * @return bool True if a secondary rate plan was detected. | |
| 1852 | + */ | |
| 1853 | + protected function guessOTASecondaryRatePlan($idchannel, array $room_rates, array $room_cache) | |
| 1854 | + { | |
| 1855 | + $room_type_id = $room_rates['idroom'] ?? 0; | |
| 1856 | + $rate_plan_id = $room_rates['idprice'] ?? 0; | |
| 1857 | + $rate_plan_name = $room_rates['name'] ?? 'Standard'; | |
| 1858 | + | |
| 1859 | + if (!$room_cache) { | |
| 1860 | + // unable to perform a detection | |
| 1861 | + return false; | |
| 1862 | + } | |
| 1863 | + | |
| 1864 | + // check how many rate plans are linked to the given channel identifier | |
| 1865 | + $cached_ota_rate_plans = []; | |
| 1866 | + | |
| 1867 | + foreach ($room_cache as $price_id => $plan_cache) { | |
| 1868 | + if (is_array($plan_cache) && ($plan_cache['rplans'][$idchannel] ?? null)) { | |
| 1869 | + $cached_ota_rate_plans[] = $price_id; | |
| 1870 | + } | |
| 1871 | + } | |
| 1872 | + | |
| 1873 | + if (!$cached_ota_rate_plans) { | |
| 1874 | + // unable to perform a detection due to missing bulk rates cache data | |
| 1875 | + return false; | |
| 1876 | + } | |
| 1877 | + | |
| 1878 | + if (in_array($rate_plan_id, $cached_ota_rate_plans)) { | |
| 1879 | + // this rate plan was updated through a bulk action, so it's reliable | |
| 1880 | + return false; | |
| 1881 | + } | |
| 1882 | + | |
| 1883 | + // access the room rate plan relations | |
| 1884 | + if (!($this->room_rate_plans[$room_type_id] ?? [])) { | |
| 1885 | + $this->room_rate_plans[$room_type_id] = VBORoomHelper::getInstance()->getRatePlans($room_type_id); | |
| 1886 | + } | |
| 1887 | + | |
| 1888 | + if (count(($this->room_rate_plans[$room_type_id] ?: [])) < 2) { | |
| 1889 | + // this room-type has got just one rate plan assigned, so it must be a parent rate | |
| 1890 | + return false; | |
| 1891 | + } | |
| 1892 | + | |
| 1893 | + if (stripos($rate_plan_name, 'Standard') !== false) { | |
| 1894 | + // we assume this a main rate plan | |
| 1895 | + return false; | |
| 1896 | + } | |
| 1897 | + | |
| 1898 | + // this is probably a secondary rate plan for this OTA | |
| 1899 | + return true; | |
| 1900 | + } | |
| 1901 | + | |
| 1902 | + /** | |
| 1903 | + * Detects if we are updating a non-mapped rate plan, probably not supported by the OTA. Useful to | |
| 1904 | + * prevent non-refundable rate plans to be transmitted to OTAs that would not directly support it. | |
| 1905 | + * | |
| 1906 | + * @param int $idchannel The channel unique key. | |
| 1907 | + * @param array $room_rates The rate plan record details. | |
| 1908 | + * @param array $room_cahe The Bulk Rates Cache for the current room-type. | |
| 1909 | + * | |
| 1910 | + * @return bool True if the rate plan was mapped in the Bulk Rates Cache. | |
| 1911 | + * | |
| 1912 | + * @since 1.17.2 (J) - 1.7.2 (WP) | |
| 1913 | + */ | |
| 1914 | + protected function isOTARatePlanMapped($idchannel, array $room_rates, array $room_cache) | |
| 1915 | + { | |
| 1916 | + $room_type_id = $room_rates['idroom'] ?? 0; | |
| 1917 | + $rate_plan_id = $room_rates['idprice'] ?? 0; | |
| 1918 | + | |
| 1919 | + // access the room rate plan relations | |
| 1920 | + if (!($this->room_rate_plans[$room_type_id] ?? [])) { | |
| 1921 | + $this->room_rate_plans[$room_type_id] = VBORoomHelper::getInstance()->getRatePlans($room_type_id); | |
| 1922 | + } | |
| 1923 | + | |
| 1924 | + if (count(($this->room_rate_plans[$room_type_id] ?: [])) < 2) { | |
| 1925 | + // this room-type has got just one rate plan assigned, so we consider it as mapped | |
| 1926 | + return true; | |
| 1927 | + } | |
| 1928 | + | |
| 1929 | + if (!$room_cache) { | |
| 1930 | + // unable to perform a detection | |
| 1931 | + return false; | |
| 1932 | + } | |
| 1933 | + | |
| 1934 | + // check how many rate plans are linked to the given channel identifier | |
| 1935 | + $cached_ota_rate_plans = []; | |
| 1936 | + | |
| 1937 | + foreach ($room_cache as $price_id => $plan_cache) { | |
| 1938 | + if (is_array($plan_cache) && ($plan_cache['rplans'][$idchannel] ?? null)) { | |
| 1939 | + $cached_ota_rate_plans[] = $price_id; | |
| 1940 | + } | |
| 1941 | + } | |
| 1942 | + | |
| 1943 | + if (!$cached_ota_rate_plans) { | |
| 1944 | + // unable to perform a detection due to missing bulk rates cache data | |
| 1945 | + return false; | |
| 1946 | + } | |
| 1947 | + | |
| 1948 | + // return whether this rate plan was updated through a bulk action | |
| 1949 | + return in_array($rate_plan_id, $cached_ota_rate_plans); | |
| 1950 | + } | |
| 1951 | + | |
| 1952 | + /** | |
| 1953 | + * Used for breakdown purposes, converts a list of week-day indexes. | |
| 1954 | + * | |
| 1955 | + * @param array $wdays List of zero-based week day indexes. | |
| 1956 | + * | |
| 1957 | + * @return array List of readable week days. | |
| 1958 | + * | |
| 1959 | + * @since 1.17.1 (J) - 1.7.1 (WP) | |
| 1960 | + */ | |
| 1961 | + protected function weekDaysToShort(array $wdays) | |
| 1962 | + { | |
| 1963 | + $map = [ | |
| 1964 | + 'Sun', | |
| 1965 | + 'Mon', | |
| 1966 | + 'Tue', | |
| 1967 | + 'Wed', | |
| 1968 | + 'Thu', | |
| 1969 | + 'Fri', | |
| 1970 | + 'Sat', | |
| 1971 | + ]; | |
| 1972 | + | |
| 1973 | + return array_map(function($wday_index) use ($map) { | |
| 1974 | + $wday_index = (int) $wday_index; | |
| 1975 | + return $map[$wday_index] ?? $wday_index; | |
| 1976 | + }, $wdays); | |
| 1977 | + } | |
| 1599 | 1978 | } |