PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.16
VikBooking Hotel Booking Engine & PMS v1.8.16
1.8.16 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 All 37 releases
← All changes | admin/helpers/src/room/helper.php +1127 -454 1.7.2 → 1.8.16 View file →
@@ -13,527 +13,1200 @@
13 13
14 14 /**
15 15 * Helper class to handle rooms data.
16 16 *
17 - * @since 1.15.1 (J) - 1.5.2 (WP)
17 + * @since 1.15.1 (J) - 1.5.2 (WP)
18 18 */
19 19 final class VBORoomHelper extends JObject
20 20 {
21 - /**
22 - * The singleton instance of the class.
23 - *
24 - * @var VBORoomHelper
25 - */
26 - private static $instance = null;
21 + /**
22 + * The singleton instance of the class.
23 + *
24 + * @var VBORoomHelper
25 + */
26 + private static $instance = null;
27 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 - }
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 41
42 - return static::$instance;
43 - }
42 + return static::$instance;
43 + }
44 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 - }
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 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 - }
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 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);
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 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 - }
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 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 - }
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 111
112 - // all costs per night were proportional
113 - return false;
114 - }
112 + // all costs per night were proportional
113 + return false;
114 + }
115 115
116 - /**
117 - * Gets the available room upgrade options, if any.
118 - *
119 - * @param VikBookingTranslator $vbo_tn the translator object.
120 - *
121 - * @return array list of available upgrade options,
122 - * or empty array if nothing availabe.
123 - *
124 - * @since 1.16.0 (J) - 1.6.0 (WP)
125 - */
126 - public function getUpgradeOptions($vbo_tn = null)
127 - {
128 - $booking = $this->get('booking', []);
129 - $rooms = $this->get('rooms', []);
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 + }
130 132
131 - if (!$booking || !$rooms || $booking['status'] != 'confirmed') {
132 - return [];
133 - }
133 + $dbo = JFactory::getDbo();
134 134
135 - $dbo = JFactory::getDbo();
136 - $config = VBOFactory::getConfig();
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);
137 142
138 - $upgrade_options = [];
139 - $room_ids = [];
143 + return (int) $dbo->loadResult();
144 + }
140 145
141 - foreach ($rooms as $num => $broom) {
142 - if (empty($broom['idroom']) || empty($broom['idtar'])) {
143 - // room must have a valid tariff assigned
144 - continue;
145 - }
146 - $room_upgrade_options = $config->getArray('room_upgrade_options_' . $broom['idroom'], []);
147 - if (empty($room_upgrade_options) || empty($room_upgrade_options['rooms'])) {
148 - // no relations for this room
149 - continue;
150 - }
151 - // fetch the original tariff for this room
152 - $orig_tariff = $this->getTariffData($broom['idtar']);
153 - if (!$orig_tariff) {
154 - // unable to get the original tariff information for this room booked
155 - continue;
156 - }
157 - // push suitable rooms
158 - $upgrade_options[$num] = [
159 - 'rooms' => array_map('intval', array_filter(array_unique($room_upgrade_options['rooms']))),
160 - 'discount' => (!empty($room_upgrade_options['discount']) ? (float)$room_upgrade_options['discount'] : 0),
161 - 'tariff' => $orig_tariff,
162 - 'r_costs' => [],
163 - ];
164 - $room_ids = array_merge($room_ids, $room_upgrade_options['rooms']);
165 - }
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 + }
166 162
167 - if (!$upgrade_options) {
168 - return [];
169 - }
163 + $dbo = JFactory::getDbo();
170 164
171 - // get all room IDs involved
172 - $room_ids = array_map('intval', array_filter(array_unique($room_ids)));
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);
173 172
174 - $q = "SELECT * FROM `#__vikbooking_rooms` WHERE `id` IN (" . implode(', ', $room_ids) . ") AND `avail`=1;";
175 - $dbo->setQuery($q);
176 - $room_records = $dbo->loadAssocList();
177 - if (!$room_records) {
178 - return [];
179 - }
180 - if ($vbo_tn) {
181 - // translate rooms
182 - $vbo_tn->translateContents($room_records, '#__vikbooking_rooms');
183 - }
173 + return (int) $dbo->loadResult();
174 + }
184 175
185 - // build up an associative array of room infos
186 - $room_infos = [];
187 - foreach ($room_records as $room_record) {
188 - $room_infos[$room_record['id']] = $this->prepareCMSContents($room_record, ['info', 'smalldesc']);
189 - }
190 - unset($room_records);
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 + }
191 192
192 - // keep the count of the room units suggested
193 - $room_units_counter = [];
193 + $dbo = JFactory::getDbo();
194 194
195 - // filter the suitable rooms by rate plan, and calculate the costs
196 - foreach ($upgrade_options as $num => $upgrade_option) {
197 - // build the costs for each upgrade room option
198 - $upgrade_room_costs = [];
199 - // parse all rooms compatible
200 - foreach ($upgrade_option['rooms'] as $rkey => $rid) {
201 - // find the same tariff for this room and nights
202 - $room_same_tariff = $this->findTariff($rid, $upgrade_option['tariff']['days'], $upgrade_option['tariff']['idprice']);
203 - if (!$room_same_tariff || !isset($room_infos[$rid])) {
204 - // this room is not suited
205 - unset($upgrade_options[$num]['rooms'][$rkey]);
206 - continue;
207 - }
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);
208 201
209 - // count the actual number of room remaining units
210 - $use_room_units = $room_infos[$rid]['units'];
211 - if (isset($room_units_counter[$rid])) {
212 - $use_room_units -= $room_units_counter[$rid];
213 - }
202 + $highest_max_los = (int) $dbo->loadResult();
214 203
215 - // make sure the room is bookable on these dates (restrictions are ignored)
216 - if (!VikBooking::roomBookable($rid, $use_room_units, $booking['checkin'], $booking['checkout'])) {
217 - // room is not available for upgrade
218 - unset($upgrade_options[$num]['rooms'][$rkey]);
219 - continue;
220 - }
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);
221 211
222 - // update room units counter
223 - if (!isset($room_units_counter[$rid])) {
224 - $room_units_counter[$rid] = 0;
225 - }
226 - $room_units_counter[$rid]++;
212 + $total_room_rplans = (int) $dbo->loadResult();
227 213
228 - // apply seasonal rates
229 - $tar = VikBooking::applySeasonsRoom([$room_same_tariff], $booking['checkin'], $booking['checkout']);
214 + if ($total_room_rplans < 2) {
215 + // this room has got rates defined for a single rate plan
216 + return 0;
217 + }
218 + }
230 219
231 - // apply OBP rules
232 - $tar = $this->applyOBPRules($tar, $room_infos[$rid], $rooms[$num]['adults']);
220 + return $highest_max_los;
221 + }
233 222
234 - // apply upgrade discount (if any) and calculate upgrade cost
235 - foreach ($tar as $tk => $tv) {
236 - $tar[$tk]['upgrade_cost'] = $upgrade_option['discount'] > 0 ? round(($tv['cost'] * (100 - $upgrade_option['discount']) / 100), 2) : $tv['cost'];
237 - }
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', []);
238 237
239 - // push room tariff (just one rate plan, the originally booked one)
240 - $upgrade_room_costs[$rid] = $tar[0];
241 - }
238 + if (!$booking || !$rooms || $booking['status'] != 'confirmed') {
239 + return [];
240 + }
242 241
243 - if (!count($upgrade_options[$num]['rooms'])) {
244 - // no more suitable rooms
245 - unset($upgrade_options[$num]);
246 - continue;
247 - }
242 + $dbo = JFactory::getDbo();
243 + $config = VBOFactory::getConfig();
248 244
249 - // sort by price descending (most expensive on top)
250 - $sort_map = [];
251 - foreach ($upgrade_room_costs as $rid => $tar) {
252 - $sort_map[$rid] = $tar['upgrade_cost'];
253 - }
254 - arsort($sort_map);
245 + $upgrade_options = [];
246 + $room_ids = [];
255 247
256 - // replace values with sorted ordering
257 - $cp_upgrade_room_costs = [];
258 - foreach ($sort_map as $rid => $sorted) {
259 - $cp_upgrade_room_costs[$rid] = $upgrade_room_costs[$rid];
260 - }
261 - $upgrade_room_costs = $cp_upgrade_room_costs;
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 + }
262 273
263 - // set upgrade room costs
264 - $upgrade_options[$num]['r_costs'] = $upgrade_room_costs;
265 - }
274 + if (!$upgrade_options) {
275 + return [];
276 + }
266 277
267 - if (!count($upgrade_options)) {
268 - return [];
269 - }
278 + // get all room IDs involved
279 + $room_ids = array_map('intval', array_filter(array_unique($room_ids)));
270 280
271 - // return the associative array information
272 - return [
273 - 'upgrade' => $upgrade_options,
274 - 'rooms' => $room_infos,
275 - ];
276 - }
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 + }
277 291
278 - /**
279 - * Gets the record details about a specific tariff ID.
280 - *
281 - * @param int $idtar the ID of the room-tariff.
282 - *
283 - * @return array record found, or empty array.
284 - *
285 - * @since 1.16.0 (J) - 1.6.0 (WP)
286 - */
287 - public function getTariffData($idtar)
288 - {
289 - $dbo = JFactory::getDbo();
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);
290 298
291 - $dbo->setQuery("SELECT * FROM `#__vikbooking_dispcost` WHERE `id` = " . (int)$idtar, 0, 1);
292 - $tariff = $dbo->loadAssoc();
293 - if (!$tariff) {
294 - return [];
295 - }
299 + // keep the count of the room units suggested
300 + $room_units_counter = [];
296 301
297 - return $tariff;
298 - }
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 + }
299 315
300 - /**
301 - * Finds a tariff for the given rate plan ID, room and nights.
302 - *
303 - * @param int $rid the room ID.
304 - * @param int $nights the number of nights of stay.
305 - * @param int $idprice the rate plan ID.
306 - *
307 - * @return array record found or empty array.
308 - *
309 - * @since 1.16.0 (J) - 1.6.0 (WP)
310 - */
311 - public function findTariff($rid, $nights, $idprice)
312 - {
313 - $dbo = JFactory::getDbo();
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 + }
314 321
315 - $q = "SELECT `t`.*, `p`.`name` AS `rate_plan_name` FROM `#__vikbooking_dispcost` AS `t`
316 - LEFT JOIN `#__vikbooking_prices` AS `p` ON `t`.`idprice`=`p`.`id`
317 - WHERE `t`.`idroom` = " . (int)$rid . " AND `t`.`days`=" . (int)$nights . " AND `t`.`idprice`=" . (int)$idprice;
318 - $dbo->setQuery($q, 0, 1);
319 - $tariff = $dbo->loadAssoc();
320 - if (!$tariff) {
321 - return [];
322 - }
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 + }
323 328
324 - return $tariff;
325 - }
329 + // update room units counter
330 + if (!isset($room_units_counter[$rid])) {
331 + $room_units_counter[$rid] = 0;
332 + }
333 + $room_units_counter[$rid]++;
326 334
327 - /**
328 - * Applies the OBP rules over an array of tariffs.
329 - *
330 - * @param array $tar list of tariff records, one per rate plan, after seasonal rates.
331 - * @param array $room the room (or order-room) record for which tariffs where loaded.
332 - * @param int $adults the number of adults to consider.
333 - *
334 - * @return array original tariffs array with OBP costs applied.
335 - *
336 - * @since 1.16.0 (J) - 1.6.0 (WP)
337 - */
338 - public function applyOBPRules(array $tar, array $room, $adults = 2)
339 - {
340 - // check for different usage
341 - if (!isset($room['fromadult']) || $room['fromadult'] > $adults || $room['toadult'] < $adults) {
342 - return $tar;
343 - }
335 + // apply seasonal rates
336 + $tar = VikBooking::applySeasonsRoom([$room_same_tariff], $booking['checkin'], $booking['checkout']);
344 337
345 - // check for room ID
346 - $use_room_id = isset($room['idroom']) ? $room['idroom'] : $room['id'];
338 + // apply OBP rules
339 + $tar = $this->applyOBPRules($tar, $room_infos[$rid], $rooms[$num]['adults']);
347 340
348 - // different usage
349 - $diffusageprice = VikBooking::loadAdultsDiff($use_room_id, $adults);
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 + }
350 345
351 - /**
352 - * Memorize immediately the OBP rules defined at room-level in order to avoid conflicts
353 - * with rate plans with and without OBP overrides defined at rate plan level through SP.
354 - */
355 - $orig_diffusage = $diffusageprice;
346 + // push room tariff (just one rate plan, the originally booked one)
347 + $upgrade_room_costs[$rid] = $tar[0];
348 + }
356 349
357 - // occupancy overrides
358 - $occ_ovr = VikBooking::occupancyOverrideExists($tar, $adults);
359 - $diffusageprice = $occ_ovr !== false ? $occ_ovr : $diffusageprice;
350 + if (!count($upgrade_options[$num]['rooms'])) {
351 + // no more suitable rooms
352 + unset($upgrade_options[$num]);
353 + continue;
354 + }
360 355
361 - if (!$diffusageprice) {
362 - return $tar;
363 - }
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);
364 362
365 - // set a charge or discount to the price(s) for the different usage of the room
366 - foreach ($tar as $kpr => $vpr) {
367 - // occupancy override
368 - $diffusageprice = isset($vpr['occupancy_ovr']) && isset($vpr['occupancy_ovr'][$adults]) ? $vpr['occupancy_ovr'][$adults] : $orig_diffusage;
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 369
370 - // set usage of the room
371 - $tar[$kpr]['diffusage'] = $adults;
370 + // set upgrade room costs
371 + $upgrade_options[$num]['r_costs'] = $upgrade_room_costs;
372 + }
372 373
373 - if ($diffusageprice['chdisc'] == 1) {
374 - // charge
375 - if ($diffusageprice['valpcent'] == 1) {
376 - // fixed value
377 - $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
378 - $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
379 - $tar[$kpr]['diffusagecost'] = "+" . $aduseval;
380 - $tar[$kpr]['room_base_cost'] = $vpr['cost'];
381 - $tar[$kpr]['cost'] = $vpr['cost'] + $aduseval;
382 - } else {
383 - // percentage value
384 - $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
385 - $aduseval = $diffusageprice['pernight'] == 1 ? round(($vpr['cost'] * $diffusageprice['value'] / 100) * $tar[$kpr]['days'] + $vpr['cost'], 2) : round(($vpr['cost'] * (100 + $diffusageprice['value']) / 100), 2);
386 - $tar[$kpr]['diffusagecost'] = "+" . $diffusageprice['value'] . "%";
387 - $tar[$kpr]['room_base_cost'] = $vpr['cost'];
388 - $tar[$kpr]['cost'] = $aduseval;
389 - }
390 - } else {
391 - // discount
392 - if ($diffusageprice['valpcent'] == 1) {
393 - // fixed value
394 - $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? 1 : 0;
395 - $aduseval = $diffusageprice['pernight'] == 1 ? $diffusageprice['value'] * $tar[$kpr]['days'] : $diffusageprice['value'];
396 - $tar[$kpr]['diffusagecost'] = "-" . $aduseval;
397 - $tar[$kpr]['room_base_cost'] = $vpr['cost'];
398 - $tar[$kpr]['cost'] = $vpr['cost'] - $aduseval;
399 - } else {
400 - // percentage value
401 - $tar[$kpr]['diffusagecostpernight'] = $diffusageprice['pernight'] == 1 ? $vpr['cost'] : 0;
402 - $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);
403 - $tar[$kpr]['diffusagecost'] = "-" . $diffusageprice['value'] . "%";
404 - $tar[$kpr]['room_base_cost'] = $vpr['cost'];
405 - $tar[$kpr]['cost'] = $aduseval;
406 - }
407 - }
408 - }
374 + if (!count($upgrade_options)) {
375 + return [];
376 + }
409 377
410 - // return the array of tariffs with OBP included
411 - return $tar;
412 - }
378 + // return the associative array information
379 + return [
380 + 'upgrade' => $upgrade_options,
381 + 'rooms' => $room_infos,
382 + ];
383 + }
413 384
414 - /**
415 - * Prepares some description strings for the current CMS, by triggering
416 - * the necessary platform-related functions for third party plugins.
417 - *
418 - * @param array $room_record the room record to prepare.
419 - * @param array $keys list of record keys to prepare.
420 - *
421 - * @return array the original array given with keys prepared.
422 - *
423 - * @since 1.16.0 (J) - 1.6.0 (WP)
424 - */
425 - public function prepareCMSContents(array $room_record, array $keys)
426 - {
427 - foreach ($keys as $key) {
428 - if (!isset($room_record[$key])) {
429 - continue;
430 - }
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();
431 397
432 - if (VBOPlatformDetection::isWordPress()) {
433 - /**
434 - * @wponly we try to parse any shortcode inside the HTML description of the room
435 - */
436 - $room_record[$key] = do_shortcode(wpautop($room_record[$key]));
437 - } else {
438 - // BEGIN: Joomla Content Plugins Rendering
439 - JPluginHelper::importPlugin('content');
398 + $dbo->setQuery("SELECT * FROM `#__vikbooking_dispcost` WHERE `id` = " . (int)$idtar, 0, 1);
399 + $tariff = $dbo->loadAssoc();
400 + if (!$tariff) {
401 + return [];
402 + }
440 403
441 - $myItem = JTable::getInstance('content');
404 + return $tariff;
405 + }
442 406
443 - $myItem->text = $room_record[$key];
444 - $objparams = array();
445 - if (class_exists('JEventDispatcher')) {
446 - $dispatcher = JEventDispatcher::getInstance();
447 - $dispatcher->trigger('onContentPrepare', array('com_vikbooking.roomdetails', &$myItem, &$objparams, 0));
448 - } else {
449 - /**
450 - * @joomla4only
451 - */
452 - $dispatcher = JFactory::getApplication();
453 - if (method_exists($dispatcher, 'triggerEvent')) {
454 - $dispatcher->triggerEvent('onContentPrepare', array('com_vikbooking.roomdetails', &$myItem, &$objparams, 0));
455 - }
456 - }
457 - $room_record[$key] = $myItem->text;
458 - // END: Joomla Content Plugins Rendering
459 - }
460 - }
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();
461 421
462 - return $room_record;
463 - }
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 + }
464 430
465 - /**
466 - * Gets an associative list of rate plans with a few pricing information for the given room.
467 - *
468 - * @param int $rid the VBO room id.
469 - * @param int $rplan_id optional rate plan ID to get.
470 - *
471 - * @return array associative list of rate plans for the given room or specific rate plan.
472 - *
473 - * @since 1.16.3 (J) - 1.6.3 (WP)
474 - */
475 - public function getRatePlans($rid = 0, $rplan_id = 0)
476 - {
477 - if (empty($rid)) {
478 - $rid = $this->get('id', 0);
479 - }
431 + return $tariff;
432 + }
480 433
481 - $dbo = JFactory::getDbo();
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 + }
482 451
483 - $q = $dbo->getQuery(true)
484 - ->select([
485 - $dbo->qn('r.id'),
486 - $dbo->qn('r.idroom'),
487 - $dbo->qn('r.days'),
488 - $dbo->qn('r.idprice'),
489 - $dbo->qn('r.cost'),
490 - $dbo->qn('p.name'),
491 - $dbo->qn('p.minlos'),
492 - ])
493 - ->from($dbo->qn('#__vikbooking_dispcost', 'r'))
494 - ->leftJoin($dbo->qn('#__vikbooking_prices', 'p') . ' ON ' . $dbo->qn('r.idprice') . ' = ' . $dbo->qn('p.id'))
495 - ->where($dbo->qn('r.idroom') . ' = ' . (int)$rid)
496 - ->order($dbo->qn('r.days') . ' ASC')
497 - ->order($dbo->qn('r.cost') . ' ASC');
452 + // check for room ID
453 + $use_room_id = isset($room['idroom']) ? $room['idroom'] : $room['id'];
498 454
499 - $dbo->setQuery($q, 0, 50);
455 + // different usage
456 + $diffusageprice = VikBooking::loadAdultsDiff($use_room_id, $adults);
500 457
501 - $tariffs = $dbo->loadObjectList();
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;
502 463
503 - if (!$tariffs) {
504 - return [];
505 - }
464 + // occupancy overrides
465 + $occ_ovr = VikBooking::occupancyOverrideExists($tar, $adults);
466 + $diffusageprice = $occ_ovr !== false ? $occ_ovr : $diffusageprice;
506 467
507 - $parsed_room_prices = [];
508 - foreach ($tariffs as $rrk => $rrv) {
509 - if (isset($parsed_room_prices[$rrv->idprice])) {
510 - unset($tariffs[$rrk]);
511 - continue;
512 - }
513 - $tariffs[$rrk]->cost = round(($rrv->cost / $rrv->days), 2);
514 - $tariffs[$rrk]->days = 1;
515 - $parsed_room_prices[$rrv->idprice] = 1;
516 - }
468 + if (!$diffusageprice) {
469 + return $tar;
470 + }
517 471
518 - $tariffs = array_values($tariffs);
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;
519 476
520 - $room_rate_plans = [];
521 - foreach ($tariffs as $rplan) {
522 - if ($rplan_id && $rplan_id == $rplan->idprice) {
523 - return (array)$rplan;
524 - }
477 + // set usage of the room
478 + $tar[$kpr]['diffusage'] = $adults;
525 479
526 - $room_rate_plans[] = [
527 - 'id' => $rplan->idprice,
528 - 'name' => $rplan->name,
529 - 'cost' => $rplan->cost,
530 - ];
531 - }
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 + }
532 516
533 - if ($rplan_id) {
534 - return [];
535 - }
517 + // return the array of tariffs with OBP included
518 + return $tar;
519 + }
536 520
537 - return $room_rate_plans;
538 - }
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 + }
539 1212 }