| 1 |
<?php |
| 2 |
/** |
| 3 |
* @package VikBooking |
| 4 |
* @subpackage core |
| 5 |
* @author E4J s.r.l. |
| 6 |
* @copyright Copyright (C) 2024 E4J s.r.l. All Rights Reserved. |
| 7 |
* @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL |
| 8 |
* @link https://vikwp.com |
| 9 |
*/ |
| 10 |
|
| 11 |
// No direct access |
| 12 |
defined('ABSPATH') or die('No script kiddies please!'); |
| 13 |
|
| 14 |
/** |
| 15 |
* VikBooking model pricing. |
| 16 |
* |
| 17 |
* @since 1.16.10 (J) - 1.6.10 (WP) |
| 18 |
*/ |
| 19 |
class VBOModelPricing extends JObject |
| 20 |
{ |
| 21 |
/** @var array */ |
| 22 |
protected $room_rate_plans = []; |
| 23 |
|
| 24 |
/** @var array */ |
| 25 |
protected $channels_updated_list = []; |
| 26 |
|
| 27 |
/** @var array */ |
| 28 |
protected $channel_warnings = []; |
| 29 |
|
| 30 |
/** @var array */ |
| 31 |
protected $channel_errors = []; |
| 32 |
|
| 33 |
/** @var array */ |
| 34 |
protected $cached_restrictions = []; |
| 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 |
} |
| 48 |
|
| 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(); |
| 62 |
|
| 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); |
| 70 |
|
| 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 |
} |
| 75 |
|
| 76 |
if (JFactory::getDate($to_date) < JFactory::getDate($from_date)) { |
| 77 |
// invalid dates |
| 78 |
throw new InvalidArgumentException('Invalid dates received.', 400); |
| 79 |
} |
| 80 |
|
| 81 |
if (!$id_room) { |
| 82 |
throw new InvalidArgumentException('Room record ID is mandatory.', 400); |
| 83 |
} |
| 84 |
|
| 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); |
| 90 |
|
| 91 |
// load check-in and check-out times |
| 92 |
list($checkin_h, $checkin_m, $checkout_h, $checkout_m) = VBOModelReservation::getInstance()->loadCheckinOutTimes(); |
| 93 |
|
| 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'); |
| 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(); |
| 103 |
|
| 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 |
} |
| 110 |
|
| 111 |
if (!$id_price) { |
| 112 |
throw new Exception('No rate plans configured.', 500); |
| 113 |
} |
| 114 |
|
| 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 |
} |
| 144 |
|
| 145 |
// load room rates from db |
| 146 |
$dbo->setQuery($q); |
| 147 |
$roomrates = $dbo->loadAssocList(); |
| 148 |
|
| 149 |
foreach ($roomrates as $rrk => $rrv) { |
| 150 |
$roomrates[$rrk]['cost'] = round(($rrv['cost'] / $rrv['days']), 2); |
| 151 |
$roomrates[$rrk]['days'] = 1; |
| 152 |
} |
| 153 |
|
| 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 |
} |
| 158 |
|
| 159 |
// fetch all restrictions, if requested |
| 160 |
$all_restrictions = $restrictions ? VikBooking::loadRestrictions(true, [$id_room]) : []; |
| 161 |
|
| 162 |
// calculate global minimum stay |
| 163 |
$glob_minlos = VikBooking::getDefaultNightsCalendar(); |
| 164 |
$glob_minlos = $glob_minlos < 1 ? 1 : $glob_minlos; |
| 165 |
|
| 166 |
// read current room rates |
| 167 |
$current_rates = []; |
| 168 |
|
| 169 |
// loop through all the requested range of dates |
| 170 |
$start_ts = strtotime($from_date); |
| 171 |
$end_ts = strtotime($to_date); |
| 172 |
$infostart = getdate($start_ts); |
| 173 |
while ($infostart[0] > 0 && $infostart[0] <= $end_ts) { |
| 174 |
// calculate timestamps |
| 175 |
$tomorrow_ts = mktime(0, 0, 0, $infostart['mon'], ($infostart['mday'] + 1), $infostart['year']); |
| 176 |
$today_tsin = VikBooking::getDateTimestamp(date($df, $infostart[0]), $checkin_h, $checkin_m); |
| 177 |
$today_tsout = VikBooking::getDateTimestamp(date($df, $tomorrow_ts), $checkout_h, $checkout_m); |
| 178 |
$today_mid_ts = mktime(0, 0, 0, $infostart['mon'], $infostart['mday'], $infostart['year']); |
| 179 |
|
| 180 |
// calculate tariffs for this day |
| 181 |
$tars = VikBooking::applySeasonsRoom($roomrates, $today_tsin, $today_tsout); |
| 182 |
|
| 183 |
foreach ($tars as $index => $tar) { |
| 184 |
// apply rounding to 2 decimals at most |
| 185 |
$tars[$index]['cost'] = round($tar['cost'], 2); |
| 186 |
|
| 187 |
// set formatted cost |
| 188 |
$tars[$index]['formatted_cost'] = VikBooking::numberFormat($tar['cost']); |
| 189 |
|
| 190 |
// calculate restrictions |
| 191 |
$tars[$index]['restrictions'] = []; |
| 192 |
if ($restrictions) { |
| 193 |
$restr = VikBooking::parseSeasonRestrictions($today_mid_ts, $tomorrow_ts, 1, $all_restrictions); |
| 194 |
if (!$restr) { |
| 195 |
$restr = ['minlos' => $glob_minlos]; |
| 196 |
} |
| 197 |
// set day restrictions |
| 198 |
$tars[$index]['restrictions'] = $restr; |
| 199 |
} |
| 200 |
} |
| 201 |
|
| 202 |
if (!$all_rplans) { |
| 203 |
// set rate for this day (single rate plan) |
| 204 |
$current_rates[(date('Y-m-d', $infostart[0]))] = $tars[0]; |
| 205 |
} else { |
| 206 |
// set rates for this day (all rate plans) |
| 207 |
$current_rates[(date('Y-m-d', $infostart[0]))] = $tars; |
| 208 |
} |
| 209 |
|
| 210 |
// go to next day |
| 211 |
$infostart = getdate($tomorrow_ts); |
| 212 |
} |
| 213 |
|
| 214 |
return $current_rates; |
| 215 |
} |
| 216 |
|
| 217 |
/** |
| 218 |
* Applies new rates and/or restrictions to the given room and rate plan(s). |
| 219 |
* Changes are always applied to the website rates, and eventually also on the OTAs. |
| 220 |
* |
| 221 |
* @return array |
| 222 |
* |
| 223 |
* @throws Exception |
| 224 |
* |
| 225 |
* @since 1.17.1 (J) - 1.7.1 (WP) added support to CTA, CTD and Max LOS restrictions. |
| 226 |
*/ |
| 227 |
public function modifyRateRestrictions() |
| 228 |
{ |
| 229 |
$dbo = JFactory::getDbo(); |
| 230 |
|
| 231 |
// expected and supported properties binded |
| 232 |
$from_date = (string) $this->get('from_date', ''); |
| 233 |
$to_date = (string) $this->get('to_date', ''); |
| 234 |
$id_room = (int) $this->get('id_room', 0); |
| 235 |
$id_price = (int) $this->get('id_price', 0); |
| 236 |
$rplan_name = $this->get('rplan_name', ''); |
| 237 |
$rate = (float) $this->get('rate', 0); |
| 238 |
$min_los = (int) $this->get('min_los', 0); |
| 239 |
$max_los = (int) $this->get('max_los', 0); |
| 240 |
$cta_wdays = (array) $this->get('cta_wdays', []); |
| 241 |
$ctd_wdays = (array) $this->get('ctd_wdays', []); |
| 242 |
$upd_otas = (bool) $this->get('update_otas', true); |
| 243 |
$close_rplan = (bool) $this->get('close_rate_plan', false); |
| 244 |
$merge_restr = (bool) $this->get('merge_restrictions', true); |
| 245 |
|
| 246 |
if (!$from_date || !$to_date) { |
| 247 |
// must be in Y-m-d format |
| 248 |
throw new InvalidArgumentException('Missing dates for applying the new rates or restriction.', 400); |
| 249 |
} |
| 250 |
|
| 251 |
if (!$id_room) { |
| 252 |
throw new InvalidArgumentException('Room record ID is mandatory.', 400); |
| 253 |
} |
| 254 |
|
| 255 |
/** |
| 256 |
* Disable season records caching because new rates will have to be re-calculated |
| 257 |
* for the response by checking the same exact dates. |
| 258 |
*/ |
| 259 |
VikBooking::setSeasonsCache(false); |
| 260 |
|
| 261 |
// load check-in and check-out times |
| 262 |
list($checkin_h, $checkin_m, $checkout_h, $checkout_m) = VBOModelReservation::getInstance([], true)->loadCheckinOutTimes(); |
| 263 |
|
| 264 |
// date format |
| 265 |
$vbo_df = VikBooking::getDateFormat(); |
| 266 |
$df = $vbo_df == "%d/%m/%Y" ? 'd/m/Y' : ($vbo_df == "%m/%d/%Y" ? 'm/d/Y' : 'Y/m/d'); |
| 267 |
|
| 268 |
// access the availability helper |
| 269 |
$av_helper = VikBooking::getAvailabilityInstance(true); |
| 270 |
|
| 271 |
if (!$id_price) { |
| 272 |
// load all rate plans |
| 273 |
$all_rate_plans = $av_helper->loadRatePlans(); |
| 274 |
|
| 275 |
// use the first rate plan ID after the automatic sorting |
| 276 |
foreach ($all_rate_plans as $all_rate_plan) { |
| 277 |
$id_price = $all_rate_plan['id']; |
| 278 |
break; |
| 279 |
} |
| 280 |
|
| 281 |
if ($rplan_name) { |
| 282 |
// check if the given rate plan name is found |
| 283 |
$rplan_name = trim(preg_replace("/[^a-z]/i", ' ', $rplan_name)); |
| 284 |
foreach ($all_rate_plans as $all_rate_plan) { |
| 285 |
$match_against = trim(preg_replace("/[^a-z]/i", ' ', $all_rate_plan['name'])); |
| 286 |
if (stripos($match_against, $rplan_name) !== false || stripos($rplan_name, $match_against) !== false) { |
| 287 |
// use the matched rate plan instead |
| 288 |
$id_price = $all_rate_plan['id']; |
| 289 |
break; |
| 290 |
} |
| 291 |
} |
| 292 |
} |
| 293 |
} |
| 294 |
|
| 295 |
if (!$id_price) { |
| 296 |
throw new Exception('No rate plans configured.', 500); |
| 297 |
} |
| 298 |
|
| 299 |
// load the eventually involved derived rate plans from the given rate ID |
| 300 |
$derived_rate_plans = $av_helper->getDerivedRatePlans($id_price); |
| 301 |
|
| 302 |
// build the list of rate plans involved by adding the requested one |
| 303 |
$rate_plans_pool = [ |
| 304 |
$id_price => [ |
| 305 |
'id' => $id_price, |
| 306 |
// the main rate plan selected for the update is NEVER considered as derived, even if it actually was. |
| 307 |
'is_derived' => 0, |
| 308 |
'derived_data' => null, |
| 309 |
'rate' => $rate, |
| 310 |
], |
| 311 |
]; |
| 312 |
|
| 313 |
foreach ($derived_rate_plans as $derived_rate_plan) { |
| 314 |
if (isset($rate_plans_pool[$derived_rate_plan['id']])) { |
| 315 |
// skip duplicate rate plan |
| 316 |
continue; |
| 317 |
} |
| 318 |
|
| 319 |
// calculate new rate for this derived rate plan |
| 320 |
$rplan_derived_rate = $rate; |
| 321 |
if (($derived_rate_plan['derived_data']['mode'] ?? 'discount') == 'discount') { |
| 322 |
// discount rate |
| 323 |
if (($derived_rate_plan['derived_data']['type'] ?? 'percent') == 'percent') { |
| 324 |
// percent value |
| 325 |
$rplan_derived_rate = $rplan_derived_rate * (100 - (float) ($derived_rate_plan['derived_data']['value'] ?? 0)) / 100; |
| 326 |
} else { |
| 327 |
// absolute value |
| 328 |
$rplan_derived_rate -= (float) ($derived_rate_plan['derived_data']['value'] ?? 0); |
| 329 |
} |
| 330 |
} else { |
| 331 |
// increase rate |
| 332 |
if (($derived_rate_plan['derived_data']['type'] ?? 'percent') == 'percent') { |
| 333 |
// percent value |
| 334 |
$rplan_derived_rate = $rplan_derived_rate * (100 + (float) ($derived_rate_plan['derived_data']['value'] ?? 0)) / 100; |
| 335 |
} else { |
| 336 |
// absolute value |
| 337 |
$rplan_derived_rate += (float) ($derived_rate_plan['derived_data']['value'] ?? 0); |
| 338 |
} |
| 339 |
} |
| 340 |
|
| 341 |
if ($rplan_derived_rate < 0) { |
| 342 |
// negative rates are not allowed |
| 343 |
continue; |
| 344 |
} |
| 345 |
|
| 346 |
// make sure to apply rounding |
| 347 |
$rplan_derived_rate = round($rplan_derived_rate, 2); |
| 348 |
|
| 349 |
// push derived rate plan to the update pool |
| 350 |
$rate_plans_pool[$derived_rate_plan['id']] = [ |
| 351 |
'id' => $derived_rate_plan['id'], |
| 352 |
'is_derived' => 1, |
| 353 |
'derived_data' => $derived_rate_plan['derived_data'], |
| 354 |
'rate' => $rplan_derived_rate, |
| 355 |
]; |
| 356 |
} |
| 357 |
|
| 358 |
// the newly applied rates |
| 359 |
$newly_rates = []; |
| 360 |
|
| 361 |
// apply the pricing modification to all the involved rate plans |
| 362 |
foreach ($rate_plans_pool as $rplan_id => $rplan_info) { |
| 363 |
// set rate plan ID |
| 364 |
$now_id_price = $rplan_info['id']; |
| 365 |
|
| 366 |
// set rate to apply to the current rate plan |
| 367 |
$rate = $rplan_info['rate']; |
| 368 |
|
| 369 |
// read the rates for the lowest number of nights |
| 370 |
$q = "SELECT `r`.`id`,`r`.`idroom`,`r`.`days`,`r`.`idprice`,`r`.`cost`,`p`.`name` |
| 371 |
FROM `#__vikbooking_dispcost` AS `r` |
| 372 |
INNER JOIN ( |
| 373 |
SELECT MIN(`days`) AS `min_days` |
| 374 |
FROM `#__vikbooking_dispcost` |
| 375 |
WHERE `idroom`=" . $id_room . " AND `idprice`=" . $now_id_price . " |
| 376 |
GROUP BY `idroom` |
| 377 |
) AS `r2` ON `r`.`days`=`r2`.`min_days` |
| 378 |
LEFT JOIN `#__vikbooking_prices` `p` ON `p`.`id`=`r`.`idprice` AND `p`.`id`=" . $now_id_price . " |
| 379 |
WHERE `r`.`idroom`=" . $id_room . " AND `r`.`idprice`=" . $now_id_price . " |
| 380 |
GROUP BY `r`.`id`,`r`.`idroom`,`r`.`days`,`r`.`idprice`,`r`.`cost`,`p`.`name` |
| 381 |
ORDER BY `r`.`days` ASC, `r`.`cost` ASC;"; |
| 382 |
|
| 383 |
$dbo->setQuery($q); |
| 384 |
$roomrates = $dbo->loadAssocList(); |
| 385 |
|
| 386 |
foreach ($roomrates as $rrk => $rrv) { |
| 387 |
$roomrates[$rrk]['cost'] = round(($rrv['cost'] / $rrv['days']), 2); |
| 388 |
$roomrates[$rrk]['days'] = 1; |
| 389 |
} |
| 390 |
|
| 391 |
if (!$roomrates) { |
| 392 |
if ($rplan_info['is_derived']) { |
| 393 |
// this is not the main rate plan we are updating |
| 394 |
continue; |
| 395 |
} |
| 396 |
|
| 397 |
// terminate the process by throwing an error |
| 398 |
throw new UnexpectedValueException('No rates found for the given room ID.', 400); |
| 399 |
} |
| 400 |
|
| 401 |
// turn the rates list into a single-level array |
| 402 |
$roomrates = $roomrates[0]; |
| 403 |
|
| 404 |
// set rate plan name |
| 405 |
$rate_plan_name = $roomrates['name']; |
| 406 |
|
| 407 |
// read current room rates |
| 408 |
$current_rates = []; |
| 409 |
$start_ts = strtotime($from_date); |
| 410 |
$end_ts = strtotime($to_date); |
| 411 |
$infostart = getdate($start_ts); |
| 412 |
while ($infostart[0] > 0 && $infostart[0] <= $end_ts) { |
| 413 |
$tomorrow_ts = mktime(0, 0, 0, $infostart['mon'], ($infostart['mday'] + 1), $infostart['year']); |
| 414 |
$today_tsin = VikBooking::getDateTimestamp(date($df, $infostart[0]), $checkin_h, $checkin_m); |
| 415 |
$today_tsout = VikBooking::getDateTimestamp(date($df, $tomorrow_ts), $checkout_h, $checkout_m); |
| 416 |
|
| 417 |
$tars = VikBooking::applySeasonsRoom([$roomrates], $today_tsin, $today_tsout); |
| 418 |
|
| 419 |
// apply rounding to 2 decimals at most |
| 420 |
$tars[0]['cost'] = round($tars[0]['cost'], 2); |
| 421 |
|
| 422 |
$current_rates[(date('Y-m-d', $infostart[0]))] = $tars[0]; |
| 423 |
|
| 424 |
$infostart = getdate($tomorrow_ts); |
| 425 |
} |
| 426 |
|
| 427 |
if (!$current_rates) { |
| 428 |
if ($rplan_info['is_derived']) { |
| 429 |
// this is not the main rate plan we are updating |
| 430 |
continue; |
| 431 |
} |
| 432 |
|
| 433 |
// terminate the process by throwing an error |
| 434 |
throw new UnexpectedValueException('No seasonal rates found for the given room ID.', 400); |
| 435 |
} |
| 436 |
|
| 437 |
$all_days = array_keys($current_rates); |
| 438 |
$season_intervals = []; |
| 439 |
$firstind = 0; |
| 440 |
$firstdaycost = $current_rates[$all_days[0]]['cost']; |
| 441 |
$nextdaycost = false; |
| 442 |
for ($i = 1; $i < count($all_days); $i++) { |
| 443 |
$ind = $all_days[$i]; |
| 444 |
$nextdaycost = $current_rates[$ind]['cost']; |
| 445 |
if ($firstdaycost != $nextdaycost) { |
| 446 |
$interval = [ |
| 447 |
'from' => $all_days[$firstind], |
| 448 |
'to' => $all_days[($i - 1)], |
| 449 |
'cost' => $firstdaycost |
| 450 |
]; |
| 451 |
$season_intervals[] = $interval; |
| 452 |
$firstdaycost = $nextdaycost; |
| 453 |
$firstind = $i; |
| 454 |
} |
| 455 |
} |
| 456 |
if ($nextdaycost === false) { |
| 457 |
$interval = [ |
| 458 |
'from' => $all_days[$firstind], |
| 459 |
'to' => $all_days[$firstind], |
| 460 |
'cost' => $firstdaycost |
| 461 |
]; |
| 462 |
$season_intervals[] = $interval; |
| 463 |
} elseif ($firstdaycost == $nextdaycost) { |
| 464 |
$interval = [ |
| 465 |
'from' => $all_days[$firstind], |
| 466 |
'to' => $all_days[($i - 1)], |
| 467 |
'cost' => $firstdaycost |
| 468 |
]; |
| 469 |
$season_intervals[] = $interval; |
| 470 |
} |
| 471 |
foreach ($season_intervals as $sik => $siv) { |
| 472 |
if ((float)$siv['cost'] == $rate) { |
| 473 |
unset($season_intervals[$sik]); |
| 474 |
} |
| 475 |
} |
| 476 |
|
| 477 |
if (!$season_intervals) { |
| 478 |
// do not raise this error if it was requested to set the restriction or to close a rate plan |
| 479 |
if ((!($min_los > 0) && !$close_rplan) || !$upd_otas) { |
| 480 |
if ($rplan_info['is_derived']) { |
| 481 |
// this is not the main rate plan we are updating |
| 482 |
continue; |
| 483 |
} |
| 484 |
|
| 485 |
// terminate the process by throwing an error |
| 486 |
throw new RuntimeException('No rates modification needed with the given parameters.', 500); |
| 487 |
} |
| 488 |
} |
| 489 |
|
| 490 |
if ($rate > 0) { |
| 491 |
// make sure to set a cost greater than zero to avoid errors |
| 492 |
foreach ($season_intervals as $sik => $siv) { |
| 493 |
$first = strtotime($siv['from']); |
| 494 |
$second = strtotime($siv['to']); |
| 495 |
|
| 496 |
if ($second > 0 && $second == $first) { |
| 497 |
$second += 86399; |
| 498 |
} |
| 499 |
|
| 500 |
if (!($second > $first)) { |
| 501 |
unset($season_intervals[$sik]); |
| 502 |
continue; |
| 503 |
} |
| 504 |
|
| 505 |
$baseone = getdate($first); |
| 506 |
$basets = mktime(0, 0, 0, 1, 1, $baseone['year']); |
| 507 |
$sfrom = $baseone[0] - $basets; |
| 508 |
$basetwo = getdate($second); |
| 509 |
$basets = mktime(0, 0, 0, 1, 1, $basetwo['year']); |
| 510 |
$sto = $basetwo[0] - $basets; |
| 511 |
|
| 512 |
// check leap year |
| 513 |
if ($baseone['year'] % 4 == 0 && ($baseone['year'] % 100 != 0 || $baseone['year'] % 400 == 0)) { |
| 514 |
$leapts = mktime(0, 0, 0, 2, 29, $baseone['year']); |
| 515 |
if ($baseone[0] > $leapts) { |
| 516 |
$sfrom -= 86400; |
| 517 |
/** |
| 518 |
* To avoid issue with leap years and dates near Feb 29th, we only reduce the seconds if these were reduced |
| 519 |
* for the from-date of the seasons. Doing it just for the to-date in 2019 for 2020 (leap) produced invalid results. |
| 520 |
*/ |
| 521 |
if ($basetwo['year'] % 4 == 0 && ($basetwo['year'] % 100 != 0 || $basetwo['year'] % 400 == 0)) { |
| 522 |
$leapts = mktime(0, 0, 0, 2, 29, $basetwo['year']); |
| 523 |
if ($basetwo[0] > $leapts) { |
| 524 |
$sto -= date('d-m', $baseone[0]) != '31-12' && date('d-m', $basetwo[0]) == '31-12' ? 1 : 86400; |
| 525 |
} |
| 526 |
} |
| 527 |
} |
| 528 |
} |
| 529 |
|
| 530 |
$tieyear = $baseone['year']; |
| 531 |
$season_type = (float)$siv['cost'] > $rate ? "2" : "1"; |
| 532 |
$season_diffcost = $season_type == "1" ? ($rate - (float)$siv['cost']) : ((float)$siv['cost'] - $rate); |
| 533 |
$roomstr = "-" . $id_room . "-,"; |
| 534 |
$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'] : ''); |
| 535 |
$pricestr = "-" . $now_id_price . "-,"; |
| 536 |
|
| 537 |
// build and store season record |
| 538 |
$season_record = new stdClass; |
| 539 |
$season_record->type = $season_type == "1" ? 1 : 2; |
| 540 |
$season_record->from = $sfrom; |
| 541 |
$season_record->to = $sto; |
| 542 |
$season_record->diffcost = $season_diffcost; |
| 543 |
$season_record->idrooms = $roomstr; |
| 544 |
$season_record->spname = $season_name; |
| 545 |
$season_record->wdays = ''; |
| 546 |
$season_record->checkinincl = 0; |
| 547 |
$season_record->val_pcent = 1; |
| 548 |
$season_record->losoverride = ''; |
| 549 |
$season_record->year = $tieyear; |
| 550 |
$season_record->idprices = $pricestr; |
| 551 |
|
| 552 |
$dbo->insertObject('#__vikbooking_seasons', $season_record, 'id'); |
| 553 |
} |
| 554 |
} |
| 555 |
|
| 556 |
// calculate the involved dates |
| 557 |
$start_ts = strtotime($from_date); |
| 558 |
$end_ts = strtotime($to_date); |
| 559 |
$infostart = getdate($start_ts); |
| 560 |
$infoend = getdate($end_ts); |
| 561 |
|
| 562 |
/** |
| 563 |
* Restrictions can be set only if VCM is enabled because we use the Connector Class. |
| 564 |
* It is allowed to set just a restriction for the website without any rate modification. |
| 565 |
* OTAs instead would need a rate to be passed in order to eventually transmit the restrictions. |
| 566 |
*/ |
| 567 |
$current_minlos = 0; |
| 568 |
$current_maxlos = 0; |
| 569 |
$current_cta = []; |
| 570 |
$current_ctd = []; |
| 571 |
$split_ct_nodes = []; |
| 572 |
$vboConnector = null; |
| 573 |
|
| 574 |
if (method_exists('VikChannelManager', 'getVikBookingConnectorInstance')) { |
| 575 |
// invoke the Connector for any update request |
| 576 |
$vboConnector = VikChannelManager::getVikBookingConnectorInstance(); |
| 577 |
// set the caller to 'VBO' to reduce the sleep time between the requests |
| 578 |
$vboConnector->caller = 'VBO'; |
| 579 |
} else { |
| 580 |
// make sure the OTA update flag is off |
| 581 |
$upd_otas = false; |
| 582 |
} |
| 583 |
|
| 584 |
// minimum length of stay is always necessary for creating a restriction even with other modifiers |
| 585 |
if ($min_los > 0 && $vboConnector) { |
| 586 |
// set the end date to the last second |
| 587 |
$end_ts = mktime(23, 59, 59, $infoend['mon'], $infoend['mday'], $infoend['year']); |
| 588 |
|
| 589 |
/** |
| 590 |
* Setting just a min los restriction may lift a previously defined CTA/CTD rule for the same dates. |
| 591 |
* If merging restrictions is not disabled, and if no other modifiers are set (max los, cta, ctd), the |
| 592 |
* system will automatically calculate the current modifiers in order to keep them on the OTAs. Such |
| 593 |
* calculated restriction modifiers will not need to be created on VikBooking, but just passed to the OTAs. |
| 594 |
* If conflicting restriction modifiers are detected, it is recommended to schedule an auto bulk action. |
| 595 |
* |
| 596 |
* @since 1.17.1 (J) - 1.7.1 (WP) |
| 597 |
*/ |
| 598 |
if ($merge_restr && !$max_los && !$cta_wdays && !$ctd_wdays) { |
| 599 |
// calculate the restriction modifiers beside the min los and get additional details |
| 600 |
list($calc_maxlos, $calc_cta, $calc_ctd, $split_ct_nodes, $conflicts) = $this->calculateRestrictionModifiers($start_ts, $end_ts, $id_room); |
| 601 |
|
| 602 |
if ($calc_maxlos) { |
| 603 |
// set calculated max los |
| 604 |
$max_los = $calc_maxlos; |
| 605 |
} |
| 606 |
|
| 607 |
if ($calc_cta) { |
| 608 |
// set calculated cta week days |
| 609 |
$cta_wdays = $calc_cta; |
| 610 |
} |
| 611 |
|
| 612 |
if ($calc_ctd) { |
| 613 |
// set calculated ctd week days |
| 614 |
$ctd_wdays = $calc_ctd; |
| 615 |
} |
| 616 |
} |
| 617 |
|
| 618 |
if (!$rplan_info['is_derived']) { |
| 619 |
// build the minimum stay restriction string, eventually inclusive of CTA/CTD rules |
| 620 |
$vbo_min_los_str = $min_los; |
| 621 |
if ($cta_wdays) { |
| 622 |
// append cta instructions |
| 623 |
$vbo_min_los_str .= 'CTA[' . implode(',', $cta_wdays) . ']'; |
| 624 |
} |
| 625 |
if ($ctd_wdays) { |
| 626 |
// append ctd instructions |
| 627 |
$vbo_min_los_str .= 'CTD[' . implode(',', $ctd_wdays) . ']'; |
| 628 |
} |
| 629 |
|
| 630 |
// create the restriction in VBO (only for the parent rate since it will be a room-level restriction) |
| 631 |
$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]); |
| 632 |
|
| 633 |
// update values for the response and the rest of the operations |
| 634 |
if ($restr_res) { |
| 635 |
$current_minlos = $min_los; |
| 636 |
$current_maxlos = $max_los; |
| 637 |
$current_cta = $cta_wdays; |
| 638 |
$current_ctd = $ctd_wdays; |
| 639 |
} else { |
| 640 |
$current_minlos = 'e4j.error.' . $vboConnector->getError(); |
| 641 |
} |
| 642 |
} else { |
| 643 |
// always update the min/max stay information |
| 644 |
$current_minlos = $min_los; |
| 645 |
$current_maxlos = $max_los; |
| 646 |
$current_cta = $cta_wdays; |
| 647 |
$current_ctd = $ctd_wdays; |
| 648 |
} |
| 649 |
} |
| 650 |
|
| 651 |
// check if all dates involved share the same price |
| 652 |
$common_rate = -1; |
| 653 |
|
| 654 |
// prepare output by re-calculating the new rates in real-time |
| 655 |
while ($infostart[0] > 0 && $infostart[0] <= $end_ts) { |
| 656 |
$tomorrow_ts = mktime(0, 0, 0, $infostart['mon'], ($infostart['mday'] + 1), $infostart['year']); |
| 657 |
$today_tsin = VikBooking::getDateTimestamp(date($df, $infostart[0]), $checkin_h, $checkin_m); |
| 658 |
$today_tsout = VikBooking::getDateTimestamp(date($df, $tomorrow_ts), $checkout_h, $checkout_m); |
| 659 |
|
| 660 |
$tars = VikBooking::applySeasonsRoom([$roomrates], $today_tsin, $today_tsout); |
| 661 |
|
| 662 |
// apply rounding to 2 decimals at most |
| 663 |
$tars[0]['cost'] = round($tars[0]['cost'], 2); |
| 664 |
|
| 665 |
if ($common_rate < 0) { |
| 666 |
// save first-day common rate |
| 667 |
$common_rate = $tars[0]['cost']; |
| 668 |
} else { |
| 669 |
// check if we've got a different rate for this day |
| 670 |
if ($common_rate != $tars[0]['cost']) { |
| 671 |
// freeze all controls, because this day has got a different cost |
| 672 |
$common_rate = 0; |
| 673 |
} |
| 674 |
} |
| 675 |
|
| 676 |
$indkey = $infostart['mday'] . '-' . $infostart['mon'] . '-' . $infostart['year'] . '-' . $now_id_price; |
| 677 |
$newly_rates[$indkey] = $tars[0]; |
| 678 |
if (is_int($current_minlos) && $current_minlos > 0) { |
| 679 |
$newly_rates[$indkey]['newminlos'] = $current_minlos; |
| 680 |
} |
| 681 |
|
| 682 |
$infostart = getdate($tomorrow_ts); |
| 683 |
} |
| 684 |
|
| 685 |
/** |
| 686 |
* Store a record in the rates flow for this rate modification on VBO. |
| 687 |
*/ |
| 688 |
$rflow_handler = VikBooking::getRatesFlowInstance($anew = true); |
| 689 |
if ($rflow_handler !== null) { |
| 690 |
$rflow_record = $rflow_handler->getRecord() |
| 691 |
->setCreatedBy($this->get('_created_by', 'VBO')) |
| 692 |
->setDates($from_date, $to_date) |
| 693 |
->setVBORoomID($id_room) |
| 694 |
->setVBORatePlanID($now_id_price); |
| 695 |
|
| 696 |
if ($rate > 0) { |
| 697 |
// a new rate was set |
| 698 |
$rflow_record->setNightlyFee($rate); |
| 699 |
} |
| 700 |
|
| 701 |
if (is_int($current_minlos) && $current_minlos > 0) { |
| 702 |
// push restriction extra data |
| 703 |
$rflow_record->setRestrictions([ |
| 704 |
'minLOS' => $current_minlos, |
| 705 |
'maxLOS' => $current_maxlos, |
| 706 |
'cta' => (bool) $current_cta, |
| 707 |
'ctd' => (bool) $current_ctd, |
| 708 |
]); |
| 709 |
} |
| 710 |
|
| 711 |
if (method_exists($rflow_record, 'setBaseFee')) { |
| 712 |
$rflow_record->setBaseFee($roomrates['cost']); |
| 713 |
} |
| 714 |
|
| 715 |
// push rates flow record |
| 716 |
$rflow_handler->pushRecord($rflow_record); |
| 717 |
|
| 718 |
// store rates flow record |
| 719 |
$rflow_handler->storeRecords(); |
| 720 |
} |
| 721 |
|
| 722 |
// check if restrictions can be transmitted to OTAs in case of no rate given |
| 723 |
if ($upd_otas && $rate <= 0 && is_int($current_minlos) && $current_minlos > 0 && $common_rate > 0) { |
| 724 |
// ensure OTAs will get the minimum stay by using the rate shared by all dates involved |
| 725 |
$rate = $common_rate; |
| 726 |
} |
| 727 |
|
| 728 |
/** |
| 729 |
* Channels will only be updated if a rate greater than zero was passed, |
| 730 |
* and of course if the flag to the update the OTAs is enabled. Restrictions |
| 731 |
* alone could not be pushed to the OTAs, or rate threshold errors would occur. |
| 732 |
*/ |
| 733 |
if ($upd_otas && $rate > 0) { |
| 734 |
// launch channel manager (from VBO, unlikely through the App, we update one rate plan per request) |
| 735 |
$vcm_logos = VikBooking::getVcmChannelsLogo('', true); |
| 736 |
$channels_updated = []; |
| 737 |
$channels_bkdown = []; |
| 738 |
$channels_success = []; |
| 739 |
$channels_warnings = []; |
| 740 |
$channels_errors = []; |
| 741 |
|
| 742 |
// load room details |
| 743 |
$q = "SELECT `id`,`name`,`units` FROM `#__vikbooking_rooms` WHERE `id`=" . $id_room . ";"; |
| 744 |
$dbo->setQuery($q); |
| 745 |
$row = $dbo->loadAssoc(); |
| 746 |
if ($row) { |
| 747 |
$row['channels'] = []; |
| 748 |
// get the mapped channels for this room |
| 749 |
$q = "SELECT * FROM `#__vikchannelmanager_roomsxref` WHERE `idroomvb`=" . $id_room . ";"; |
| 750 |
$dbo->setQuery($q); |
| 751 |
foreach ($dbo->loadAssocList() as $ch_data) { |
| 752 |
$row['channels'][$ch_data['idchannel']] = $ch_data; |
| 753 |
} |
| 754 |
} |
| 755 |
|
| 756 |
if ($row && ($row['channels'] ?? [])) { |
| 757 |
// this room is actually mapped to some channels supporting AV requests |
| 758 |
// load the 'Bulk Action - Rates Upload' cache |
| 759 |
$bulk_rates_cache = VikChannelManager::getBulkRatesCache(); |
| 760 |
|
| 761 |
// we update one rate plan per time, even though we could update all of them with a similar request |
| 762 |
$rates_data = [ |
| 763 |
[ |
| 764 |
'rate_id' => $now_id_price, |
| 765 |
'cost' => $rate, |
| 766 |
] |
| 767 |
]; |
| 768 |
|
| 769 |
// build the array with the update details |
| 770 |
$update_rows = []; |
| 771 |
foreach ($rates_data as $rk => $rd) { |
| 772 |
$node = $row; |
| 773 |
$setminlos = ''; |
| 774 |
$setmaxlos = ''; |
| 775 |
$setctawdays = ''; |
| 776 |
$setctdwdays = ''; |
| 777 |
|
| 778 |
// pass the restrictions to the channels if specified |
| 779 |
if (is_int($current_maxlos) && $current_maxlos > 0) { |
| 780 |
// set max los first |
| 781 |
$setmaxlos = $current_maxlos; |
| 782 |
} |
| 783 |
if (is_int($current_minlos) && $current_minlos > 0) { |
| 784 |
// set min los after |
| 785 |
$setminlos = $current_minlos; |
| 786 |
// max los must have a length > 0 or min los won't be set |
| 787 |
$setmaxlos = $setmaxlos ?: '0'; |
| 788 |
// eavaluate whether cta/ctd week-days should be added |
| 789 |
if ($current_cta) { |
| 790 |
$setctawdays = 'CTA[' . implode(',', $current_cta) . ']'; |
| 791 |
} |
| 792 |
if ($current_ctd) { |
| 793 |
$setctdwdays = 'CTD[' . implode(',', $current_ctd) . ']'; |
| 794 |
} |
| 795 |
} |
| 796 |
|
| 797 |
// check for follow restriction flag in a derived rate plan |
| 798 |
if ($rplan_info['is_derived'] && !((bool) ($rplan_info['derived_data']['follow_restr'] ?? 1))) { |
| 799 |
// unset restriction values for only updating the rates |
| 800 |
$setminlos = ''; |
| 801 |
$setmaxlos = ''; |
| 802 |
$setctawdays = ''; |
| 803 |
$setctdwdays = ''; |
| 804 |
} |
| 805 |
|
| 806 |
// close rate plan (min or max los do not need to be set) |
| 807 |
if ($close_rplan) { |
| 808 |
// VikBookingConnector class in VCM requires the closure to be concatenated to maxlos |
| 809 |
$setmaxlos .= 'closed'; |
| 810 |
} |
| 811 |
|
| 812 |
// 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) |
| 813 |
if (($bulk_rates_cache[$id_room][$rd['rate_id']] ?? null)) { |
| 814 |
if ((int) $bulk_rates_cache[$id_room][$rd['rate_id']]['rmod'] > 0 && (float) $bulk_rates_cache[$id_room][$rd['rate_id']]['rmodamount'] > 0) { |
| 815 |
if ((int) $bulk_rates_cache[$id_room][$rd['rate_id']]['rmodop'] > 0) { |
| 816 |
// Increase rates |
| 817 |
if ((int) $bulk_rates_cache[$id_room][$rd['rate_id']]['rmodval'] > 0) { |
| 818 |
// Percentage charge |
| 819 |
$rd['cost'] = $rd['cost'] * (100 + (float) $bulk_rates_cache[$id_room][$rd['rate_id']]['rmodamount']) / 100; |
| 820 |
} else { |
| 821 |
// Fixed charge |
| 822 |
$rd['cost'] += (float) $bulk_rates_cache[$id_room][$rd['rate_id']]['rmodamount']; |
| 823 |
} |
| 824 |
} else { |
| 825 |
// Lower rates |
| 826 |
if ((int) $bulk_rates_cache[$id_room][$rd['rate_id']]['rmodval'] > 0) { |
| 827 |
// Percentage discount |
| 828 |
$disc_op = $rd['cost'] * (float) $bulk_rates_cache[$id_room][$rd['rate_id']]['rmodamount'] / 100; |
| 829 |
$rd['cost'] -= $disc_op; |
| 830 |
} else { |
| 831 |
// Fixed discount |
| 832 |
$rd['cost'] -= (float) $bulk_rates_cache[$id_room][$rd['rate_id']]['rmodamount']; |
| 833 |
} |
| 834 |
} |
| 835 |
} |
| 836 |
} |
| 837 |
|
| 838 |
// set rate inventory node(s) |
| 839 |
if (count($split_ct_nodes) > 1 && $setminlos && ($current_cta || $current_ctd)) { |
| 840 |
// there will be multiple OTA rate inventory nodes for better accuracy and avoid conflicts with cta/ctd rules |
| 841 |
$node['ratesinventory'] = []; |
| 842 |
|
| 843 |
foreach ($split_ct_nodes as $split_ct_node) { |
| 844 |
// calculate proper cta/ctd strings for this date interval |
| 845 |
$setctawdays_range = $split_ct_node['cta'] ? 'CTA[' . implode(',', $split_ct_node['cta']) . ']' : ''; |
| 846 |
$setctdwdays_range = $split_ct_node['ctd'] ? 'CTD[' . implode(',', $split_ct_node['ctd']) . ']' : ''; |
| 847 |
|
| 848 |
// set rate inventory node for the properly calculated range of dates, cta and ctd rules |
| 849 |
$node['ratesinventory'][] = implode('_', [ |
| 850 |
$split_ct_node['from_dt'], |
| 851 |
$split_ct_node['to_dt'], |
| 852 |
$setminlos . $setctawdays_range . $setctdwdays_range, |
| 853 |
$setmaxlos, |
| 854 |
1, |
| 855 |
2, |
| 856 |
$rd['cost'], |
| 857 |
0, |
| 858 |
]); |
| 859 |
} |
| 860 |
} else { |
| 861 |
// regular rate inventory node |
| 862 |
$node['ratesinventory'] = [ |
| 863 |
$from_date . '_' . $to_date . '_' . $setminlos . $setctawdays . $setctdwdays . '_' . $setmaxlos . '_1_2_' . $rd['cost'] . '_0', |
| 864 |
]; |
| 865 |
} |
| 866 |
|
| 867 |
// set rate data |
| 868 |
$node['pushdata'] = [ |
| 869 |
'pricetype' => $rd['rate_id'], |
| 870 |
'defrate' => $roomrates['cost'], |
| 871 |
'rplans' => [], |
| 872 |
'cur_rplans' => [], |
| 873 |
'rplanarimode' => [], |
| 874 |
]; |
| 875 |
|
| 876 |
// build push data for each channel rate plan according to the Bulk Rates Cache or to the OTA Pricing |
| 877 |
if (($bulk_rates_cache[$id_room][$rd['rate_id']] ?? null)) { |
| 878 |
// Bulk Rates Cache available for this room_id and rate_id |
| 879 |
$node['pushdata']['rplans'] = $bulk_rates_cache[$id_room][$rd['rate_id']]['rplans']; |
| 880 |
$node['pushdata']['cur_rplans'] = $bulk_rates_cache[$id_room][$rd['rate_id']]['cur_rplans']; |
| 881 |
$node['pushdata']['rplanarimode'] = $bulk_rates_cache[$id_room][$rd['rate_id']]['rplanarimode']; |
| 882 |
} |
| 883 |
|
| 884 |
// check the channels mapped for this room and add what was not found in the Bulk Rates Cache, if anything |
| 885 |
foreach ($node['channels'] as $idchannel => $ch_data) { |
| 886 |
if (!isset($node['pushdata']['rplans'][$idchannel])) { |
| 887 |
// this channel was not found in the Bulk Rates Cache. Read data from OTA Pricing |
| 888 |
$otapricing = json_decode($ch_data['otapricing'], true); |
| 889 |
$ch_rplan_id = ''; |
| 890 |
if (is_array($otapricing) && isset($otapricing['RatePlan'])) { |
| 891 |
foreach ($otapricing['RatePlan'] as $rpkey => $rpv) { |
| 892 |
// get the first key (rate plan ID) of the RatePlan array from OTA Pricing |
| 893 |
$ch_rplan_id = $rpkey; |
| 894 |
break; |
| 895 |
} |
| 896 |
} |
| 897 |
|
| 898 |
// prevent Airbnb from being updated if not for the main rate plan only |
| 899 |
if ($idchannel == VikChannelManagerConfig::AIRBNBAPI) { |
| 900 |
if ($rplan_info['is_derived'] || $this->guessOTASecondaryRatePlan($idchannel, $roomrates, ($bulk_rates_cache[$id_room] ?? []))) { |
| 901 |
// skip this channel from updating a derived/secondary rate plan that would not exist |
| 902 |
$ch_rplan_id = ''; |
| 903 |
} |
| 904 |
} |
| 905 |
|
| 906 |
// make sure an OTA rate plan ID was found |
| 907 |
if (empty($ch_rplan_id)) { |
| 908 |
// exclude this channel from being updated |
| 909 |
unset($node['channels'][$idchannel]); |
| 910 |
continue; |
| 911 |
} |
| 912 |
|
| 913 |
// set channel rate plan data |
| 914 |
$node['pushdata']['rplans'][$idchannel] = $ch_rplan_id; |
| 915 |
if ($idchannel == VikChannelManagerConfig::BOOKING) { |
| 916 |
// Default Pricing is used by default, when no data available |
| 917 |
$node['pushdata']['rplanarimode'][$idchannel] = 'person'; |
| 918 |
} |
| 919 |
} |
| 920 |
} |
| 921 |
|
| 922 |
// push update node |
| 923 |
$update_rows[] = $node; |
| 924 |
} |
| 925 |
|
| 926 |
// update rates on the various channels |
| 927 |
$channels_map = []; |
| 928 |
foreach ($update_rows as $update_row) { |
| 929 |
if (!$update_row['channels']) { |
| 930 |
// skip update for this room as no channels are involved |
| 931 |
continue; |
| 932 |
} |
| 933 |
|
| 934 |
// set channels updated |
| 935 |
foreach ($update_row['channels'] as $ch) { |
| 936 |
if (($channels_updated[$ch['idchannel']] ?? [])) { |
| 937 |
continue; |
| 938 |
} |
| 939 |
$channels_map[$ch['idchannel']] = ucfirst($ch['channel']); |
| 940 |
$ota_logo_url = is_object($vcm_logos) ? $vcm_logos->setProvenience($ch['channel'])->getLogoURL() : false; |
| 941 |
$channel_logo = $ota_logo_url !== false ? $ota_logo_url : ''; |
| 942 |
$channels_updated[$ch['idchannel']] = [ |
| 943 |
'id' => $ch['idchannel'], |
| 944 |
'name' => ucfirst($ch['channel']), |
| 945 |
'logo' => $channel_logo |
| 946 |
]; |
| 947 |
} |
| 948 |
|
| 949 |
// prepare request data |
| 950 |
$channels_ids = array_keys($update_row['channels']); |
| 951 |
$channels_rplans = []; |
| 952 |
foreach ($channels_ids as $ch_id) { |
| 953 |
$ch_rplan = isset($update_row['pushdata']['rplans'][$ch_id]) ? $update_row['pushdata']['rplans'][$ch_id] : ''; |
| 954 |
$ch_rplan .= isset($update_row['pushdata']['rplanarimode'][$ch_id]) ? '='.$update_row['pushdata']['rplanarimode'][$ch_id] : ''; |
| 955 |
$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] : ''; |
| 956 |
$channels_rplans[] = $ch_rplan; |
| 957 |
} |
| 958 |
|
| 959 |
$channels = [ |
| 960 |
implode(',', $channels_ids) |
| 961 |
]; |
| 962 |
$chrplans = [ |
| 963 |
implode(',', $channels_rplans) |
| 964 |
]; |
| 965 |
$nodes = [ |
| 966 |
implode(';', $update_row['ratesinventory']) |
| 967 |
]; |
| 968 |
$rooms = [$id_room]; |
| 969 |
$pushvars = [ |
| 970 |
implode(';', [ |
| 971 |
$update_row['pushdata']['pricetype'], |
| 972 |
$update_row['pushdata']['defrate'], |
| 973 |
]) |
| 974 |
]; |
| 975 |
|
| 976 |
// send the request |
| 977 |
$result = $vboConnector->channelsRatesPush($channels, $chrplans, $nodes, $rooms, $pushvars); |
| 978 |
if ($vc_error = $vboConnector->getError(true)) { |
| 979 |
$channels_errors[] = $vc_error; |
| 980 |
continue; |
| 981 |
} |
| 982 |
|
| 983 |
// parse the channels update result and compose success, warnings, errors |
| 984 |
$result_pool = json_decode($result, true); |
| 985 |
foreach (($result_pool ?: []) as $rid => $ch_responses) { |
| 986 |
foreach ($ch_responses as $ch_id => $ch_res) { |
| 987 |
if ($ch_id == 'breakdown' || !is_numeric($ch_id)) { |
| 988 |
// get the rates/dates breakdown of the update request |
| 989 |
$bkdown = $ch_res; |
| 990 |
if (is_array($ch_res)) { |
| 991 |
$bkdown = ''; |
| 992 |
foreach ($ch_res as $bk => $bv) { |
| 993 |
$bkparts = explode('-', $bk); |
| 994 |
if (count($bkparts) == 6) { |
| 995 |
// breakdown key is usually composed of two dates in Y-m-d concatenated with another "-". |
| 996 |
$bkdown .= 'From ' . implode('-', array_slice($bkparts, 0, 3)) . ' - To ' . implode('-', array_slice($bkparts, 3, 3)) . ': ' . $bv . "\n"; |
| 997 |
} else { |
| 998 |
$bkdown .= $bk . ': ' . $bv . "\n"; |
| 999 |
} |
| 1000 |
} |
| 1001 |
// since the Connector does not return breakdown info about the restrictions, we concatenate the response here |
| 1002 |
if ((int) $setminlos > 0) { |
| 1003 |
$bkdown = rtrim($bkdown, "\n"); |
| 1004 |
$bkdown .= ' - Min LOS: ' . $setminlos; |
| 1005 |
if ((int) $setmaxlos > 0) { |
| 1006 |
$bkdown .= ' - Max LOS: ' . $setmaxlos; |
| 1007 |
} |
| 1008 |
$bkdown_ctad_rules = []; |
| 1009 |
if ($setctawdays && $current_cta) { |
| 1010 |
$bkdown_ctad_rules[] = 'CTA: ' . implode(',', $this->weekDaysToShort($current_cta)); |
| 1011 |
} |
| 1012 |
if ($setctdwdays && $current_ctd) { |
| 1013 |
$bkdown_ctad_rules[] = 'CTD: ' . implode(',', $this->weekDaysToShort($current_ctd)); |
| 1014 |
} |
| 1015 |
if ($bkdown_ctad_rules) { |
| 1016 |
$bkdown .= ' [' . implode(' ', $bkdown_ctad_rules) . ']'; |
| 1017 |
} |
| 1018 |
$bkdown .= "\n"; |
| 1019 |
} |
| 1020 |
$bkdown = rtrim($bkdown, "\n"); |
| 1021 |
} |
| 1022 |
if (!isset($channels_bkdown[$ch_id])) { |
| 1023 |
$channels_bkdown[$ch_id] = $bkdown; |
| 1024 |
} else { |
| 1025 |
$channels_bkdown[$ch_id] .= "\n".$bkdown; |
| 1026 |
} |
| 1027 |
continue; |
| 1028 |
} |
| 1029 |
$ch_id = (int)$ch_id; |
| 1030 |
if (substr($ch_res, 0, 6) == 'e4j.OK') { |
| 1031 |
// success |
| 1032 |
if (!isset($channels_success[$ch_id])) { |
| 1033 |
$channels_success[$ch_id] = $channels_map[$ch_id]; |
| 1034 |
} |
| 1035 |
} elseif (substr($ch_res, 0, 11) == 'e4j.warning') { |
| 1036 |
// warning |
| 1037 |
if (!isset($channels_warnings[$ch_id])) { |
| 1038 |
$channels_warnings[$ch_id] = $channels_map[$ch_id].': '.str_replace('e4j.warning.', '', $ch_res); |
| 1039 |
} else { |
| 1040 |
$channels_warnings[$ch_id] .= "\n".str_replace('e4j.warning.', '', $ch_res); |
| 1041 |
} |
| 1042 |
// add the channel also to the successful list in case of Warning |
| 1043 |
if (!isset($channels_success[$ch_id])) { |
| 1044 |
$channels_success[$ch_id] = $channels_map[$ch_id]; |
| 1045 |
} |
| 1046 |
} elseif (substr($ch_res, 0, 9) == 'e4j.error') { |
| 1047 |
// error |
| 1048 |
if (!isset($channels_errors[$ch_id])) { |
| 1049 |
$channels_errors[$ch_id] = $channels_map[$ch_id].': '.str_replace('e4j.error.', '', $ch_res); |
| 1050 |
} else { |
| 1051 |
$channels_errors[$ch_id] .= "\n".str_replace('e4j.error.', '', $ch_res); |
| 1052 |
} |
| 1053 |
} |
| 1054 |
} |
| 1055 |
} |
| 1056 |
} |
| 1057 |
} |
| 1058 |
|
| 1059 |
if ($channels_updated) { |
| 1060 |
/** |
| 1061 |
* We now support chained updates due to derived rate plans. |
| 1062 |
* The "vcm" response property is now an array of objects with equal structure. |
| 1063 |
* |
| 1064 |
* @since 1.16.10 (J) - 1.6.10 (WP) |
| 1065 |
*/ |
| 1066 |
if (!isset($newly_rates['vcm'])) { |
| 1067 |
$newly_rates['vcm'] = []; |
| 1068 |
} |
| 1069 |
|
| 1070 |
// build channels response data for the current rate plan |
| 1071 |
$channels_response_data = [ |
| 1072 |
'rplan_id' => $now_id_price, |
| 1073 |
'rplan_name' => $rate_plan_name, |
| 1074 |
'is_derived' => $rplan_info['is_derived'], |
| 1075 |
'channels_updated' => $channels_updated, |
| 1076 |
]; |
| 1077 |
|
| 1078 |
// set these property only if not empty |
| 1079 |
if ($channels_bkdown) { |
| 1080 |
$channels_response_data['channels_bkdown'] = $channels_bkdown['breakdown']; |
| 1081 |
} |
| 1082 |
if ($channels_success) { |
| 1083 |
$channels_response_data['channels_success'] = $channels_success; |
| 1084 |
} |
| 1085 |
if ($channels_warnings) { |
| 1086 |
$channels_response_data['channels_warnings'] = $channels_warnings; |
| 1087 |
// cache channel warnings |
| 1088 |
$this->channel_warnings = $channels_warnings; |
| 1089 |
} |
| 1090 |
if ($channels_errors) { |
| 1091 |
$channels_response_data['channels_errors'] = $channels_errors; |
| 1092 |
// cache channel errors |
| 1093 |
$this->channel_errors = $channels_errors; |
| 1094 |
} |
| 1095 |
|
| 1096 |
// push channels response data to the pool |
| 1097 |
$newly_rates['vcm'][] = $channels_response_data; |
| 1098 |
|
| 1099 |
// cache the channels updated list by eventually merging what was already set |
| 1100 |
foreach ($channels_updated as $idch => $chinfo) { |
| 1101 |
$this->channels_updated_list[$idch] = $chinfo; |
| 1102 |
} |
| 1103 |
|
| 1104 |
// check for possible conflicts |
| 1105 |
if (!$rplan_info['is_derived'] && !$split_ct_nodes && ($conflicts ?? false) === true) { |
| 1106 |
// trigger an automatic bulk action for uploading the rates to ensure a full refresh |
| 1107 |
VikChannelManager::autoBulkActions([ |
| 1108 |
'from_date' => $from_date, |
| 1109 |
'to_date' => $to_date, |
| 1110 |
'forced_rooms' => [$id_room], |
| 1111 |
'update' => 'rates', |
| 1112 |
]); |
| 1113 |
} |
| 1114 |
} |
| 1115 |
} |
| 1116 |
} |
| 1117 |
|
| 1118 |
return $newly_rates; |
| 1119 |
} |
| 1120 |
|
| 1121 |
/** |
| 1122 |
* Returns the information about the channels updated with |
| 1123 |
* the last rates/restrictions modification request, if any. |
| 1124 |
* |
| 1125 |
* @return array |
| 1126 |
*/ |
| 1127 |
public function getChannelsUpdated() |
| 1128 |
{ |
| 1129 |
$channel_details = []; |
| 1130 |
|
| 1131 |
$vcm_logos = VikBooking::getVcmChannelsLogo('', true); |
| 1132 |
|
| 1133 |
foreach ($this->channels_updated_list as $idchannel => $channel_data) { |
| 1134 |
$small_logo_url = $vcm_logos ? $vcm_logos->setProvenience(strtolower($channel_data['name']))->getSmallLogoURL() : ''; |
| 1135 |
$tiny_logo_url = $vcm_logos ? $vcm_logos->setProvenience(strtolower($channel_data['name']))->getTinyLogoURL() : ''; |
| 1136 |
|
| 1137 |
// set channel details |
| 1138 |
$channel_details[] = [ |
| 1139 |
'id' => $channel_data['id'], |
| 1140 |
'name' => $channel_data['name'], |
| 1141 |
'logo' => $channel_data['logo'], |
| 1142 |
'small_logo' => $small_logo_url, |
| 1143 |
'tiny_logo' => $tiny_logo_url, |
| 1144 |
]; |
| 1145 |
} |
| 1146 |
|
| 1147 |
return $channel_details; |
| 1148 |
} |
| 1149 |
|
| 1150 |
/** |
| 1151 |
* Returns the associative list of channel warning messages. |
| 1152 |
* |
| 1153 |
* @return array |
| 1154 |
*/ |
| 1155 |
public function getChannelWarnings() |
| 1156 |
{ |
| 1157 |
return $this->channel_warnings; |
| 1158 |
} |
| 1159 |
|
| 1160 |
/** |
| 1161 |
* Returns the associative list of channel error messages. |
| 1162 |
* |
| 1163 |
* @return array |
| 1164 |
*/ |
| 1165 |
public function getChannelErrors() |
| 1166 |
{ |
| 1167 |
return $this->channel_errors; |
| 1168 |
} |
| 1169 |
|
| 1170 |
/** |
| 1171 |
* Calculates the restriction modifiers for a specific range of dates and room. |
| 1172 |
* |
| 1173 |
* @param int $start_ts The date range start date timestamp. |
| 1174 |
* @param int $end_ts The date range end date timestamp. |
| 1175 |
* @param int $room_id The VikBooking room ID. |
| 1176 |
* |
| 1177 |
* @return array Numeric list of modifiers (Max LOS, CTA, CTD, split nodes, conflicts). |
| 1178 |
* |
| 1179 |
* @since 1.17.1 (J) - 1.7.1 (WP) |
| 1180 |
*/ |
| 1181 |
public function calculateRestrictionModifiers($start_ts, $end_ts, $room_id) |
| 1182 |
{ |
| 1183 |
// build cache signature |
| 1184 |
$from_dt = date('Y-m-d', $start_ts); |
| 1185 |
$to_dt = date('Y-m-d', $end_ts); |
| 1186 |
$signature = $from_dt . '_' . $to_dt . '_' . $room_id; |
| 1187 |
|
| 1188 |
if (!isset($this->cached_restrictions[$signature])) { |
| 1189 |
// load and cache restrictions |
| 1190 |
$this->cached_restrictions[$signature] = VikBooking::loadRestrictions(true, [$room_id]); |
| 1191 |
} |
| 1192 |
|
| 1193 |
// build default return values |
| 1194 |
$calc_maxlos = 0; |
| 1195 |
$calc_cta = []; |
| 1196 |
$calc_ctd = []; |
| 1197 |
$split_ct_nodes = []; |
| 1198 |
$conflicts = false; |
| 1199 |
|
| 1200 |
if (!($this->cached_restrictions[$signature] ?? [])) { |
| 1201 |
// do not proceed as nothing would be found |
| 1202 |
return [ |
| 1203 |
$calc_maxlos, |
| 1204 |
$calc_cta, |
| 1205 |
$calc_ctd, |
| 1206 |
$split_ct_nodes, |
| 1207 |
$conflicts, |
| 1208 |
]; |
| 1209 |
} |
| 1210 |
|
| 1211 |
// list of week days involved |
| 1212 |
$wdays_involved = []; |
| 1213 |
|
| 1214 |
// loop through the requested range of dates |
| 1215 |
$infostart = getdate($start_ts); |
| 1216 |
$infofirst = $infostart; |
| 1217 |
while ($infostart[0] > 0 && $infostart[0] <= $end_ts) { |
| 1218 |
// calculate timestamps |
| 1219 |
$tomorrow_ts = mktime(0, 0, 0, $infostart['mon'], ($infostart['mday'] + 1), $infostart['year']); |
| 1220 |
$today_mid_ts = mktime(0, 0, 0, $infostart['mon'], $infostart['mday'], $infostart['year']); |
| 1221 |
|
| 1222 |
// set week-day involved |
| 1223 |
$wdays_involved[] = $infostart['wday']; |
| 1224 |
|
| 1225 |
// calculate restrictions |
| 1226 |
$restrictions = VikBooking::parseSeasonRestrictions($today_mid_ts, $tomorrow_ts, 1, $this->cached_restrictions[$signature] ?? []); |
| 1227 |
|
| 1228 |
// check for max LOS |
| 1229 |
if ($restrictions['maxlos'] ?? null) { |
| 1230 |
if (!$calc_maxlos) { |
| 1231 |
$calc_maxlos = (int) $restrictions['maxlos']; |
| 1232 |
} |
| 1233 |
} |
| 1234 |
|
| 1235 |
// check for CTA week-days |
| 1236 |
if ($restrictions['cta'] ?? []) { |
| 1237 |
if (!$calc_cta) { |
| 1238 |
$calc_cta = (array) $restrictions['cta']; |
| 1239 |
$conflicts = $conflicts || !in_array($infostart['wday'], (array) $restrictions['cta']); |
| 1240 |
} else { |
| 1241 |
$conflicts = $conflicts || $calc_cta != (array) $restrictions['cta']; |
| 1242 |
} |
| 1243 |
} elseif ($calc_cta) { |
| 1244 |
$conflicts = true; |
| 1245 |
} |
| 1246 |
|
| 1247 |
// check for CTD week-days |
| 1248 |
if ($restrictions['ctd'] ?? []) { |
| 1249 |
if (!$calc_ctd) { |
| 1250 |
$calc_ctd = (array) $restrictions['ctd']; |
| 1251 |
$conflicts = $conflicts || !in_array($infostart['wday'], (array) $restrictions['ctd']); |
| 1252 |
} else { |
| 1253 |
$conflicts = $conflicts || $calc_ctd != (array) $restrictions['ctd']; |
| 1254 |
} |
| 1255 |
} elseif ($calc_ctd) { |
| 1256 |
$conflicts = true; |
| 1257 |
} |
| 1258 |
|
| 1259 |
// go to next day |
| 1260 |
$infostart = getdate($tomorrow_ts); |
| 1261 |
} |
| 1262 |
|
| 1263 |
if ($calc_cta) { |
| 1264 |
// ensure it's a list of integers representing week-days |
| 1265 |
$calc_cta = array_map(function($w) { |
| 1266 |
return (int) str_replace('-', '', (string) $w); |
| 1267 |
}, $calc_cta); |
| 1268 |
|
| 1269 |
if (count($wdays_involved) < 7) { |
| 1270 |
// filter the CTA week days according to the nights involved |
| 1271 |
$calc_cta = array_filter($calc_cta, function($w) use ($wdays_involved) { |
| 1272 |
return in_array($w, $wdays_involved); |
| 1273 |
}); |
| 1274 |
} |
| 1275 |
} |
| 1276 |
|
| 1277 |
if ($calc_ctd) { |
| 1278 |
// ensure it's a list of integers representing week-days |
| 1279 |
$calc_ctd = array_map(function($w) { |
| 1280 |
return (int) str_replace('-', '', (string) $w); |
| 1281 |
}, $calc_ctd); |
| 1282 |
|
| 1283 |
if (count($wdays_involved) < 7) { |
| 1284 |
// filter the CTD week days according to the nights involved |
| 1285 |
$calc_ctd = array_filter($calc_ctd, function($w) use ($wdays_involved) { |
| 1286 |
return in_array($w, $wdays_involved); |
| 1287 |
}); |
| 1288 |
} |
| 1289 |
} |
| 1290 |
|
| 1291 |
if ($conflicts && count($wdays_involved) < 7 && ($calc_cta || $calc_ctd)) { |
| 1292 |
// conflicting cta/ctd restrictions for a range of dates less than a week may be split into multiple OTA nodes |
| 1293 |
$day_ctad_list = []; |
| 1294 |
for ($w = 0; $w < count($wdays_involved); $w++) { |
| 1295 |
// build day individual cta/ctd restrictions |
| 1296 |
$split_ts = mktime(0, 0, 0, $infofirst['mon'], $infofirst['mday'] + $w, $infofirst['year']); |
| 1297 |
$day_ctad_list[] = [ |
| 1298 |
'ts' => $split_ts, |
| 1299 |
'day' => date('Y-m-d', $split_ts), |
| 1300 |
'cta' => in_array($wdays_involved[$w], $calc_cta) ? $calc_cta : [], |
| 1301 |
'ctd' => in_array($wdays_involved[$w], $calc_ctd) ? $calc_ctd : [], |
| 1302 |
]; |
| 1303 |
} |
| 1304 |
|
| 1305 |
// attempt to merge the split cta/ctd nodes for consecutive dates with equal rules |
| 1306 |
$split_from_ts = 0; |
| 1307 |
$split_to_ts = 0; |
| 1308 |
$split_cta = []; |
| 1309 |
$split_ctd = []; |
| 1310 |
foreach ($day_ctad_list as $ct_node_restr) { |
| 1311 |
if (!$split_from_ts) { |
| 1312 |
// initialize values |
| 1313 |
$split_from_ts = $ct_node_restr['ts']; |
| 1314 |
$split_to_ts = $ct_node_restr['ts']; |
| 1315 |
$split_cta = $ct_node_restr['cta']; |
| 1316 |
$split_ctd = $ct_node_restr['ctd']; |
| 1317 |
continue; |
| 1318 |
} |
| 1319 |
if ($split_cta != $ct_node_restr['cta'] || $split_ctd != $ct_node_restr['ctd']) { |
| 1320 |
// close previous interval |
| 1321 |
$split_ct_nodes[] = [ |
| 1322 |
'from_ts' => $split_from_ts, |
| 1323 |
'to_ts' => $split_to_ts, |
| 1324 |
'cta' => $split_cta, |
| 1325 |
'ctd' => $split_ctd, |
| 1326 |
]; |
| 1327 |
// start new values |
| 1328 |
$split_from_ts = $ct_node_restr['ts']; |
| 1329 |
$split_to_ts = $ct_node_restr['ts']; |
| 1330 |
$split_cta = $ct_node_restr['cta']; |
| 1331 |
$split_ctd = $ct_node_restr['ctd']; |
| 1332 |
} else { |
| 1333 |
// increase till day timestamp |
| 1334 |
$split_to_ts = $ct_node_restr['ts']; |
| 1335 |
} |
| 1336 |
} |
| 1337 |
if (!$split_ct_nodes || $split_ct_nodes[count($split_ct_nodes) - 1]['to_ts'] != $split_from_ts) { |
| 1338 |
// close last or only interval |
| 1339 |
$split_ct_nodes[] = [ |
| 1340 |
'from_ts' => $split_from_ts, |
| 1341 |
'to_ts' => $split_to_ts, |
| 1342 |
'cta' => $split_cta, |
| 1343 |
'ctd' => $split_ctd, |
| 1344 |
]; |
| 1345 |
} |
| 1346 |
|
| 1347 |
// normalize timestamps |
| 1348 |
foreach ($split_ct_nodes as &$split_ct_node) { |
| 1349 |
$split_ct_node['from_dt'] = date('Y-m-d', $split_ct_node['from_ts']); |
| 1350 |
$split_ct_node['to_dt'] = date('Y-m-d', $split_ct_node['to_ts']); |
| 1351 |
} |
| 1352 |
|
| 1353 |
// unset last reference |
| 1354 |
unset($split_ct_node); |
| 1355 |
} |
| 1356 |
|
| 1357 |
// double check for possible conflicts |
| 1358 |
$conflicts = count($wdays_involved) < 7 ? false : $conflicts; |
| 1359 |
|
| 1360 |
return [ |
| 1361 |
// max LOS |
| 1362 |
$calc_maxlos, |
| 1363 |
// cta week-days |
| 1364 |
$calc_cta, |
| 1365 |
// ctd week-days |
| 1366 |
$calc_ctd, |
| 1367 |
// split cta/ctd nodes (range of dates) |
| 1368 |
$split_ct_nodes, |
| 1369 |
// whether some dates have conflicting modifiers |
| 1370 |
$conflicts, |
| 1371 |
]; |
| 1372 |
} |
| 1373 |
|
| 1374 |
/** |
| 1375 |
* Detects if we are updating a secondary rate plan, probably not supported by the OTA. |
| 1376 |
* Useful to prevent non-refundable rate plans to be transmitted to channels like Airbnb. |
| 1377 |
* |
| 1378 |
* @param int $idchannel The channel unique key. |
| 1379 |
* @param array $room_rates The rate plan record details. |
| 1380 |
* @param array $room_cahe The Bulk Rates Cache for the current room-type. |
| 1381 |
* |
| 1382 |
* @return bool True if a secondary rate plan was detected. |
| 1383 |
*/ |
| 1384 |
protected function guessOTASecondaryRatePlan($idchannel, array $room_rates, array $room_cache) |
| 1385 |
{ |
| 1386 |
$room_type_id = $room_rates['idroom'] ?? 0; |
| 1387 |
$rate_plan_id = $room_rates['idprice'] ?? 0; |
| 1388 |
$rate_plan_name = $room_rates['name'] ?? 'Standard'; |
| 1389 |
|
| 1390 |
if (!$room_cache) { |
| 1391 |
// unable to perform a detection |
| 1392 |
return false; |
| 1393 |
} |
| 1394 |
|
| 1395 |
// check how many rate plans are linked to the given channel identifier |
| 1396 |
$cached_ota_rate_plans = []; |
| 1397 |
|
| 1398 |
foreach ($room_cache as $price_id => $plan_cache) { |
| 1399 |
if (is_array($plan_cache) && ($plan_cache['rplans'][$idchannel] ?? null)) { |
| 1400 |
$cached_ota_rate_plans[] = $price_id; |
| 1401 |
} |
| 1402 |
} |
| 1403 |
|
| 1404 |
if (!$cached_ota_rate_plans) { |
| 1405 |
// unable to perform a detection due to missing bulk rates cache data |
| 1406 |
return false; |
| 1407 |
} |
| 1408 |
|
| 1409 |
if (in_array($rate_plan_id, $cached_ota_rate_plans)) { |
| 1410 |
// this rate plan was updated through a bulk action, so it's reliable |
| 1411 |
return false; |
| 1412 |
} |
| 1413 |
|
| 1414 |
// access the room rate plan relations |
| 1415 |
if (!($this->room_rate_plans[$room_type_id] ?? [])) { |
| 1416 |
$this->room_rate_plans[$room_type_id] = VBORoomHelper::getInstance()->getRatePlans($room_type_id); |
| 1417 |
} |
| 1418 |
|
| 1419 |
if (count(($this->room_rate_plans[$room_type_id] ?: [])) < 2) { |
| 1420 |
// this room-type has got just one rate plan assigned, so it must be a parent rate |
| 1421 |
return false; |
| 1422 |
} |
| 1423 |
|
| 1424 |
if (stripos($rate_plan_name, 'Standard') !== false) { |
| 1425 |
// we assume this a main rate plan |
| 1426 |
return false; |
| 1427 |
} |
| 1428 |
|
| 1429 |
// this is probably a secondary rate plan for this OTA |
| 1430 |
return true; |
| 1431 |
} |
| 1432 |
|
| 1433 |
/** |
| 1434 |
* Used for breakdown purposes, converts a list of week-day indexes. |
| 1435 |
* |
| 1436 |
* @param array $wdays List of zero-based week day indexes. |
| 1437 |
* |
| 1438 |
* @return array List of readable week days. |
| 1439 |
* |
| 1440 |
* @since 1.17.1 (J) - 1.7.1 (WP) |
| 1441 |
*/ |
| 1442 |
protected function weekDaysToShort(array $wdays) |
| 1443 |
{ |
| 1444 |
$map = [ |
| 1445 |
'Sun', |
| 1446 |
'Mon', |
| 1447 |
'Tue', |
| 1448 |
'Wed', |
| 1449 |
'Thu', |
| 1450 |
'Fri', |
| 1451 |
'Sat', |
| 1452 |
]; |
| 1453 |
|
| 1454 |
return array_map(function($wday_index) use ($map) { |
| 1455 |
$wday_index = (int) $wday_index; |
| 1456 |
return $map[$wday_index] ?? $wday_index; |
| 1457 |
}, $wdays); |
| 1458 |
} |
| 1459 |
} |
| 1460 |
|