PluginProbe
VikBooking Hotel Booking Engine & PMS / trunk
VikBooking Hotel Booking Engine & PMS vtrunk
1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.7.3 All 36 releases
vikbooking / admin / helpers / src / room / helper.php

helper.php in VikBooking Hotel Booking Engine & PMS trunk, at admin/helpers/src/room/helper.php

1,213 lines 47.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage core
5 * @author Alessio Gaggii - E4J s.r.l.
6 * @copyright Copyright (C) 2022 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 * Helper class to handle rooms data.
16 *
17 * @since 1.15.1 (J) - 1.5.2 (WP)
18 */
19 final class VBORoomHelper extends JObject
20 {
21 /**
22 * The singleton instance of the class.
23 *
24 * @var VBORoomHelper
25 */
26 private static $instance = null;
27
28 /**
29 * Proxy to construct the object.
30 *
31 * @param array|object $data optional data to bind.
32 * @param boolean $anew true for forcing a new instance.
33 *
34 * @return self
35 */
36 public static function getInstance($data = [], $anew = false)
37 {
38 if (is_null(static::$instance) || $anew) {
39 static::$instance = new static($data);
40 }
41
42 return static::$instance;
43 }
44
45 /**
46 * Checks whether a room has been configured with LOS pricing rules.
47 * VCM comes with a similar built-in method, but we need this feature
48 * to be available also for those who only use VBO. Moreover, this method
49 * can identify the first night with a non-proportional rate.
50 *
51 * @param int $idroom the ID of the room in VBO.
52 * @param int $idprice the optional rate plan ID in VBO.
53 * @param bool $get_nights whether to return the number of nights when LOS starts.
54 *
55 * @return bool|int false on failure or if no LOS prices found, true or int otherwise.
56 */
57 public static function hasLosRecords($idroom, $idprice = 0, $get_nights = false)
58 {
59 if (empty($idroom)) {
60 return false;
61 }
62
63 $dbo = JFactory::getDbo();
64 $q = "SELECT * FROM `#__vikbooking_dispcost` WHERE `idroom`=" . (int)$idroom . (!empty($idprice) ? " AND `idprice`=" . (int)$idprice : '') . " ORDER BY `days` ASC;";
65 $dbo->setQuery($q);
66 $los_data = $dbo->loadAssocList();
67 if (!$los_data) {
68 return false;
69 }
70
71 $los_pricing = array();
72 foreach ($los_data as $cost) {
73 if (!isset($los_pricing[$cost['days']])) {
74 $los_pricing[$cost['days']] = array();
75 }
76 array_push($los_pricing[$cost['days']], $cost);
77 }
78 // sort by number of nights
79 ksort($los_pricing);
80
81 // compose lowest costs per rate plan
82 $base_costs = array();
83 foreach ($los_pricing as $nights => $costs) {
84 foreach ($costs as $rplan_cost) {
85 $base_costs[$rplan_cost['idprice']] = ($rplan_cost['cost'] / $rplan_cost['days']);
86 }
87 // we take the costs for the lowest number of nights
88 break;
89 }
90
91 // check if rates change depending on the number of nights of stay
92 foreach ($los_pricing as $nights => $costs) {
93 foreach ($costs as $rplan_cost) {
94 $base_cost = ($rplan_cost['cost'] / $rplan_cost['days']);
95 if (isset($base_costs[$rplan_cost['idprice']]) && round($base_costs[$rplan_cost['idprice']], 2) != round($base_cost, 2)) {
96 /**
97 * Average rates should be compared after applying rounding or we may face issues.
98 * For example, 383.97 / 3 = 127.99, but it's actually = 127.99000000000001 with
99 * an absolute number for the difference with 127.99 of 1.4210854715202004E-14
100 * which results to be greater than 0 but less than 1. Therefore, we also allow
101 * an absolute number for the difference of 0.05 cents for a proper check.
102 */
103 $price_diff = abs($base_costs[$rplan_cost['idprice']] - $base_cost);
104 if ($price_diff > 0.05) {
105 // this is a non-proportional cost per night, so LOS records have been defined
106 return $get_nights ? $nights : true;
107 }
108 }
109 }
110 }
111
112 // all costs per night were proportional
113 return false;
114 }
115
116 /**
117 * Calculates the effective Min LOS from the Rates Table
118 * for the given room and rate plan ID.
119 *
120 * @param int $idroom the ID of the room-type on the website.
121 * @param int $idprice the ID of the rate plan on the website.
122 *
123 * @return int the effective Min LOS or 0.
124 *
125 * @since 1.18.0 (J) - 1.8.0 (WP)
126 */
127 public static function calcEffectiveMinLOS($idroom, $idprice)
128 {
129 if (empty($idroom) || empty($idprice)) {
130 return 0;
131 }
132
133 $dbo = JFactory::getDbo();
134
135 $dbo->setQuery(
136 $dbo->getQuery(true)
137 ->select('MIN(' . $dbo->qn('days') . ')')
138 ->from($dbo->qn('#__vikbooking_dispcost'))
139 ->where($dbo->qn('idroom') . ' = ' . (int) $idroom)
140 ->where($dbo->qn('idprice') . ' = ' . (int) $idprice)
141 , 0, 1);
142
143 return (int) $dbo->loadResult();
144 }
145
146 /**
147 * Calculates the effective Max LOS from the Rates Table
148 * for the given room and rate plan ID.
149 *
150 * @param int $idroom the ID of the room-type on the website.
151 * @param int $idprice the ID of the rate plan on the website.
152 *
153 * @return int the effective Max LOS or 0.
154 *
155 * @since 1.18.0 (J) - 1.8.0 (WP)
156 */
157 public static function calcEffectiveMaxLOS($idroom, $idprice)
158 {
159 if (empty($idroom) || empty($idprice)) {
160 return 0;
161 }
162
163 $dbo = JFactory::getDbo();
164
165 $dbo->setQuery(
166 $dbo->getQuery(true)
167 ->select('MAX(' . $dbo->qn('days') . ')')
168 ->from($dbo->qn('#__vikbooking_dispcost'))
169 ->where($dbo->qn('idroom') . ' = ' . (int) $idroom)
170 ->where($dbo->qn('idprice') . ' = ' . (int) $idprice)
171 , 0, 1);
172
173 return (int) $dbo->loadResult();
174 }
175
176 /**
177 * Calculates the highest Max LOS from the Rates Table for the given room.
178 * Useful to tell the highest max LOS allowed in a multi-rate plan environment.
179 *
180 * @param int $idroom the ID of the room-type on the website.
181 * @param bool $needs_multi_rate whether the rates table requires multiple rate plans.
182 *
183 * @return int the effective Max LOS or 0.
184 *
185 * @since 1.18.6 (J) - 1.8.6 (WP)
186 */
187 public static function calcHighestMaxLOS(int $idroom, bool $needs_multi_rate = false)
188 {
189 if (empty($idroom)) {
190 return 0;
191 }
192
193 $dbo = JFactory::getDbo();
194
195 $dbo->setQuery(
196 $dbo->getQuery(true)
197 ->select('MAX(' . $dbo->qn('days') . ')')
198 ->from($dbo->qn('#__vikbooking_dispcost'))
199 ->where($dbo->qn('idroom') . ' = ' . (int) $idroom)
200 , 0, 1);
201
202 $highest_max_los = (int) $dbo->loadResult();
203
204 if ($needs_multi_rate) {
205 $dbo->setQuery(
206 $dbo->getQuery(true)
207 ->select('COUNT(DISTINCT ' . $dbo->qn('idprice') . ')')
208 ->from($dbo->qn('#__vikbooking_dispcost'))
209 ->where($dbo->qn('idroom') . ' = ' . (int) $idroom)
210 , 0, 1);
211
212 $total_room_rplans = (int) $dbo->loadResult();
213
214 if ($total_room_rplans < 2) {
215 // this room has got rates defined for a single rate plan
216 return 0;
217 }
218 }
219
220 return $highest_max_los;
221 }
222
223 /**
224 * Gets the available room upgrade options, if any.
225 *
226 * @param VikBookingTranslator $vbo_tn the translator object.
227 *
228 * @return array list of available upgrade options,
229 * or empty array if nothing availabe.
230 *
231 * @since 1.16.0 (J) - 1.6.0 (WP)
232 */
233 public function getUpgradeOptions($vbo_tn = null)
234 {
235 $booking = $this->get('booking', []);
236 $rooms = $this->get('rooms', []);
237
238 if (!$booking || !$rooms || $booking['status'] != 'confirmed') {
239 return [];
240 }
241
242 $dbo = JFactory::getDbo();
243 $config = VBOFactory::getConfig();
244
245 $upgrade_options = [];
246 $room_ids = [];
247
248 foreach ($rooms as $num => $broom) {
249 if (empty($broom['idroom']) || empty($broom['idtar'])) {
250 // room must have a valid tariff assigned
251 continue;
252 }
253 $room_upgrade_options = $config->getArray('room_upgrade_options_' . $broom['idroom'], []);
254 if (empty($room_upgrade_options) || empty($room_upgrade_options['rooms'])) {
255 // no relations for this room
256 continue;
257 }
258 // fetch the original tariff for this room
259 $orig_tariff = $this->getTariffData($broom['idtar']);
260 if (!$orig_tariff) {
261 // unable to get the original tariff information for this room booked
262 continue;
263 }
264 // push suitable rooms
265 $upgrade_options[$num] = [
266 'rooms' => array_map('intval', array_filter(array_unique($room_upgrade_options['rooms']))),
267 'discount' => (!empty($room_upgrade_options['discount']) ? (float)$room_upgrade_options['discount'] : 0),
268 'tariff' => $orig_tariff,
269 'r_costs' => [],
270 ];
271 $room_ids = array_merge($room_ids, $room_upgrade_options['rooms']);
272 }
273
274 if (!$upgrade_options) {
275 return [];
276 }
277
278 // get all room IDs involved
279 $room_ids = array_map('intval', array_filter(array_unique($room_ids)));
280
281 $q = "SELECT * FROM `#__vikbooking_rooms` WHERE `id` IN (" . implode(', ', $room_ids) . ") AND `avail`=1;";
282 $dbo->setQuery($q);
283 $room_records = $dbo->loadAssocList();
284 if (!$room_records) {
285 return [];
286 }
287 if ($vbo_tn) {
288 // translate rooms
289 $vbo_tn->translateContents($room_records, '#__vikbooking_rooms');
290 }
291
292 // build up an associative array of room infos
293 $room_infos = [];
294 foreach ($room_records as $room_record) {
295 $room_infos[$room_record['id']] = $this->prepareCMSContents($room_record, ['info', 'smalldesc']);
296 }
297 unset($room_records);
298
299 // keep the count of the room units suggested
300 $room_units_counter = [];
301
302 // filter the suitable rooms by rate plan, and calculate the costs
303 foreach ($upgrade_options as $num => $upgrade_option) {
304 // build the costs for each upgrade room option
305 $upgrade_room_costs = [];
306 // parse all rooms compatible
307 foreach ($upgrade_option['rooms'] as $rkey => $rid) {
308 // find the same tariff for this room and nights
309 $room_same_tariff = $this->findTariff($rid, $upgrade_option['tariff']['days'], $upgrade_option['tariff']['idprice']);
310 if (!$room_same_tariff || !isset($room_infos[$rid])) {
311 // this room is not suited
312 unset($upgrade_options[$num]['rooms'][$rkey]);
313 continue;
314 }
315
316 // count the actual number of room remaining units
317 $use_room_units = $room_infos[$rid]['units'];
318 if (isset($room_units_counter[$rid])) {
319 $use_room_units -= $room_units_counter[$rid];
320 }
321
322 // make sure the room is bookable on these dates (restrictions are ignored)
323 if (!VikBooking::roomBookable($rid, $use_room_units, $booking['checkin'], $booking['checkout'])) {
324 // room is not available for upgrade
325 unset($upgrade_options[$num]['rooms'][$rkey]);
326 continue;
327 }
328
329 // update room units counter
330 if (!isset($room_units_counter[$rid])) {
331 $room_units_counter[$rid] = 0;
332 }
333 $room_units_counter[$rid]++;
334
335 // apply seasonal rates
336 $tar = VikBooking::applySeasonsRoom([$room_same_tariff], $booking['checkin'], $booking['checkout']);
337
338 // apply OBP rules
339 $tar = $this->applyOBPRules($tar, $room_infos[$rid], $rooms[$num]['adults']);
340
341 // apply upgrade discount (if any) and calculate upgrade cost
342 foreach ($tar as $tk => $tv) {
343 $tar[$tk]['upgrade_cost'] = $upgrade_option['discount'] > 0 ? round(($tv['cost'] * (100 - $upgrade_option['discount']) / 100), 2) : $tv['cost'];
344 }
345
346 // push room tariff (just one rate plan, the originally booked one)
347 $upgrade_room_costs[$rid] = $tar[0];
348 }
349
350 if (!count($upgrade_options[$num]['rooms'])) {
351 // no more suitable rooms
352 unset($upgrade_options[$num]);
353 continue;
354 }
355
356 // sort by price descending (most expensive on top)
357 $sort_map = [];
358 foreach ($upgrade_room_costs as $rid => $tar) {
359 $sort_map[$rid] = $tar['upgrade_cost'];
360 }
361 arsort($sort_map);
362
363 // replace values with sorted ordering
364 $cp_upgrade_room_costs = [];
365 foreach ($sort_map as $rid => $sorted) {
366 $cp_upgrade_room_costs[$rid] = $upgrade_room_costs[$rid];
367 }
368 $upgrade_room_costs = $cp_upgrade_room_costs;
369
370 // set upgrade room costs
371 $upgrade_options[$num]['r_costs'] = $upgrade_room_costs;
372 }
373
374 if (!count($upgrade_options)) {
375 return [];
376 }
377
378 // return the associative array information
379 return [
380 'upgrade' => $upgrade_options,
381 'rooms' => $room_infos,
382 ];
383 }
384
385 /**
386 * Gets the record details about a specific tariff ID.
387 *
388 * @param int $idtar the ID of the room-tariff.
389 *
390 * @return array record found, or empty array.
391 *
392 * @since 1.16.0 (J) - 1.6.0 (WP)
393 */
394 public function getTariffData($idtar)
395 {
396 $dbo = JFactory::getDbo();
397
398 $dbo->setQuery("SELECT * FROM `#__vikbooking_dispcost` WHERE `id` = " . (int)$idtar, 0, 1);
399 $tariff = $dbo->loadAssoc();
400 if (!$tariff) {
401 return [];
402 }
403
404 return $tariff;
405 }
406
407 /**
408 * Finds a tariff for the given rate plan ID, room and nights.
409 *
410 * @param int $rid the room ID.
411 * @param int $nights the number of nights of stay.
412 * @param int $idprice the rate plan ID.
413 *
414 * @return array record found or empty array.
415 *
416 * @since 1.16.0 (J) - 1.6.0 (WP)
417 */
418 public function findTariff($rid, $nights, $idprice)
419 {
420 $dbo = JFactory::getDbo();
421
422 $q = "SELECT `t`.*, `p`.`name` AS `rate_plan_name` FROM `#__vikbooking_dispcost` AS `t`
423 LEFT JOIN `#__vikbooking_prices` AS `p` ON `t`.`idprice`=`p`.`id`
424 WHERE `t`.`idroom` = " . (int)$rid . " AND `t`.`days`=" . (int)$nights . " AND `t`.`idprice`=" . (int)$idprice;
425 $dbo->setQuery($q, 0, 1);
426 $tariff = $dbo->loadAssoc();
427 if (!$tariff) {
428 return [];
429 }
430
431 return $tariff;
432 }
433
434 /**
435 * Applies the OBP rules over an array of tariffs.
436 *
437 * @param array $tar list of tariff records, one per rate plan, after seasonal rates.
438 * @param array $room the room (or order-room) record for which tariffs where loaded.
439 * @param int $adults the number of adults to consider.
440 *
441 * @return array original tariffs array with OBP costs applied.
442 *
443 * @since 1.16.0 (J) - 1.6.0 (WP)
444 */
445 public function applyOBPRules(array $tar, array $room, $adults = 2)
446 {
447 // check for different usage
448 if (!isset($room['fromadult']) || $room['fromadult'] > $adults || $room['toadult'] < $adults) {
449 return $tar;
450 }
451
452 // check for room ID
453 $use_room_id = isset($room['idroom']) ? $room['idroom'] : $room['id'];
454
455 // different usage
456 $diffusageprice = VikBooking::loadAdultsDiff($use_room_id, $adults);
457
458 /**
459 * Memorize immediately the OBP rules defined at room-level in order to avoid conflicts
460 * with rate plans with and without OBP overrides defined at rate plan level through SP.
461 */
462 $orig_diffusage = $diffusageprice;
463
464 // occupancy overrides
465 $occ_ovr = VikBooking::occupancyOverrideExists($tar, $adults);
466 $diffusageprice = $occ_ovr !== false ? $occ_ovr : $diffusageprice;
467
468 if (!$diffusageprice) {
469 return $tar;
470 }
471
472 // set a charge or discount to the price(s) for the different usage of the room
473 foreach ($tar as $kpr => $vpr) {
474 // occupancy override
475 $diffusageprice = isset($vpr['occupancy_ovr']) && isset($vpr['occupancy_ovr'][$adults]) ? $vpr['occupancy_ovr'][$adults] : $orig_diffusage;
476
477 // set usage of the room
478 $tar[$kpr]['diffusage'] = $adults;
479
480 if ($diffusageprice['chdisc'] == 1) {
481 // charge
482 if ($diffusageprice['valpcent'] == 1) {
483 // fixed value
484 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
485 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
486 $tar[$kpr]['diffusagecost'] = "+" . $aduseval;
487 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
488 $tar[$kpr]['cost'] = $vpr['cost'] + $aduseval;
489 } else {
490 // percentage value
491 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
492 $aduseval = $diffusageprice['pernight'] == 1 ? round(($vpr['cost'] * $diffusageprice['value'] / 100) * $tar[$kpr]['days'] + $vpr['cost'], 2) : round(($vpr['cost'] * (100 + $diffusageprice['value']) / 100), 2);
493 $tar[$kpr]['diffusagecost'] = "+" . $diffusageprice['value'] . "%";
494 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
495 $tar[$kpr]['cost'] = $aduseval;
496 }
497 } else {
498 // discount
499 if ($diffusageprice['valpcent'] == 1) {
500 // fixed value
501 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
502 $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
503 $tar[$kpr]['diffusagecost'] = "-" . $aduseval;
504 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
505 $tar[$kpr]['cost'] = $vpr['cost'] - $aduseval;
506 } else {
507 // percentage value
508 $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
509 $aduseval = $diffusageprice['pernight'] == 1 ? round($vpr['cost'] - ((($vpr['cost'] / $tar[$kpr]['days']) * $diffusageprice['value'] / 100) * $tar[$kpr]['days']), 2) : round(($vpr['cost'] * (100 - $diffusageprice['value']) / 100), 2);
510 $tar[$kpr]['diffusagecost'] = "-" . $diffusageprice['value'] . "%";
511 $tar[$kpr]['room_base_cost'] = $vpr['cost'];
512 $tar[$kpr]['cost'] = $aduseval;
513 }
514 }
515 }
516
517 // return the array of tariffs with OBP included
518 return $tar;
519 }
520
521 /**
522 * Prepares some description strings for the current CMS, by triggering
523 * the necessary platform-related functions for third party plugins.
524 *
525 * @param array $room_record the room record to prepare.
526 * @param array $keys list of record keys to prepare.
527 *
528 * @return array the original array given with keys prepared.
529 *
530 * @since 1.16.0 (J) - 1.6.0 (WP)
531 */
532 public function prepareCMSContents(array $room_record, array $keys)
533 {
534 foreach ($keys as $key) {
535 if (!isset($room_record[$key])) {
536 continue;
537 }
538
539 if (VBOPlatformDetection::isWordPress()) {
540 /**
541 * @wponly we try to parse any shortcode inside the HTML description of the room
542 */
543 $room_record[$key] = do_shortcode(wpautop($room_record[$key]));
544 } else {
545 // BEGIN: Joomla Content Plugins Rendering
546 JPluginHelper::importPlugin('content');
547
548 $myItem = JTable::getInstance('content');
549
550 $myItem->text = $room_record[$key];
551 $objparams = array();
552 if (class_exists('JEventDispatcher')) {
553 $dispatcher = JEventDispatcher::getInstance();
554 $dispatcher->trigger('onContentPrepare', array('com_vikbooking.roomdetails', &$myItem, &$objparams, 0));
555 } else {
556 /**
557 * @joomla4only
558 */
559 $dispatcher = JFactory::getApplication();
560 if (method_exists($dispatcher, 'triggerEvent')) {
561 $dispatcher->triggerEvent('onContentPrepare', array('com_vikbooking.roomdetails', &$myItem, &$objparams, 0));
562 }
563 }
564 $room_record[$key] = $myItem->text;
565 // END: Joomla Content Plugins Rendering
566 }
567 }
568
569 return $room_record;
570 }
571
572 /**
573 * Gets an associative list of rate plans with a few pricing information for the given room.
574 *
575 * @param int $rid the VBO room id.
576 * @param int $rplan_id optional rate plan ID to get.
577 *
578 * @return array associative list of rate plans for the given room or specific rate plan.
579 *
580 * @since 1.16.3 (J) - 1.6.3 (WP)
581 */
582 public function getRatePlans($rid = 0, $rplan_id = 0)
583 {
584 if (empty($rid)) {
585 $rid = $this->get('id', 0);
586 }
587
588 $dbo = JFactory::getDbo();
589
590 $q = $dbo->getQuery(true)
591 ->select([
592 $dbo->qn('r.id'),
593 $dbo->qn('r.idroom'),
594 $dbo->qn('r.days'),
595 $dbo->qn('r.idprice'),
596 $dbo->qn('r.cost'),
597 $dbo->qn('p.name'),
598 $dbo->qn('p.minlos'),
599 $dbo->qn('p.derived_id'),
600 ])
601 ->from($dbo->qn('#__vikbooking_dispcost', 'r'))
602 ->leftJoin($dbo->qn('#__vikbooking_prices', 'p') . ' ON ' . $dbo->qn('r.idprice') . ' = ' . $dbo->qn('p.id'))
603 ->where($dbo->qn('r.idroom') . ' = ' . (int)$rid)
604 ->order($dbo->qn('r.days') . ' ASC')
605 ->order($dbo->qn('r.cost') . ' ASC');
606
607 $dbo->setQuery($q, 0, 50);
608
609 $tariffs = $dbo->loadObjectList();
610
611 if (!$tariffs) {
612 return [];
613 }
614
615 $parsed_room_prices = [];
616 foreach ($tariffs as $rrk => $rrv) {
617 if (isset($parsed_room_prices[$rrv->idprice])) {
618 unset($tariffs[$rrk]);
619 continue;
620 }
621 $tariffs[$rrk]->cost = round(($rrv->cost / $rrv->days), 2);
622 $tariffs[$rrk]->days = 1;
623 $parsed_room_prices[$rrv->idprice] = 1;
624 }
625
626 $tariffs = array_values($tariffs);
627
628 $room_rate_plans = [];
629 foreach ($tariffs as $rplan) {
630 if ($rplan_id && $rplan_id == $rplan->idprice) {
631 return (array)$rplan;
632 }
633
634 $room_rate_plans[] = [
635 'id' => $rplan->idprice,
636 'name' => $rplan->name,
637 'cost' => $rplan->cost,
638 'minlos' => $rplan->minlos,
639 'derived_id' => $rplan->derived_id,
640 ];
641 }
642
643 if ($rplan_id) {
644 return [];
645 }
646
647 return $room_rate_plans;
648 }
649
650 /**
651 * Calculates if the provided booking information require a split payment for the damage deposit.
652 *
653 * @param array $booking The booking record data.
654 * @param array $booking_rooms The rooms booking list.
655 *
656 * @return array Associative list of damage deposit details.
657 *
658 * @since 1.17.6 (J) - 1.7.6 (WP)
659 */
660 public function getDamageDepositSplitPayment(array $booking, array $booking_rooms)
661 {
662 // load all option records of type damage deposit
663 $dbo = JFactory::getDbo();
664 $dbo->setQuery(
665 $dbo->getQuery(true)
666 ->select('*')
667 ->from($dbo->qn('#__vikbooking_optionals'))
668 ->where($dbo->qn('forcesel') . ' = 1')
669 ->where($dbo->qn('oparams') . ' LIKE ' . $dbo->q('%' . str_replace(['{', '}'], '', json_encode(['damagedep' => 1])) . '%'))
670 );
671 $dd_records = $dbo->loadAssocList();
672
673 // scan all records for validation, if any
674 foreach ($dd_records as &$dd_record) {
675 // make sure to decode the option params
676 $dd_record['oparams'] = (array) json_decode($dd_record['oparams'], true);
677
678 if (empty($dd_record['oparams']['damagedep_settings']['paywhen'])) {
679 // no separate payment defined
680 unset($dd_record);
681 continue;
682 }
683
684 // validate maximum nights of stay
685 if (!empty($dd_record['oparams']['damagedep_settings']['bmaxlos']) && ($booking['days'] ?? 1) > $dd_record['oparams']['damagedep_settings']['bmaxlos']) {
686 // limit exceeded
687 unset($dd_record);
688 continue;
689 }
690
691 // validate payment method ID
692 if (empty($dd_record['oparams']['damagedep_settings']['payid']) && empty($booking['idpayment'])) {
693 // no payment method defined anywhere
694 unset($dd_record);
695 continue;
696 }
697
698 // calculate and set the payment window values
699 $dd_record['payment_window'] = [];
700 if (!strlen((string) $dd_record['oparams']['damagedep_settings']['paywind'])) {
701 // payable from today (always)
702 $dd_record['payment_window']['payment_from_dt'] = date('Y-m-d');
703 $dd_record['payment_window']['payable'] = true;
704 } elseif (empty($dd_record['oparams']['damagedep_settings']['paywind'])) {
705 // payable from the check-in day
706 $dd_record['payment_window']['payment_from_dt'] = date('Y-m-d', $booking['checkin']);
707 $dd_record['payment_window']['payable'] = strtotime($dd_record['payment_window']['payment_from_dt']) <= strtotime(date('Y-m-d'));
708 } else {
709 // calculate the payable date
710 $window_days = (int) $dd_record['oparams']['damagedep_settings']['paywind'];
711 $dd_record['payment_window']['payment_from_dt'] = date('Y-m-d', strtotime(sprintf('-%d days', $window_days), $booking['checkin']));
712 $dd_record['payment_window']['payable'] = strtotime($dd_record['payment_window']['payment_from_dt']) <= strtotime(date('Y-m-d'));
713 }
714
715 // check if a custom payment method should be used
716 if (!empty($dd_record['oparams']['damagedep_settings']['payid'])) {
717 $dd_record['payment_window']['pay_id'] = $dd_record['oparams']['damagedep_settings']['payid'];
718 }
719
720 // ensure damage deposit amount was not paid already
721 if (empty($booking['idorderota']) && !empty($booking['totpaid']) && $booking['totpaid'] >= ($booking['total'] ?? 0)) {
722 // payment window not available because damage deposit already paid
723 $dd_record['payment_window'] = [];
724 }
725 }
726
727 // unset last reference
728 unset($dd_record);
729
730 if (!$dd_records) {
731 // unable to proceed
732 return [];
733 }
734
735 // always reset array keys
736 $dd_records = array_values($dd_records);
737
738 // list of damage deposit option IDs
739 $dd_record_ids = array_column($dd_records, 'id');
740
741 // room reservation IDs affected
742 $room_reservation_dd = [];
743
744 // collect all damage deposit options from the booked rooms
745 $rooms_dd_data = [];
746 foreach ($booking_rooms as $or) {
747 if (empty($or['optionals'])) {
748 continue;
749 }
750
751 $stepo = array_filter(explode(";", $or['optionals']));
752 foreach ($stepo as $roptkey => $one) {
753 $stept = explode(":", $one);
754 if (in_array($stept[0], $dd_record_ids)) {
755 // push damage deposit ID and room record
756 $rooms_dd_data[] = [
757 'dd_id' => $stept[0],
758 'rr' => $or,
759 ];
760
761 // push room reservation ID
762 $room_reservation_dd[] = $or['idroom'] ?? 0;
763 }
764 }
765 }
766
767 if (!$rooms_dd_data) {
768 // no damage deposit options were booked
769 return [];
770 }
771
772 // get the unique array
773 $rooms_dd_unique = array_values(array_unique(array_column($rooms_dd_data, 'dd_id')));
774
775 // turn the records into an associative list
776 $dd_records_assoc = [];
777 foreach ($dd_records as $dd_record) {
778 $dd_records_assoc[$dd_record['id']] = $dd_record;
779 }
780
781 // calculate amounts and damage deposit payment window
782 $tot_dd_amount_gross = 0;
783 $tot_dd_amount_net = 0;
784 $tot_dd_amount_tax = 0;
785 $payment_window = [];
786
787 foreach ($rooms_dd_data as $room_dd_data) {
788 $opt_id = $room_dd_data['dd_id'];
789 if (!($dd_records_assoc[$opt_id] ?? [])) {
790 continue;
791 }
792
793 // calculate damage deposit price
794 $dd_price = (float) $dd_records_assoc[$opt_id]['cost'];
795 if (!empty($dd_records_assoc[$opt_id]['pcentroom'])) {
796 // percent cost of the room reservation
797 $room_cost = ($room_dd_data['rr']['room_cost'] ?? 0) ?: ($room_dd_data['rr']['cust_cost'] ?? 0) ?: 0;
798 $dd_price = $room_cost * $dd_price / 100;
799 }
800
801 if ($dd_price <= 0) {
802 // invalid damage deposit cost
803 continue;
804 }
805
806 if ($dd_records_assoc[$opt_id]['perday'] == 1) {
807 // cost per night
808 $dd_price = $dd_price * ($booking['days'] ?? 1);
809 }
810
811 if (($dd_records_assoc[$opt_id]['maxprice'] ?? 0) > 0 && $dd_price > $dd_records_assoc[$opt_id]['maxprice']) {
812 // maximum cost
813 $dd_price = (float) $dd_records_assoc[$opt_id]['maxprice'];
814 }
815
816 if ($dd_records_assoc[$opt_id]['perperson'] == 1) {
817 // cost per person
818 $dd_price = $dd_price * ((int) $room_dd_data['rr']['adults']);
819 }
820
821 /**
822 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
823 *
824 * @since 1.17.7 (J) - 1.7.7 (WP)
825 */
826 $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$dd_price, &$dd_records_assoc[$opt_id], $booking, $booking_rooms]);
827 if ($custom_calculation) {
828 $dd_price = (float) $custom_calculation[0];
829 }
830
831 if ($dd_price <= 0) {
832 // invalid damage deposit cost
833 continue;
834 }
835
836 // calculate taxes, if any
837 $dd_amount_gross = VikBooking::sayOptionalsPlusIva($dd_price, $dd_records_assoc[$opt_id]['idiva']);
838 $dd_amount_net = VikBooking::sayOptionalsMinusIva($dd_price, $dd_records_assoc[$opt_id]['idiva']);
839 $dd_amount_tax = $dd_amount_gross - $dd_amount_net;
840
841 // increase global values
842 $tot_dd_amount_gross += $dd_amount_gross;
843 $tot_dd_amount_net += $dd_amount_net;
844 $tot_dd_amount_tax += $dd_amount_tax;
845
846 // update payment window (one for all option records)
847 $payment_window = (array) $dd_records_assoc[$opt_id]['payment_window'];
848 }
849
850 if (!$tot_dd_amount_gross) {
851 // no compliant damage deposit option found for separate payment
852 return [];
853 }
854
855 return [
856 'damagedep_gross' => $tot_dd_amount_gross,
857 'damagedep_net' => $tot_dd_amount_net,
858 'damagedep_tax' => $tot_dd_amount_tax,
859 'payment_window' => $payment_window,
860 'damagedep_rids' => $room_reservation_dd,
861 ];
862 }
863
864 /**
865 * Given a list of room records, returns an associative list
866 * of room IDs and corresponding mini thumbnail URLs, if any.
867 *
868 * @param array $rooms List of room records.
869 *
870 * @return array
871 *
872 * @since 1.17.6 (J) - 1.7.6 (WP)
873 */
874 public function loadMiniThumbnails(array $rooms, string $def_uri = '')
875 {
876 $mini_thumbnails = [];
877
878 $base_img_path = implode(DIRECTORY_SEPARATOR, [VBO_SITE_PATH, 'resources', 'uploads']) . DIRECTORY_SEPARATOR;
879 $base_img_uri = VBO_SITE_URI . 'resources/uploads/';
880
881 foreach ($rooms as $room) {
882 if (empty($room['id'])) {
883 continue;
884 }
885
886 if (!empty($room['img']) && is_file($base_img_path . 'mini_' . $room['img'])) {
887 $mini_thumbnails[$room['id']] = $base_img_uri . 'mini_' . $room['img'];
888 } elseif ($def_uri) {
889 $mini_thumbnails[$room['id']] = $def_uri;
890 }
891 }
892
893 return $mini_thumbnails;
894 }
895
896 /**
897 * Returns a list of all option records, regardless of the rooms.
898 *
899 * @return array
900 *
901 * @since 1.18.8 (J) - 1.8.8 (WP)
902 */
903 public function loadAnyOptions()
904 {
905 $dbo = JFactory::getDbo();
906
907 $dbo->setQuery(
908 $dbo->getQuery(true)
909 ->select('*')
910 ->from($dbo->qn('#__vikbooking_optionals'))
911 // place options for children age at the beginning
912 ->order('CASE WHEN ' . $dbo->qn('ifchildren') . ' = 1 AND ' . $dbo->qn('ageintervals') . ' IS NOT NULL THEN 1 ELSE 0 END DESC')
913 // sort by ordering value
914 ->order($dbo->qn('ordering') . ' ASC')
915 );
916
917 return $dbo->loadAssocList();
918 }
919
920 /**
921 * Returns a list of eligible options for the given room ID and party data.
922 *
923 * @param int $roomId The room ID.
924 * @param array $data Room party data (stay dates, guests, tariff, cache data).
925 *
926 * @return array List of eligible option records with the related cost.
927 *
928 * @since 1.18.8 (J) - 1.8.8 (WP)
929 */
930 public function getEligibleOptions(int $roomId, array $data = [])
931 {
932 // load room record unless provided
933 $roomRecord = (array) ($data['_rooms'][$roomId] ?? VikBooking::getRoomInfo($roomId, [], true));
934
935 if (!$roomRecord) {
936 return [];
937 }
938
939 // load all option records for the current room
940 $optionRecords = VikBooking::getRoomOptionals($roomRecord['idopt'] ?? '');
941
942 // availability helper
943 $av_helper = VikBooking::getAvailabilityInstance(true);
944
945 // inject involved room ID
946 $av_helper->setRoomIds($roomId);
947
948 // default nights of stay
949 $stayNights = 1;
950
951 // default room rates
952 $roomRates = [];
953
954 if (!empty($data['checkin']) && !empty($data['checkout'])) {
955 // filter option records by date
956 VikBooking::filterOptionalsByDate($optionRecords, strtotime($data['checkin']), strtotime($data['checkout']));
957
958 // set stay dates
959 $av_helper->setStayDates($data['checkin'], $data['checkout']);
960
961 // calculate nights of stay
962 $stayNights = $av_helper->countNightsOfStay();
963 }
964
965 if (!empty($data['adults']) || !empty($data['children'])) {
966 // filter option records by guests party
967 VikBooking::filterOptionalsByParty($optionRecords, ($data['adults'] ?? 0), ($data['children'] ?? 0));
968
969 // set room party
970 $av_helper->setRoomParty(($data['adults'] ?? 0), ($data['children'] ?? 0));
971 }
972
973 if ($av_helper->getStayDates()) {
974 // set flag to ignore the restrictions
975 $av_helper->ignoreRestrictions(true);
976
977 // set flag to ignore the rooms availability
978 $av_helper->ignoreAvailability(true);
979
980 // calculate room rates
981 $roomRatesList = $av_helper->getRates([
982 'num_rooms' => 1,
983 'only_rates' => 1,
984 'forced_room_ids' => [$roomId],
985 ]);
986
987 if ($roomRatesList[$roomId] ?? []) {
988 // room rates were loaded
989 if (!empty($data['rate_id'])) {
990 // filter rates by rate plan
991 $ratePlanId = $data['rate_id'];
992
993 // get only the rates for the requested rate plan ID
994 $roomRatesList[$roomId] = array_values(array_filter($roomRatesList[$roomId], function($rates) use ($ratePlanId) {
995 return ($rates['idprice'] ?? 0) == $ratePlanId;
996 }));
997 }
998
999 // assign the room rates for the first rate plan, if any
1000 $roomRates = $roomRatesList[$roomId][0] ?? [];
1001 }
1002 }
1003
1004 // extract children-age options from records
1005 list($optionRecords, $ageintervals) = VikBooking::loadOptionAgeIntervals($optionRecords, ($data['adults'] ?? 0), ($data['children'] ?? 0));
1006
1007 if (empty($data['children'])) {
1008 // filter out option records only for children, but not for age intervals
1009 $optionRecords = array_values(array_filter($optionRecords, function($option) {
1010 return empty($option['ifchildren']);
1011 }));
1012 }
1013
1014 // scan all option records to calculate the cost
1015 foreach ($optionRecords as &$optionRecord) {
1016 // get base option cost
1017 $optionCost = boolval($optionRecord['pcentroom']) ? (($roomRates['cost'] ?? 0) * $optionRecord['cost'] / 100) : $optionRecord['cost'];
1018
1019 // check nightly cost multiplier
1020 $optionCost = boolval($optionRecord['perday']) ? ($optionCost * $stayNights) : $optionCost;
1021
1022 // check for max cost (cap)
1023 if (!empty($optionRecord['maxprice']) && $optionRecord['maxprice'] > 0 && $optionCost > $optionRecord['maxprice']) {
1024 // apply cost cap
1025 $optionCost = $optionRecord['maxprice'];
1026 }
1027
1028 // check per-person multiplier
1029 if (!empty($optionRecord['perperson'])) {
1030 $optionCost = $optionCost * max(1, ($data['adults'] ?? 0));
1031 }
1032
1033 // multiply by integer
1034 $optionCost *= 1;
1035
1036 /**
1037 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
1038 */
1039 $custom_calc_booking = [
1040 'days' => $stayNights,
1041 ];
1042 $custom_calc_booking_room = [
1043 'adults' => $data['adults'] ?? 0,
1044 'children' => $data['children'] ?? 0,
1045 'room_cost' => $roomRates['cost'] ?? 0,
1046 ];
1047 $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$optionCost, &$optionRecord, $custom_calc_booking, $custom_calc_booking_room]);
1048 if ($custom_calculation) {
1049 $optionCost = (float) $custom_calculation[0];
1050 }
1051
1052 // set option computed cost
1053 $optionRecord['_computed_cost'] = round($optionCost, 2);
1054 }
1055
1056 // unset last reference
1057 unset($optionRecord);
1058
1059 if ($ageintervals && !empty($data['children'])) {
1060 // prepare custom calculation values
1061 $custom_calc_booking = [
1062 'days' => $stayNights,
1063 ];
1064 $custom_calc_booking_room = [
1065 'adults' => $data['adults'] ?? 0,
1066 'children' => $data['children'] ?? 0,
1067 'room_cost' => $roomRates['cost'] ?? 0,
1068 ];
1069
1070 // set computed costs and age intervals list
1071 $ageintervals['_computed_children_costs'] = [];
1072 $ageintervals['_computed_age_intervals'] = [];
1073
1074 // calculate the cost for each child for every age interval
1075 for ($ch = 1; $ch <= (int) $data['children']; $ch++) {
1076 // age intervals can be overridden per child number
1077 $optageovrct = VikBooking::getOptionIntervalChildOverrides($ageintervals, ($data['adults'] ?? 0), ($data['children'] ?? 0));
1078 $intervals = array_filter(explode(';;', $optageovrct['ageintervals_child' . $ch] ?? $ageintervals['ageintervals']));
1079
1080 // start child number computed costs per age interval
1081 $ageintervals['_computed_children_costs'][$ch] = [];
1082 $ageintervals['_computed_age_intervals'][$ch] = [];
1083
1084 // scan all age intervals
1085 foreach ($intervals as $kintv => $intv) {
1086 // calculate base cost
1087 $intvparts = explode('_', $intv);
1088 $intvparts[2] = boolval($ageintervals['perday']) ? ($intvparts[2] * $stayNights) : (float) $intvparts[2];
1089
1090 if (!empty($ageintervals['maxprice']) && $ageintervals['maxprice'] > 0 && $intvparts[2] > $ageintervals['maxprice']) {
1091 // apply cost cap
1092 $intvparts[2] = (float) $ageintervals['maxprice'];
1093 }
1094
1095 /**
1096 * Trigger event to allow third party plugins to apply a custom calculation for the option/extra fee or tax.
1097 */
1098 $custom_calculation = VBOFactory::getPlatform()->getDispatcher()->filter('onCalculateBookingOptionFeeCost', [$intvparts[2], &$ageintervals, $custom_calc_booking, $custom_calc_booking_room]);
1099 if ($custom_calculation) {
1100 $intvparts[2] = (float) $custom_calculation[0];
1101 }
1102
1103 // computed cost for the current child
1104 $childCost = $intvparts[2];
1105
1106 if ($roomRates && array_key_exists(3, $intvparts) && strpos($intvparts[3], '%') !== false && $childCost >= 0) {
1107 // percent cost
1108 if (strpos($intvparts[3], '%b') !== false) {
1109 // percentage value of room base cost
1110 $childCost = (($roomRates['costbeforeoccupancy'] ?? 0) ?: $roomRates['cost']) * $childCost / 100;
1111 } else {
1112 // percentage value of adults tariff
1113 $childCost = $roomRates['cost'] * $childCost / 100;
1114 }
1115 }
1116
1117 // push age interval computed cost for the current child number
1118 $ageintervals['_computed_children_costs'][$ch][] = round($childCost, 2);
1119 $ageintervals['_computed_age_intervals'][$ch][] = sprintf('%s - %s', $intvparts[0], $intvparts[1]);
1120 }
1121 }
1122
1123 // prepend the previously extracted option for children age intervals to the list
1124 array_unshift($optionRecords, $ageintervals);
1125 }
1126
1127 // return the linear list of option records
1128 return array_values($optionRecords);
1129 }
1130
1131 /**
1132 * Given a room booking record and eligible options list, computes their costs.
1133 *
1134 * @param array $bookingRoom The room booking record data.
1135 * @param array $eligibleOptions List of eligible options with computed costs.
1136 *
1137 * @return array List of option records with calculated costs based on quantity booked.
1138 *
1139 * @since 1.18.8 (J) - 1.8.8 (WP)
1140 */
1141 public function computeBookingOptions(array $bookingRoom, array $eligibleOptions)
1142 {
1143 // start container
1144 $computedBookingOptions = [];
1145
1146 // access string of room booking options
1147 $bookingOptions = $bookingRoom['optionals'] ?? '';
1148
1149 // get the list of booked options and related quantity details
1150 $optionsList = array_values(array_filter(explode(';', $bookingOptions)));
1151
1152 // scan booked options to build a map of child age intervals selected and quantity booked
1153 $optionQuantityMap = [];
1154 $optionChildIndexMap = [];
1155 $optionChildIndexUse = [];
1156 foreach ($optionsList as $optStr) {
1157 $parts = explode(':', $optStr);
1158 $optId = (int) $parts[0];
1159 $quantData = $parts[1] ?? '';
1160 if (strpos($quantData, '-') !== false) {
1161 $quantParts = explode('-', $quantData);
1162 $quantData = $quantParts[0];
1163 // push child age interval index
1164 $optionChildIndexMap[$optId] = $optionChildIndexMap[$optId] ?? [];
1165 $optionChildIndexMap[$optId][] = (int) $quantParts[1];
1166 }
1167 // push quantity map
1168 $optionQuantityMap[$optId] = (int) $quantData;
1169 }
1170
1171 // scan all eligible option records
1172 foreach ($eligibleOptions as $eligibleOption) {
1173 // get current option ID
1174 $optId = $eligibleOption['id'];
1175
1176 if (!isset($optionQuantityMap[$optId])) {
1177 // ignore option not booked
1178 continue;
1179 }
1180
1181 // count quantity by supporting multiple children fees
1182 $optQuant = ($computedBookingOptions[$optId]['quantity'] ?? 0) + $optionQuantityMap[$optId];
1183
1184 // calculate option cost
1185 $optCost = $computedBookingOptions[$optId]['cost'] ?? 0;
1186
1187 if (isset($optionChildIndexMap[$optId])) {
1188 // child fee option
1189 foreach ($optionChildIndexMap[$optId] as $childIndex => $ageIntervalIndex) {
1190 $childNumber = $childIndex + 1;
1191 $ageListIndex = $ageIntervalIndex - 1;
1192 // increase child fee option cost
1193 $optCost += $eligibleOption['_computed_children_costs'][$childNumber][$ageListIndex] ?? 0;
1194 }
1195 } else {
1196 // regular option
1197 $optCost += $eligibleOption['_computed_cost'] ?? 0;
1198 }
1199
1200 // build and push booking option
1201 $computedBookingOptions[$optId] = [
1202 'id' => $optId,
1203 'name' => $eligibleOption['name'],
1204 'quantity' => $optionQuantityMap[$optId],
1205 'cost' => $optCost,
1206 ];
1207 }
1208
1209 // return the linear list of computed booking options
1210 return array_values($computedBookingOptions);
1211 }
1212 }
1213