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/model/reservation.php +3877 -1006 1.6.6 → 1.8.16 View file →
@@ -13,1162 +13,4033 @@
13 13
14 14 /**
15 15 * VikBooking reservation model.
16 16 *
17 - * @since 1.16.0 (J) - 1.6.0 (WP)
17 + * @since 1.16.0 (J) - 1.6.0 (WP)
18 18 */
19 19 class VBOModelReservation extends JObject
20 20 {
21 - /**
22 - * The singleton instance of the class.
23 - *
24 - * @var VBOModelReservation
25 - */
26 - private static $instance = null;
21 + /**
22 + * The singleton instance of the class.
23 + *
24 + * @var VBOModelReservation
25 + */
26 + private static $instance = null;
27 27
28 - /**
29 - * Proxy for immediately getting the object and bind data.
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 + * The total number of bookings found through the last search.
30 + *
31 + * @var int
32 + */
33 + protected $totalBookings = 0;
41 34
42 - return static::$instance;
43 - }
35 + /**
36 + * @var array
37 + */
38 + protected $allRooms = [];
44 39
45 - /**
46 - * Sets the customer information.
47 - *
48 - * @param array $customer the customer array.
49 - *
50 - * @return self
51 - */
52 - public function setCustomer(array $customer = [])
53 - {
54 - $this->set('_customer', $customer);
40 + /**
41 + * @var ?VBOBookingRegistry
42 + */
43 + protected ?VBOBookingRegistry $prevBookingRegistry = null;
55 44
56 - return $this;
57 - }
45 + /**
46 + * Proxy for immediately getting the object and bind data.
47 + *
48 + * @param array|object $data optional data to bind.
49 + * @param boolean $anew true for forcing a new instance.
50 + *
51 + * @return self
52 + */
53 + public static function getInstance($data = [], $anew = false)
54 + {
55 + if (is_null(static::$instance) || $anew) {
56 + static::$instance = new static($data);
57 + }
58 58
59 - /**
60 - * Returns the customer information.
61 - *
62 - * @return array
63 - */
64 - public function getCustomer()
65 - {
66 - return (array)$this->get('_customer', []);
67 - }
59 + return static::$instance;
60 + }
68 61
69 - /**
70 - * Sets the room information.
71 - *
72 - * @param array $room the room array.
73 - *
74 - * @return self
75 - */
76 - public function setRoom(array $room = [])
77 - {
78 - $this->set('_room', $room);
62 + /**
63 + * Sets the caller information used to save history records.
64 + *
65 + * @param string $caller The caller identifier.
66 + *
67 + * @return self
68 + *
69 + * @since 1.16.10 (J) - 1.6.10 (WP)
70 + */
71 + public function setCaller($caller = '')
72 + {
73 + $this->set('_caller', (string) $caller);
79 74
80 - return $this;
81 - }
75 + return $this;
76 + }
82 77
83 - /**
84 - * Returns the room information.
85 - *
86 - * @return array
87 - */
88 - public function getRoom()
89 - {
90 - return (array)$this->get('_room', []);
91 - }
78 + /**
79 + * Returns the caller information.
80 + *
81 + * @return string
82 + *
83 + * @since 1.16.10 (J) - 1.6.10 (WP)
84 + */
85 + public function getCaller()
86 + {
87 + return (string) $this->get('_caller', '');
88 + }
92 89
93 - /**
94 - * Sets the new booking ID created.
95 - *
96 - * @param int $bid the newly added record ID.
97 - *
98 - * @return self
99 - */
100 - protected function setNewBookingID($bid = 0)
101 - {
102 - $this->set('_newBookingID', $bid);
90 + /**
91 + * Sets the history extra data value.
92 + *
93 + * @param array $data The history extra data array.
94 + *
95 + * @return self
96 + *
97 + * @since 1.16.10 (J) - 1.6.10 (WP)
98 + */
99 + public function setHistoryData(array $data = [])
100 + {
101 + $this->set('_historyData', $data);
103 102
104 - return $this;
105 - }
103 + return $this;
104 + }
106 105
107 - /**
108 - * Returns the new booking ID created, or 0.
109 - *
110 - * @return int
111 - */
112 - public function getNewBookingID()
113 - {
114 - return (int)$this->get('_newBookingID', 0);
115 - }
106 + /**
107 + * Returns the customer information.
108 + *
109 + * @return array
110 + *
111 + * @since 1.16.10 (J) - 1.6.10 (WP)
112 + */
113 + public function getHistoryData()
114 + {
115 + return (array) $this->get('_historyData', []);
116 + }
116 117
117 - /**
118 - * Sets the VCM action to be performed in order to sync the availability.
119 - *
120 - * @param string $action the VCM action, usually an HTML link.
121 - *
122 - * @return self
123 - */
124 - protected function setChannelManagerAction($action = '')
125 - {
126 - $this->set('_vcmAction', $action);
118 + /**
119 + * Sets the search filters.
120 + *
121 + * @param array $data The search filters associative array.
122 + *
123 + * @return self
124 + *
125 + * @since 1.16.10 (J) - 1.6.10 (WP)
126 + */
127 + public function setFilters(array $data = [])
128 + {
129 + $this->set('_filters', $data);
127 130
128 - return $this;
129 - }
131 + return $this;
132 + }
130 133
131 - /**
132 - * Returns the VCM action (if any) to sync the availability.
133 - *
134 - * @return string
135 - */
136 - public function getChannelManagerAction()
137 - {
138 - return $this->get('_vcmAction', '');
139 - }
134 + /**
135 + * Returns the search filters.
136 + *
137 + * @return array
138 + *
139 + * @since 1.16.10 (J) - 1.6.10 (WP)
140 + */
141 + public function getFilters()
142 + {
143 + return (array) $this->get('_filters', []);
144 + }
140 145
141 - /**
142 - * Sets the check-in and check-out times with hours and minutes.
143 - *
144 - * @return array list of check-in and check-out hours and minutes.
145 - */
146 - public function loadCheckinOutTimes()
147 - {
148 - static $times_loaded = null;
146 + /**
147 + * Sets the booking information record.
148 + *
149 + * @param array $booking The booking record.
150 + *
151 + * @return self
152 + *
153 + * @since 1.16.10 (J) - 1.6.10 (WP)
154 + */
155 + public function setBooking(array $booking = [])
156 + {
157 + $this->set('_booking', $booking);
149 158
150 - if ($times_loaded) {
151 - return [
152 - $this->get('checkin_h'),
153 - $this->get('checkin_m'),
154 - $this->get('checkout_h'),
155 - $this->get('checkout_m'),
156 - ];
157 - }
159 + return $this;
160 + }
158 161
159 - $timeopst = VikBooking::getTimeOpenStore();
160 - if (is_array($timeopst) && $timeopst) {
161 - $opent = VikBooking::getHoursMinutes($timeopst[0]);
162 - $closet = VikBooking::getHoursMinutes($timeopst[1]);
163 - $hcheckin = $opent[0];
164 - $mcheckin = $opent[1];
165 - $hcheckout = $closet[0];
166 - $mcheckout = $closet[1];
167 - } else {
168 - $hcheckin = 0;
169 - $mcheckin = 0;
170 - $hcheckout = 0;
171 - $mcheckout = 0;
172 - }
162 + /**
163 + * Returns the booking information record.
164 + *
165 + * @return array
166 + *
167 + * @since 1.16.10 (J) - 1.6.10 (WP)
168 + */
169 + public function getBooking()
170 + {
171 + return (array) $this->get('_booking', []);
172 + }
173 173
174 - $this->set('checkin_h', $hcheckin);
175 - $this->set('checkin_m', $mcheckin);
176 - $this->set('checkout_h', $hcheckout);
177 - $this->set('checkout_m', $mcheckout);
174 + /**
175 + * Sets the room booking records.
176 + *
177 + * @param array $room_booking The room booking records.
178 + *
179 + * @return self
180 + *
181 + * @since 1.16.10 (J) - 1.6.10 (WP)
182 + */
183 + public function setRoomBooking(array $room_booking = [])
184 + {
185 + $this->set('_roomBooking', $room_booking);
178 186
179 - $times_loaded = 1;
187 + return $this;
188 + }
180 189
181 - return [
182 - $hcheckin,
183 - $mcheckin,
184 - $hcheckout,
185 - $mcheckout,
186 - ];
187 - }
190 + /**
191 + * Returns the room booking records.
192 + *
193 + * @return array
194 + *
195 + * @since 1.16.10 (J) - 1.6.10 (WP)
196 + */
197 + public function getRoomBooking()
198 + {
199 + return (array) $this->get('_roomBooking', []);
200 + }
188 201
189 - /**
190 - * Attempts to extract the Special Requests from the customer raw data.
191 - *
192 - * @return string
193 - *
194 - * @since 1.16.5 (J) - 1.6.5 (WP)
195 - */
196 - public function extractSpecialRequests()
197 - {
198 - $raw_cust_data = $this->get('custdata', '');
199 - if (empty($raw_cust_data)) {
200 - return '';
201 - }
202 + /**
203 + * Sets the customer information.
204 + *
205 + * @param array $customer the customer array.
206 + *
207 + * @return self
208 + */
209 + public function setCustomer(array $customer = [])
210 + {
211 + $this->set('_customer', $customer);
202 212
203 - $special_requests = '';
204 - if (preg_match("/(?:special_?requests:\s*)(.*?)$/is", $raw_cust_data, $match)) {
205 - $special_requests = $match[1];
206 - } elseif (preg_match("/(?:special_?request:\s*)(.*?)$/is", $raw_cust_data, $match)) {
207 - $special_requests = $match[1];
208 - } elseif (preg_match("/(?:special_?request\s*)(.*?)$/is", $raw_cust_data, $match)) {
209 - $special_requests = $match[1];
210 - } elseif (preg_match("/(?:" . JText::translate('ORDER_SPREQUESTS') . ":\s*)(.*?)$/is", $raw_cust_data, $match)) {
211 - $special_requests = $match[1];
212 - }
213 + return $this;
214 + }
213 215
214 - return $special_requests;
215 - }
216 + /**
217 + * Returns the customer information.
218 + *
219 + * @return array
220 + */
221 + public function getCustomer()
222 + {
223 + return (array) $this->get('_customer', []);
224 + }
216 225
217 - /**
218 - * Creates a new reservation record after having constructed the
219 - * object by properly injecting all the necessary booking information.
220 - *
221 - * @return bool
222 - */
223 - public function create()
224 - {
225 - if (!$this->canCreate()) {
226 - $this->setError('Forbidden');
227 - return false;
228 - }
226 + /**
227 + * Sets a list of rooms data for a multi-room context.
228 + *
229 + * @param array $rooms List of rooms data arrays.
230 + *
231 + * @return self
232 + *
233 + * @since 1.18.8 (J) - 1.8.8 (WP)
234 + */
235 + public function setRooms(array $rooms)
236 + {
237 + // set rooms list data
238 + $this->set('_rooms', $rooms);
229 239
230 - // availability helper
231 - $av_helper = VikBooking::getAvailabilityInstance();
240 + if (!$this->getRoom() && ($rooms[0] ?? [])) {
241 + // populate data also for the current/first room
242 + $this->set('_room', (array) $rooms[0]);
243 + }
232 244
233 - // validate mandatory fields
234 - $room = $this->getRoom();
235 - if (!$this->get('checkin') || !$this->get('checkout') || empty($room['id'])) {
236 - $this->setError('Missing mandatory fields');
237 - return false;
238 - }
245 + return $this;
246 + }
239 247
240 - if ($this->get('checkin') >= $this->get('checkout')) {
241 - $this->setError('Invalid dates');
242 - return false;
243 - }
248 + /**
249 + * Sets the room booking information for both single and multi room context.
250 + *
251 + * @param array $room The room data array.
252 + * @param ?int $index Optional room index to update in a multi-room context.
253 + *
254 + * @return self
255 + *
256 + * @since 1.18.8 (J) - 1.8.8 (WP) added argument $index.
257 + */
258 + public function setRoom(array $room, ?int $index = null)
259 + {
260 + // set current room data
261 + $this->set('_room', $room);
244 262
245 - // make sure we have the time for check-in and check-out
246 - if (!$this->get('checkin_h') || !$this->get('checkout_h')) {
247 - // make sure to set the times (hours/minutes) for check-in and check-out
248 - $this->loadCheckinOutTimes();
249 - }
263 + // access all rooms previously set
264 + $rooms = $this->getRooms();
250 265
251 - // number of nights of stay
252 - if (!$this->get('nights')) {
253 - $this->set('nights', $av_helper->countNightsOfStay($this->get('checkin'), $this->get('checkout')));
254 - }
266 + if ($index !== null && isset($rooms[$index])) {
267 + // overwrite requested room index
268 + $rooms[$index] = $room;
269 + $this->set('_rooms', $rooms);
270 + } elseif ($index === null) {
271 + // push room data to list
272 + $rooms[] = $room;
273 + $this->set('_rooms', $rooms);
274 + }
255 275
256 - // fetch and apply turnover time before doing anything else
257 - $this->applyTurnover();
276 + return $this;
277 + }
258 278
259 - // if rate plan selected, get the tariff ID
260 - $this->loadTariffID();
279 + /**
280 + * Returns the information of all rooms set. This works
281 + * for both single-room and multi-room booking context.
282 + *
283 + * @return array
284 + *
285 + * @since 1.18.8 (J) - 1.8.8 (WP)
286 + */
287 + public function getRooms()
288 + {
289 + return (array) $this->get('_rooms', []);
290 + }
261 291
262 - // get pool of rooms involved
263 - $rooms_pool = $this->getRoomsPool();
264 - if (!$rooms_pool) {
265 - if ($this->getError() === false) {
266 - // set generic error if not set already
267 - $this->setError('No rooms involved in the reservation');
268 - }
269 - return false;
270 - }
292 + /**
293 + * Returns the current room information.
294 + *
295 + * @return array
296 + */
297 + public function getRoom()
298 + {
299 + return (array) $this->get('_room', []);
300 + }
271 301
272 - // check if the room is available
273 - $room_available = $this->isRoomAvailable();
274 - if (!$this->get('force_booking', 0) && !$this->get('set_closed', 0) && !$room_available) {
275 - // no forcing, no closure and room fully booked results into an error message
276 - $this->setError(JText::translate('VBBOOKNOTMADE'));
277 - return false;
278 - }
302 + /**
303 + * Tells if we are dealing with a multi-room booking.
304 + *
305 + * @return bool
306 + *
307 + * @since 1.18.8 (J) - 1.8.8 (WP)
308 + */
309 + public function isMultiRoom()
310 + {
311 + return (bool) (count($this->getRooms()) > 1);
312 + }
279 313
280 - // detect if we are forcing the reservation
281 - $this->detectForcedReason($room_available);
314 + /**
315 + * Sets the new booking ID created.
316 + *
317 + * @param int $bid the newly added record ID.
318 + *
319 + * @return self
320 + */
321 + protected function setNewBookingID($bid = 0)
322 + {
323 + $this->set('_newBookingID', $bid);
282 324
283 - // store the customer information
284 - $this->storeCustomer();
325 + return $this;
326 + }
285 327
286 - // calculate total amount and total tax
287 - $this->calculateTotal();
328 + /**
329 + * Returns the new booking ID created, or 0.
330 + *
331 + * @return int
332 + */
333 + public function getNewBookingID()
334 + {
335 + return (int) $this->get('_newBookingID', 0);
336 + }
288 337
289 - // store booking and room-booking records
290 - if (!$this->storeReservationRecords($rooms_pool)) {
291 - if ($this->getError() === false) {
292 - // set generic error if not set already
293 - $this->setError('Could not create the reservation');
294 - }
295 - return false;
296 - }
338 + /**
339 + * Sets the VCM action to be performed in order to sync the availability.
340 + *
341 + * @param string $action the VCM action, usually an HTML link.
342 + *
343 + * @return self
344 + */
345 + protected function setChannelManagerAction($action = '')
346 + {
347 + $this->set('_vcmAction', $action);
297 348
298 - return true;
299 - }
349 + return $this;
350 + }
300 351
301 - /**
302 - * Tells whether the operation is authenticated. By default this
303 - * model is only available in the administrator section of the site.
304 - *
305 - * @return bool
306 - */
307 - protected function canCreate()
308 - {
309 - if (!JFactory::getApplication()->isClient('administrator') && !$this->get('_isAdministrator')) {
310 - return false;
311 - }
352 + /**
353 + * Returns the VCM action (if any) to sync the availability.
354 + *
355 + * @return string
356 + */
357 + public function getChannelManagerAction()
358 + {
359 + return $this->get('_vcmAction', '');
360 + }
312 361
313 - return true;
314 - }
362 + /**
363 + * Sets the check-in and check-out times with hours and minutes.
364 + *
365 + * @return array list of check-in and check-out hours and minutes.
366 + *
367 + * @since 1.18.8 (J) - 1.8.8 (WP) added support to room-level times.
368 + * removed static cache because the object could be used multiple times.
369 + */
370 + public function loadCheckinOutTimes()
371 + {
372 + $timeopst = VikBooking::getTimeOpenStore();
373 + if (is_array($timeopst) && $timeopst) {
374 + $opent = VikBooking::getHoursMinutes($timeopst[0]);
375 + $closet = VikBooking::getHoursMinutes($timeopst[1]);
376 + $hcheckin = $opent[0];
377 + $mcheckin = $opent[1];
378 + $hcheckout = $closet[0];
379 + $mcheckout = $closet[1];
380 + } else {
381 + $hcheckin = 0;
382 + $mcheckin = 0;
383 + $hcheckout = 0;
384 + $mcheckout = 0;
385 + }
315 386
316 - /**
317 - * Gets and sets the tariff ID if a rate plan was set.
318 - *
319 - * @return int the tariff ID or 0.
320 - */
321 - protected function loadTariffID()
322 - {
323 - $dbo = JFactory::getDbo();
387 + // set global check-in/check-out hours and minutes
388 + $this->set('checkin_h', $hcheckin);
389 + $this->set('checkin_m', $mcheckin);
390 + $this->set('checkout_h', $hcheckout);
391 + $this->set('checkout_m', $mcheckout);
324 392
325 - $id_tariff = 0;
326 - $room = $this->getRoom();
327 - $daysdiff = (int)$this->get('nights', 1);
393 + // calculate room-level check-in/check-out hours and minutes
394 + $involvedRoomIds = array_map('intval', array_values(array_unique(array_column($this->getRooms(), 'id'))));
395 + $roomParamsAssoc = array_combine($involvedRoomIds, array_map(function($roomId) {
396 + return VikBooking::getRoomInfo($roomId, ['params'], true)['params'] ?? '';
397 + }, $involvedRoomIds));
398 + foreach ($roomParamsAssoc as $roomId => $roomParams) {
399 + $customCheckin = VikBooking::getRoomParam('checkin', $roomParams);
400 + $customCheckout = VikBooking::getRoomParam('checkout', $roomParams);
401 + if ($customCheckin) {
402 + // set proper check-in time at room-level
403 + $parts = explode(':', $customCheckin);
404 + $this->set($roomId . '_checkin_h', (int) $parts[0]);
405 + $this->set($roomId . '_checkin_m', (int) ($parts[1] ?? 0));
406 + }
407 + if ($customCheckout) {
408 + // set proper check-out time at room-level
409 + $parts = explode(':', $customCheckout);
410 + $this->set($roomId . '_checkout_h', (int) $parts[0]);
411 + $this->set($roomId . '_checkout_m', (int) ($parts[1] ?? 0));
412 + }
413 + }
328 414
329 - if (!empty($room['id']) && !empty($room['id_price']) && !empty($room['room_cost']) && $room['room_cost'] > 0 && !(int)$this->get('set_closed') && !$this->get('split_stay', [])) {
330 - $room['id_price'] = (int)$room['id_price'];
415 + // return the linear list
416 + return [
417 + $hcheckin,
418 + $mcheckin,
419 + $hcheckout,
420 + $mcheckout,
421 + ];
422 + }
331 423
332 - $q = "SELECT `id` FROM `#__vikbooking_dispcost` WHERE `idroom`={$room['id']} AND `days`={$daysdiff} AND `idprice`={$room['id_price']};";
333 - $dbo->setQuery($q);
334 - $id_tariff = $dbo->loadResult();
335 - }
424 + /**
425 + * Attempts to extract the Special Requests from the customer raw data.
426 + *
427 + * @return string
428 + *
429 + * @since 1.16.5 (J) - 1.6.5 (WP)
430 + */
431 + public function extractSpecialRequests()
432 + {
433 + $raw_cust_data = $this->get('custdata', '');
434 + if (empty($raw_cust_data)) {
435 + return '';
436 + }
336 437
337 - $this->set('id_tariff', (int)$id_tariff);
438 + $special_requests = '';
439 + if (preg_match("/(?:special_?requests:\s*)(.*?)$/is", $raw_cust_data, $match)) {
440 + $special_requests = $match[1];
441 + } elseif (preg_match("/(?:special_?request:\s*)(.*?)$/is", $raw_cust_data, $match)) {
442 + $special_requests = $match[1];
443 + } elseif (preg_match("/(?:special_?request\s*)(.*?)$/is", $raw_cust_data, $match)) {
444 + $special_requests = $match[1];
445 + } elseif (preg_match("/(?:" . JText::translate('ORDER_SPREQUESTS') . ":\s*)(.*?)$/is", $raw_cust_data, $match)) {
446 + $special_requests = $match[1];
447 + }
338 448
339 - return (int)$id_tariff;
340 - }
449 + return $special_requests;
450 + }
341 451
342 - /**
343 - * Applies the turnover time to the checkout timestamp and sets its value.
344 - *
345 - * @return int the turnover seconds applied.
346 - */
347 - protected function applyTurnover()
348 - {
349 - $turnover_secs = 0;
350 - $checkout = $this->get('checkout', 0);
452 + /**
453 + * Runs before a reservation is created or updated.
454 + *
455 + * @param ?int $bookingId Optional reservation ID being updated.
456 + *
457 + * @return bool
458 + *
459 + * @since 1.18.11 (J) - 1.8.11 (WP)
460 + */
461 + protected function preflight(?int $bookingId = null)
462 + {
463 + // if updating a reservation, fetch the previous details
464 + try {
465 + $prevBookingRegistry = $bookingId ? VBOBookingRegistry::getInstance(['id' => $bookingId]) : null;
466 + } catch (Exception $e) {
467 + $this->setError($e->getMessage());
468 + return false;
469 + }
351 470
352 - if ($checkout) {
353 - // turnover time
354 - $turnover_secs = VikBooking::getHoursRoomAvail() * 3600;
471 + if ($prevBookingRegistry) {
472 + // set booking registry snapshot prior to update
473 + $this->prevBookingRegistry = $prevBookingRegistry;
474 + }
355 475
356 - $this->set('checkout_real', ($checkout + $turnover_secs));
357 - }
476 + // availability helper
477 + $av_helper = VikBooking::getAvailabilityInstance(true);
358 478
359 - $this->set('turnover_secs', $turnover_secs);
479 + // validate mandatory fields
480 + $room = $this->getRoom();
481 + if (!$this->get('checkin') || !$this->get('checkout') || empty($room['id'])) {
482 + $this->setError('Missing mandatory fields');
483 + return false;
484 + }
360 485
361 - return $turnover_secs;
362 - }
486 + if ($this->get('checkin') >= $this->get('checkout')) {
487 + $this->setError('Invalid dates');
488 + return false;
489 + }
363 490
364 - /**
365 - * Returns the details of a specific room ID.
366 - *
367 - * @param int $rid the room ID.
368 - *
369 - * @return array the record found or empty array.
370 - */
371 - protected function getRoomDetails($rid = null)
372 - {
373 - $all_rooms = VikBooking::getAvailabilityInstance()->loadRooms();
491 + // make sure we have the time for check-in and check-out
492 + if (!$this->get('checkin_h') || !$this->get('checkout_h')) {
493 + // make sure to set the times (hours/minutes) for check-in and check-out
494 + $this->loadCheckinOutTimes();
374 495
375 - if (!$rid) {
376 - $inj_room = $this->getRoom();
377 - if (!empty($inj_room['id'])) {
378 - $rid = $inj_room['id'];
379 - }
380 - }
496 + // load default check-in/check-out times
497 + $defCheckinHour = $this->get('checkin_h');
498 + $defCheckinMin = $this->get('checkin_m');
499 + $defCheckoutHour = $this->get('checkout_h');
500 + $defCheckoutMin = $this->get('checkout_m');
381 501
382 - if ($rid && isset($all_rooms[$rid])) {
383 - return $all_rooms[$rid];
384 - }
502 + if (!$this->isMultiRoom()) {
503 + // check if the only room we are booking has got check-in/check-out times at room-level
504 + $defCheckinHour = $this->get($room['id'] . '_checkin_h', $defCheckinHour);
505 + $defCheckinMin = $this->get($room['id'] . '_checkin_m', $defCheckinMin);
506 + $defCheckoutHour = $this->get($room['id'] . '_checkout_h', $defCheckoutHour);
507 + $defCheckoutMin = $this->get($room['id'] . '_checkout_m', $defCheckoutMin);
508 + }
385 509
386 - return [];
387 - }
510 + // make sure check-in and check-out timestamps have been set to a proper time
511 + $from_info = getdate($this->get('checkin'));
512 + $to_info = getdate($this->get('checkout'));
513 + if ((int) $from_info['hours'] != (int) $defCheckinHour) {
514 + $this->set('checkin', mktime((int) $defCheckinHour, (int) $defCheckinMin, 0, $from_info['mon'], $from_info['mday'], $from_info['year']));
515 + }
516 + if ((int) $to_info['hours'] != (int) $defCheckoutHour) {
517 + $this->set('checkout', mktime((int) $defCheckoutHour, (int) $defCheckoutMin, 0, $to_info['mon'], $to_info['mday'], $to_info['year']));
518 + }
388 519
389 - /**
390 - * Gets the list of rooms involved in the reservation in case of closures.
391 - *
392 - * @return array the list of rooms involved
393 - */
394 - protected function getRoomsPool()
395 - {
396 - $room = $this->getRoom();
397 - if (empty($room['id'])) {
398 - return [];
399 - }
400 - $room = $this->getRoomDetails($room['id']);
520 + if ($this->isMultiRoom()) {
521 + // apply check-in and check-out times at room-level in case of different stay dates
522 + foreach ($this->getRooms() as $index => $roomData) {
523 + // default values at booking-level
524 + $room_checkin_ts = $this->get('checkin');
525 + $room_checkout_ts = $this->get('checkout');
526 + if (!empty($roomData['checkin']) && !is_numeric($roomData['checkin'])) {
527 + // convert room-level stay dates into timestamps
528 + $room_checkin_ts = strtotime($roomData['checkin']);
529 + $room_checkout_ts = strtotime($roomData['checkout']);
530 + }
531 + // gather global or room-level check-in/check-out hours and minutes
532 + $useCheckinHour = $this->get($roomData['id'] . '_checkin_h', $defCheckinHour);
533 + $useCheckinMin = $this->get($roomData['id'] . '_checkin_m', $defCheckinMin);
534 + $useCheckoutHour = $this->get($roomData['id'] . '_checkout_h', $defCheckoutHour);
535 + $useCheckoutMin = $this->get($roomData['id'] . '_checkout_m', $defCheckoutMin);
536 + // calculate room-level stay dates and nights of stay
537 + $roomData['checkin'] = strtotime(sprintf('%d:%d', (int) $useCheckinHour, (int) $useCheckinMin), $room_checkin_ts);
538 + $roomData['checkout'] = strtotime(sprintf('%d:%d', (int) $useCheckoutHour, (int) $useCheckoutMin), $room_checkout_ts);
539 + $roomData['nights'] = $av_helper->countNightsOfStay($roomData['checkin'], $roomData['checkout']);
540 + // update room data
541 + $this->setRoom($roomData, $index);
542 + }
543 + }
544 + }
401 545
402 - // gather values
403 - $set_close_others = (array)$this->get('close_others', []);
404 - $split_stay_data = $this->get('split_stay', []);
405 - $set_closed = (int)$this->get('set_closed');
406 - $turnover_secs = $this->get('turnover_secs', 0);
407 - $hcheckin = $this->get('checkin_h', 12);
408 - $mcheckin = $this->get('checkin_m', 0);
409 - $hcheckout = $this->get('checkout_h', 10);
410 - $mcheckout = $this->get('checkout_m', 0);
546 + // number of nights of stay
547 + if (!$this->get('nights')) {
548 + $this->set('nights', $av_helper->countNightsOfStay($this->get('checkin'), $this->get('checkout')));
549 + }
411 550
412 - $av_helper = VikBooking::getAvailabilityInstance();
413 - $all_rooms = $av_helper->loadRooms();
414 - $rooms_pool = [];
415 - $closeothers = [];
551 + // fetch and apply turnover time before doing anything else
552 + $this->applyTurnover();
416 553
417 - if ($set_close_others && $set_closed) {
418 - // prepend current room for closing
419 - array_unshift($set_close_others, $room['id']);
420 - }
421 - $set_close_others = array_unique($set_close_others);
554 + return true;
555 + }
422 556
423 - foreach ($set_close_others as $closeid) {
424 - if (empty($closeid)) {
425 - continue;
426 - }
427 - if ((int)$closeid === -1) {
428 - // close all rooms
429 - $closeothers = [];
430 - foreach ($all_rooms as $cr) {
431 - array_push($closeothers, $cr);
432 - }
433 - break;
434 - }
435 - foreach ($all_rooms as $cr) {
436 - if ((int)$cr['id'] == (int)$closeid) {
437 - // push the main room or one of the other rooms requested for closure
438 - array_push($closeothers, $cr);
439 - break;
440 - }
441 - }
442 - }
557 + /**
558 + * Creates a new reservation record after having constructed the
559 + * object by properly injecting all the necessary booking information.
560 + *
561 + * @return bool
562 + */
563 + public function create()
564 + {
565 + if (!$this->canCreate()) {
566 + $this->setError('Forbidden');
567 + return false;
568 + }
443 569
444 - if (!$closeothers || !$set_closed) {
445 - $rooms_pool = [$room];
446 - } else {
447 - $rooms_pool = $closeothers;
448 - }
570 + /**
571 + * Run preflight to ensure data integrity.
572 + *
573 + * @since 1.18.11 (J) - 1.8.11 (WP)
574 + */
575 + if (!$this->preflight()) {
576 + return false;
577 + }
449 578
450 - // check split stay rooms booking
451 - if (!empty($split_stay_data)) {
452 - // reset pool and set it with the split stay rooms
453 - $rooms_pool = [];
454 - foreach ($split_stay_data as $sps_k => $split_stay) {
455 - if (!isset($all_rooms[$split_stay['idroom']])) {
456 - continue;
457 - }
458 - // calculate and set the exact check-in and check-out timestamps for this split-room
459 - $split_stay['checkin_ts'] = VikBooking::getDateTimestamp($split_stay['checkin'], $hcheckin, $mcheckin);
460 - $split_stay['checkout_ts'] = VikBooking::getDateTimestamp($split_stay['checkout'], $hcheckout, $mcheckout);
461 - $split_stay['realback_ts'] = $turnover_secs + $split_stay['checkout_ts'];
462 - $split_stay['nights'] = $av_helper->countNightsOfStay($split_stay['checkin_ts'], $split_stay['checkout_ts']);
463 - $split_stay_data[$sps_k] = $split_stay;
464 - // push room data to pool after storing additional information
465 - $room_data = $all_rooms[$split_stay['idroom']];
466 - $room_data['checkin_ts'] = $split_stay['checkin_ts'];
467 - $room_data['checkout_ts'] = $split_stay['checkout_ts'];
468 - $rooms_pool[] = $room_data;
469 - }
470 - if (!$rooms_pool) {
471 - $this->setError('No valid rooms for the split stay booking');
472 - return [];
473 - }
474 - // update split stay data manipulated
475 - $this->set('split_stay', $split_stay_data);
476 - }
579 + // get pool of rooms involved
580 + $rooms_pool = $this->getRoomsPool();
581 + if (!$rooms_pool) {
582 + if ($this->getError() === false) {
583 + // set generic error if not set already
584 + $this->setError('No rooms involved in the reservation');
585 + }
586 + return false;
587 + }
477 588
478 - return $rooms_pool;
479 - }
589 + // check if the rooms are available
590 + $rooms_available = $this->checkRoomsAvailability($rooms_pool);
591 + if (!$rooms_available && !$this->get('force_booking', 0) && !$this->get('set_closed', 0)) {
592 + // no forcing, no closure and room(s) fully booked means we have an error
593 + if (!$this->getError()) {
594 + $this->setError(JText::translate('VBBOOKNOTMADE'));
595 + }
596 + return false;
597 + }
480 598
481 - /**
482 - * Checks that the room is available on the requested dates.
483 - * Call this method only after getting the rooms pool.
484 - *
485 - * @return bool
486 - */
487 - protected function isRoomAvailable()
488 - {
489 - $inj_room = $this->getRoom();
490 - if (empty($inj_room['id'])) {
491 - return false;
492 - }
493 - $room = $this->getRoomDetails($inj_room['id']);
494 - if (!$room) {
495 - return false;
496 - }
599 + // detect if we are forcing the reservation
600 + $this->detectForcedReason($rooms_available);
497 601
498 - $split_stay_data = $this->get('split_stay', []);
499 - $set_closed = $this->get('set_closed', 0);
500 - $num_rooms = $this->get('num_rooms', 1);
602 + // store the customer information
603 + $this->storeCustomer();
501 604
502 - $room_available = true;
503 - $all_rooms = VikBooking::getAvailabilityInstance()->loadRooms();
605 + // calculate total amount and total tax
606 + $this->calculateTotal();
504 607
505 - if (empty($split_stay_data)) {
506 - // make sure the rooms are available
507 - $check_units = $room['units'];
508 - if ($num_rooms > 1 && $num_rooms <= $room['units'] && !$set_closed) {
509 - // only when non closing the room we check the availability for the units requested for booking
510 - $check_units = $room['units'] - $num_rooms + 1;
511 - }
512 - $room_available = VikBooking::roomBookable($room['id'], $check_units, $this->get('checkin', 0), $this->get('checkout', 0));
513 - } else {
514 - // make sure the rooms for the split stay are available
515 - foreach ($split_stay_data as $split_stay) {
516 - if (!isset($all_rooms[$split_stay['idroom']])) {
517 - $room_available = false;
518 - break;
519 - }
520 - $room_available = $room_available && VikBooking::roomBookable($split_stay['idroom'], $all_rooms[$split_stay['idroom']]['units'], $split_stay['checkin_ts'], $split_stay['checkout_ts']);
521 - }
522 - }
608 + // store booking and room-booking records
609 + if (!$this->storeReservationRecords($rooms_pool)) {
610 + if ($this->getError() === false) {
611 + // set generic error if not set already
612 + $this->setError('Could not create the reservation');
613 + }
614 + return false;
615 + }
523 616
524 - return $room_available;
525 - }
617 + return true;
618 + }
526 619
527 - /**
528 - * In case the reservation is forced or is a closure, we detect the
529 - * forced reason to eventually attach it to the booking history.
530 - *
531 - * @param bool $room_available whether the room is available.
532 - *
533 - * @return void
534 - */
535 - protected function detectForcedReason($room_available = true)
536 - {
537 - $split_stay_data = $this->get('split_stay', []);
538 - $force_booking = $this->get('force_booking', 0);
539 - $set_closed = $this->get('set_closed', 0);
620 + /**
621 + * Updates a reservation record after having constructed the
622 + * object by properly injecting all the necessary booking information.
623 + * Unlike the modify() method, this method follows the create() pattern.
624 + *
625 + * @return bool
626 + *
627 + * @since 1.18.11 (J) - 1.8.11 (WP)
628 + */
629 + public function update()
630 + {
631 + // access booking ID
632 + $bookingId = (int) $this->get('booking_id', 0);
540 633
541 - $forced_reason = '';
542 - $all_rooms = VikBooking::getAvailabilityInstance()->loadRooms();
634 + if (!$bookingId) {
635 + $this->setError('Missing booking ID to update.');
636 + return false;
637 + }
543 638
544 - if (empty($split_stay_data)) {
545 - // eventually build string for the description of the history event
546 - if (($force_booking || $set_closed) && !$room_available) {
547 - $forced_reason = JText::translate('VBO_FORCED_BOOKDATES');
548 - }
549 - } else {
550 - // set "split stay" as the description of the history event
551 - $forced_reason = JText::translate('VBO_SPLIT_STAY') . "\n";
552 - foreach ($split_stay_data as $sps_k => $split_stay) {
553 - // describe the split stay for each room
554 - if (!isset($all_rooms[$split_stay['idroom']])) {
555 - continue;
556 - }
557 - $room_stay_nights = $split_stay['nights'];
558 - $forced_reason .= $all_rooms[$split_stay['idroom']]['name'] . ': ' . $room_stay_nights . ' ' . ($room_stay_nights > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY')) . ', ';
559 - $forced_reason .= $split_stay['checkin'] . ' - ' . $split_stay['checkout'] . "\n";
560 - }
561 - $forced_reason = rtrim($forced_reason, "\n");
562 - }
639 + /**
640 + * Run preflight to ensure data integrity.
641 + */
642 + if (!$this->preflight($bookingId)) {
643 + return false;
644 + }
563 645
564 - $this->set('forced_reason', $forced_reason);
565 - }
646 + // get pool of rooms involved
647 + $rooms_pool = $this->getRoomsPool();
648 + if (!$rooms_pool) {
649 + if ($this->getError() === false) {
650 + // set generic error if not set already
651 + $this->setError('No rooms involved in the reservation');
652 + }
653 + return false;
654 + }
566 655
567 - /**
568 - * Stores the customer information to a new or existing record.
569 - * In case of success, the customer ID property is updated.
570 - * The customer shall be stored before the reservation records.
571 - *
572 - * @return bool
573 - */
574 - protected function storeCustomer()
575 - {
576 - $dbo = JFactory::getDbo();
656 + try {
657 + // check if the rooms are available for modification
658 + $rooms_available = $this->bookingModifiable($bookingId, $this->get('checkin'), $this->get('checkout'));
659 + if (!$rooms_available && !$this->get('force_booking', 0) && !$this->get('set_closed', 0)) {
660 + // no forcing, no closure and room(s) fully booked means we have an error
661 + if (!$this->getError()) {
662 + $this->setError(JText::translate('VBBOOKNOTMADE'));
663 + }
664 + return false;
665 + }
666 + } catch (Exception $e) {
667 + $this->setError($e->getMessage());
668 + return false;
669 + }
577 670
578 - $inj_customer = $this->getCustomer();
579 - $first_name = !empty($inj_customer['first_name']) ? $inj_customer['first_name'] : '';
580 - $last_name = !empty($inj_customer['last_name']) ? $inj_customer['last_name'] : '';
581 - $custdata = !empty($inj_customer['data']) ? $inj_customer['data'] : '';
582 - $email = !empty($inj_customer['email']) ? $inj_customer['email'] : '';
583 - $country = !empty($inj_customer['country']) ? $inj_customer['country'] : '';
584 - $phone = !empty($inj_customer['phone']) ? $inj_customer['phone'] : '';
671 + // detect if we are forcing the reservation
672 + $this->detectForcedReason($rooms_available);
585 673
586 - // custom fields
587 - $q = "SELECT * FROM `#__vikbooking_custfields` ORDER BY `ordering` ASC;";
588 - $dbo->setQuery($q);
589 - $all_cfields = $dbo->loadAssocList();
674 + // store or update the customer information
675 + $this->storeCustomer();
590 676
591 - $customer_cfields = [];
592 - $customer_extrainfo = [];
593 - $custdata_parts = explode("\n", $custdata);
594 - foreach ($custdata_parts as $cdataline) {
595 - if (!strlen(trim($cdataline))) {
596 - continue;
597 - }
598 - $cdata_parts = explode(':', $cdataline);
599 - if (count($cdata_parts) < 2 || !strlen(trim($cdata_parts[0])) || !strlen(trim($cdata_parts[1]))) {
600 - continue;
601 - }
602 - foreach ($all_cfields as $cf) {
603 - $needle = JText::translate($cf['name']);
604 - if (!empty($needle) && strpos($cdata_parts[0], $needle) !== false && !array_key_exists($cf['id'], $customer_cfields) && $cf['type'] != 'country') {
605 - $user_input_val = trim($cdata_parts[1]);
606 - $customer_cfields[$cf['id']] = $user_input_val;
607 - if (!empty($cf['flag'])) {
608 - $customer_extrainfo[$cf['flag']] = $user_input_val;
609 - } elseif ($cf['type'] == 'state') {
610 - $customer_extrainfo['state'] = $user_input_val;
611 - }
612 - break;
613 - }
614 - }
615 - }
677 + // calculate total amount and total tax
678 + $this->calculateTotal();
616 679
617 - $cpin = VikBooking::getCPinInstance();
618 - $cpin->is_admin = true;
619 - $cpin->setCustomerExtraInfo($customer_extrainfo);
620 - $cpin->saveCustomerDetails($first_name, $last_name, $email, $phone, $country, $customer_cfields);
680 + // update booking and room-booking records
681 + if (!$this->storeReservationRecords($rooms_pool, $bookingId)) {
682 + if ($this->getError() === false) {
683 + // set generic error if not set already
684 + $this->setError('Could not update the reservation');
685 + }
686 + return false;
687 + }
621 688
622 - $customer_id = $cpin->getNewCustomerId();
623 - if (!$customer_id) {
624 - return false;
625 - }
689 + return true;
690 + }
626 691
627 - $inj_customer['id'] = $customer_id;
628 - $this->setCustomer($inj_customer);
692 + /**
693 + * Searches for bookings according to specified filters.
694 + *
695 + * @return array
696 + *
697 + * @since 1.16.10 (J) - 1.6.10 (WP)
698 + * @since 1.18.3 (J) - 1.8.3 (WP) added support for "ota_id" and "channel" filters.
699 + */
700 + public function search()
701 + {
702 + $dbo = JFactory::getDbo();
629 703
630 - return true;
631 - }
704 + $this->totalBookings = 0;
632 705
633 - /**
634 - * Returns the calculated total booking amount and total taxes.
635 - * Sets the necessary properties with the calculated amounts.
636 - *
637 - * @return array list of booking total amount and total taxes.
638 - */
639 - protected function calculateTotal()
640 - {
641 - // the values to calculate
642 - $set_total = 0;
643 - $set_taxes = 0;
706 + $filters = $this->getFilters();
644 707
645 - $dbo = JFactory::getDbo();
708 + if (!$filters) {
709 + $this->setError('Missing filters to search for a booking.');
710 + return [];
711 + }
646 712
647 - // get data
648 - $inj_room = $this->getRoom();
649 - $set_closed = $this->get('set_closed', 0);
650 - $daysdiff = (int)$this->get('nights', 1);
651 - $num_rooms = (int)$this->get('num_rooms', 1);
652 - $totalpnight = !empty($inj_room['total_or_pnight']) ? $inj_room['total_or_pnight'] : 'total';
653 - $cust_cost = !empty($inj_room['cust_cost']) ? (float)$inj_room['cust_cost'] : 0;
654 - $room_cost = !empty($inj_room['room_cost']) ? (float)$inj_room['room_cost'] : 0;
655 - $id_price = !empty($inj_room['id_price']) ? (int)$inj_room['id_price'] : 0;
656 - $id_tax = !empty($inj_room['id_tax']) ? (int)$inj_room['id_tax'] : 0;
713 + $q = $dbo->getQuery(true)
714 + ->select($dbo->qn('o') . '.*')
715 + ->select([
716 + $dbo->qn('c.first_name', 'customer_first_name'),
717 + $dbo->qn('c.last_name', 'customer_last_name'),
718 + ])
719 + ->from($dbo->qn('#__vikbooking_orders', 'o'))
720 + ->leftJoin($dbo->qn('#__vikbooking_customers_orders', 'co') . ' ON ' . $dbo->qn('co.idorder') . ' = ' . $dbo->qn('o.id'))
721 + ->leftJoin($dbo->qn('#__vikbooking_customers', 'c') . ' ON ' . $dbo->qn('c.id') . ' = ' . $dbo->qn('co.idcustomer'))
722 + ->where(1);
657 723
658 - $split_stay_data = $this->get('split_stay', []);
724 + if (($filters['booking_id'] ?? null)) {
725 + $q->andWhere([
726 + $dbo->qn('o.id') . ' = ' . $dbo->q($filters['booking_id']),
727 + $dbo->qn('o.idorderota') . ' = ' . $dbo->q($filters['booking_id']),
728 + ], $glue = 'OR');
729 + }
659 730
660 - $room = $this->getRoomDetails();
661 - if (!$room) {
662 - return [$set_total, $set_taxes];
663 - }
731 + if (($filters['ota_id'] ?? null)) {
732 + $q->where($dbo->qn('o.idorderota') . ' = ' . $dbo->q($filters['ota_id']));
733 + if (($filters['channel'] ?? null)) {
734 + $q->where($dbo->qn('o.channel') . ' LIKE ' . (strpos($filters['channel'], '%') !== false ? $dbo->q($filters['channel']) : $dbo->q('%' . $filters['channel'] . '%')));
735 + } else {
736 + $q->where($dbo->qn('o.channel') . ' IS NOT NULL');
737 + }
738 + }
664 739
665 - if ($cust_cost > 0 && !$set_closed) {
666 - // custom cost can be per night
667 - if ($totalpnight == 'pnight') {
668 - $cust_cost = $cust_cost * $daysdiff;
669 - }
670 - $set_total = $cust_cost;
740 + if (($filters['status'] ?? null)) {
741 + $q->where($dbo->qn('o.status') . ' = ' . $dbo->q($filters['status']));
742 + }
671 743
672 - // apply taxes, if necessary
673 - if ($id_tax) {
674 - $q = "SELECT `i`.`aliq`,`i`.`taxcap` FROM `#__vikbooking_iva` AS `i` WHERE `i`.`id`=" . $id_tax . ";";
675 - $dbo->setQuery($q);
676 - $taxdata = $dbo->loadAssoc();
677 - if ($taxdata) {
678 - $aliq = $taxdata['aliq'];
679 - if (floatval($aliq) > 0.00) {
680 - if (!VikBooking::ivaInclusa()) {
681 - // add tax to the total amount
682 - $subt = 100 + (float)$aliq;
683 - $set_total = ($set_total * $subt / 100);
684 - /**
685 - * Tax Cap implementation for prices tax excluded (most common).
686 - *
687 - * @since 1.12 (J) - 1.2 (WP)
688 - */
689 - if ($taxdata['taxcap'] > 0 && ($set_total - $cust_cost) > $taxdata['taxcap']) {
690 - $set_total = ($cust_cost + $taxdata['taxcap']);
691 - }
692 - // calculate tax
693 - $set_taxes = $set_total - $cust_cost;
694 - } else {
695 - // calculate tax
696 - $cost_minus_tax = VikBooking::sayPackageMinusIva($cust_cost, $id_tax);
697 - $set_taxes += ($cust_cost - $cost_minus_tax);
698 - }
699 - }
700 - }
701 - }
702 - } elseif (!empty($id_price) && $room_cost > 0 && !$set_closed && empty($split_stay_data)) {
703 - // one website rate plan was selected, so we calculate total and taxes
704 - $set_total = $room_cost;
744 + if (($filters['exclude_closures'] ?? false)) {
745 + $q->where($dbo->qn('o.closure') . ' = 0');
746 + }
705 747
706 - // find tax rate assigned to this rate plan
707 - $q = "SELECT `p`.`id`,`p`.`idiva`,`i`.`aliq`,`i`.`taxcap` FROM `#__vikbooking_prices` AS `p` LEFT JOIN `#__vikbooking_iva` AS `i` ON `p`.`idiva`=`i`.`id` WHERE `p`.`id`=" . $id_price . ";";
708 - $dbo->setQuery($q);
709 - $taxdata = $dbo->loadAssoc();
710 - if ($taxdata) {
711 - $aliq = $taxdata['aliq'];
712 - if (floatval($aliq) > 0.00) {
713 - if (!VikBooking::ivaInclusa()) {
714 - // add tax to the total amount
715 - $subt = 100 + (float)$aliq;
716 - $set_total = ($set_total * $subt / 100);
717 - /**
718 - * Tax Cap implementation for prices tax excluded (most common).
719 - *
720 - * @since 1.12 (J) - 1.2 (WP)
721 - */
722 - if ($taxdata['taxcap'] > 0 && ($set_total - $room_cost) > $taxdata['taxcap']) {
723 - $set_total = ($room_cost + $taxdata['taxcap']);
724 - }
725 - // calculate tax
726 - $set_taxes = $set_total - $room_cost;
727 - } else {
728 - // calculate tax
729 - $cost_minus_tax = VikBooking::sayPackageMinusIva($room_cost, $taxdata['idiva']);
730 - $set_taxes += ($room_cost - $cost_minus_tax);
731 - }
732 - }
733 - }
748 + if (($filters['exclude_expired'] ?? false)) {
749 + // take only active reservations with a check-out date in the future
750 + $today_dt = JFactory::getDate('today', new DateTimeZone(date_default_timezone_get()));
751 + $q->where($dbo->qn('o.checkout') . ' >= ' . $dbo->q($today_dt->format('U', true)));
752 + }
734 753
735 - // total and taxes should be multiplied by the number of rooms booked when using a website rate plan
736 - if ($set_closed) {
737 - $set_total *= $room['units'];
738 - $set_taxes *= $room['units'];
739 - } elseif ($num_rooms > 1 && $num_rooms <= $room['units']) {
740 - $set_total *= $num_rooms;
741 - $set_taxes *= $num_rooms;
742 - }
743 - }
754 + if (($filters['email'] ?? null)) {
755 + $q->where($dbo->qn('o.custmail') . ' = ' . $dbo->q($filters['email']));
756 + }
744 757
745 - // set values
746 - $this->set('_total', $set_total);
747 - $this->set('_total_tax', $set_taxes);
758 + if (($filters['phone'] ?? null)) {
759 + $q->where(
760 + sprintf(
761 + '(%s OR %s)',
762 + sprintf('REPLACE(%s, \' \', \'\') LIKE REPLACE(%s, \' \', \'\')',
763 + $dbo->qn('o.phone'),
764 + $dbo->q('%' . $filters['phone'])
765 + ),
766 + sprintf('REPLACE(%s, \' \', \'\') LIKE REPLACE(%s, \' \', \'\')',
767 + $dbo->qn('o.phone_alias'),
768 + $dbo->q('%' . $filters['phone_alias'])
769 + )
770 + )
771 + );
772 + }
748 773
749 - return [$set_total, $set_taxes];
750 - }
774 + if (($filters['date_range']['type'] ?? null) && (($filters['date_range']['start'] ?? null) || ($filters['date_range']['end'] ?? null))) {
775 + // search by date range
776 + $from_dt = JFactory::getDate(($filters['date_range']['start'] ?? $filters['date_range']['end']));
777 + $from_dt->modify('00:00:00');
778 + $to_dt = JFactory::getDate(($filters['date_range']['end'] ?? $filters['date_range']['start']));
779 + $to_dt->modify('23:59:59');
751 780
752 - /**
753 - * Stores the booking and room-booking records.
754 - * If no errors, the newly generated booking id is set.
755 - *
756 - * @param array $rooms_pool list of rooms involved.
757 - *
758 - * @return bool
759 - */
760 - protected function storeReservationRecords(array $rooms_pool)
761 - {
762 - $dbo = JFactory::getDbo();
781 + // check the type of date
782 + if ($filters['date_range']['type'] == 'stay') {
783 + // find intersections of stay dates
784 + $q->andWhere([
785 + '(' . $dbo->qn('o.checkin') . ' <= ' . $dbo->q($from_dt->format('U')) . ' AND ' . $dbo->qn('o.checkout') . ' >= ' . $dbo->q($to_dt->format('U')) . ')',
786 + '(' . $dbo->qn('o.checkin') . ' >= ' . $dbo->q($from_dt->format('U')) . ' AND ' . $dbo->qn('o.checkout') . ' <= ' . $dbo->q($to_dt->format('U')) . ')',
787 + '(' . $dbo->qn('o.checkin') . ' >= ' . $dbo->q($from_dt->format('U')) . ' AND ' . $dbo->qn('o.checkin') . ' < ' . $dbo->q($to_dt->format('U')) . ' AND ' . $dbo->qn('o.checkout') . ' >= ' . $dbo->q($to_dt->format('U')) . ')',
788 + '(' . $dbo->qn('o.checkin') . ' <= ' . $dbo->q($from_dt->format('U')) . ' AND ' . $dbo->qn('o.checkout') . ' > ' . $dbo->q($from_dt->format('U')) . ' AND ' . $dbo->qn('o.checkout') . ' <= ' . $dbo->q($to_dt->format('U')) . ')',
789 + ], $glue = 'OR');
790 + } else {
791 + $column = $dbo->qn('o.checkin');
792 + if ($filters['date_range']['type'] == 'checkout') {
793 + $column = $dbo->qn('o.checkout');
794 + } elseif ($filters['date_range']['type'] == 'creation') {
795 + $column = $dbo->qn('o.ts');
796 + }
797 + $q->where($column . ' >= ' . $dbo->q($from_dt->format('U')));
798 + $q->where($column . ' <= ' . $dbo->q($to_dt->format('U')));
799 + }
800 + } else {
801 + // check for single date filters
802 + if (($filters['creation_date'] ?? null)) {
803 + // dates are expected to be in military format
804 + $creation = JFactory::getDate($filters['creation_date']);
805 + $creation->modify('00:00:00');
806 + $q->where($dbo->qn('o.ts') . ' >= ' . $dbo->q($creation->format('U')));
807 + $creation->modify('23:59:59');
808 + $q->where($dbo->qn('o.ts') . ' <= ' . $dbo->q($creation->format('U')));
809 + }
763 810
764 - $set_closed = $this->get('set_closed', 0);
765 - $units_closed = $this->get('units_closed', 0);
766 - $daysdiff = (int)$this->get('nights', 1);
767 - $num_rooms = (int)$this->get('num_rooms', 1);
768 - $adults = (int)$this->get('adults', 1);
769 - $children = (int)$this->get('children', 0);
770 - $status = $this->get('status', 'confirmed');
811 + if (($filters['checkin_date'] ?? null) && !($filters['checkout_date'] ?? null)) {
812 + // dates are expected to be in military format
813 + $checkin = JFactory::getDate($filters['checkin_date']);
814 + $checkin->modify('00:00:00');
815 + $q->where($dbo->qn('o.checkin') . ' >= ' . $dbo->q($checkin->format('U')));
816 + $checkin->modify('23:59:59');
817 + $q->where($dbo->qn('o.checkin') . ' <= ' . $dbo->q($checkin->format('U')));
818 + }
771 819
772 - $split_stay_data = $this->get('split_stay', []);
773 - $room = $this->getRoomDetails();
774 - if (!$room || !$rooms_pool) {
775 - return false;
776 - }
820 + if (($filters['checkout_date'] ?? null) && !($filters['checkin_date'] ?? null)) {
821 + // dates are expected to be in military format
822 + $checkout = JFactory::getDate($filters['checkout_date']);
823 + $checkout->modify('00:00:00');
824 + $q->where($dbo->qn('o.checkout') . ' >= ' . $dbo->q($checkout->format('U')));
825 + $checkout->modify('23:59:59');
826 + $q->where($dbo->qn('o.checkout') . ' <= ' . $dbo->q($checkout->format('U')));
827 + }
777 828
778 - // get current Joomla/WordPress User ID
779 - $now_user = JFactory::getUser();
780 - $store_ujid = property_exists($now_user, 'id') ? (int)$now_user->id : 0;
829 + if (($filters['checkin_date'] ?? null) && ($filters['checkout_date'] ?? null)) {
830 + // range of dates (dates are expected to be in military format)
831 + $checkin = JFactory::getDate($filters['checkin_date']);
832 + $checkin->modify('00:00:00');
833 + $checkout = JFactory::getDate($filters['checkout_date']);
834 + $checkout->modify('23:59:59');
835 + $q->andWhere([
836 + $dbo->qn('o.checkin') . ' BETWEEN ' . $dbo->q($checkin->format('U')) . ' AND ' . $dbo->q($checkout->format('U')),
837 + $dbo->qn('o.checkout') . ' BETWEEN ' . $dbo->q($checkin->format('U')) . ' AND ' . $dbo->q($checkout->format('U')),
838 + ], $glue = 'OR');
839 + }
781 840
782 - // forced booking reason, status validation and additional data
783 - $forced_reason = $this->get('forced_reason', '');
784 - $valid_statuses = ['confirmed', 'standby'];
785 - $status = in_array($status, $valid_statuses) ? $status : 'confirmed';
786 - $paymentmeth = $this->get('id_payment', '');
787 - $set_total = (float)$this->get('_total', 0);
788 - $set_taxes = (float)$this->get('_total_tax', 0);
841 + if (($filters['stay_date'] ?? null)) {
842 + // dates are expected to be in military format
843 + $staydt = JFactory::getDate($filters['stay_date']);
844 + $staydt->modify('23:59:59');
845 + $q->where($dbo->qn('o.checkin') . ' < ' . $dbo->q($staydt->format('U')));
846 + $q->where($dbo->qn('o.checkout') . ' > ' . $dbo->q($staydt->format('U')));
847 + }
848 + }
789 849
790 - // stay dates
791 - $now_ts = time();
792 - $checkin_ts = $this->get('checkin');
793 - $checkout_ts = $this->get('checkout');
794 - $realback_ts = $this->get('checkout_real', $checkout_ts);
850 + if (($filters['customer_name'] ?? null)) {
851 + $q->where('CONCAT_WS(\' \', ' . $dbo->qn('c.first_name') . ', ' . $dbo->qn('c.last_name') . ') LIKE ' . $dbo->q('%' . $filters['customer_name'] . '%'));
852 + }
795 853
796 - // room
797 - $inj_room = $this->getRoom();
798 - $cust_cost = !empty($inj_room['cust_cost']) ? (float)$inj_room['cust_cost'] : 0;
799 - $room_cost = !empty($inj_room['room_cost']) ? (float)$inj_room['room_cost'] : 0;
800 - $id_price = !empty($inj_room['id_price']) ? (int)$inj_room['id_price'] : 0;
801 - $id_tax = !empty($inj_room['id_tax']) ? (int)$inj_room['id_tax'] : 0;
802 - $id_tariff = $this->get('id_tariff', 0);
854 + if (($filters['confirmation_number'] ?? null)) {
855 + $q->where($dbo->qn('o.confirmnumber') . ' = ' . $dbo->q($filters['confirmation_number']));
856 + }
803 857
804 - // custom rate modifier per night
805 - $totalpnight = !empty($inj_room['total_or_pnight']) ? $inj_room['total_or_pnight'] : 'total';
806 - if ($cust_cost > 0.00 && !$set_closed && $totalpnight == 'pnight') {
807 - $cust_cost = $cust_cost * $daysdiff;
808 - }
858 + if (($filters['room_name'] ?? null)) {
859 + // find the room involved from the given name
860 + $room_record = VikBooking::getAvailabilityInstance()->getRoomByName($filters['room_name']);
861 + if ($room_record) {
862 + $q->leftJoin($dbo->qn('#__vikbooking_ordersrooms', 'or') . ' ON ' . $dbo->qn('or.idorder') . ' = ' . $dbo->qn('o.id'));
863 + $q->where($dbo->qn('or.idroom') . ' = ' . (int) $room_record['id']);
864 + }
865 + }
809 866
810 - // customer
811 - $cpin = VikBooking::getCPinInstance();
812 - $inj_customer = $this->getCustomer();
813 - $customer_id = !empty($inj_customer['id']) ? $inj_customer['id'] : 0;
814 - $customer_pin = !empty($inj_customer['pin']) ? $inj_customer['pin'] : '';
815 - $t_first_name = !empty($inj_customer['first_name']) ? $inj_customer['first_name'] : '';
816 - $t_last_name = !empty($inj_customer['last_name']) ? $inj_customer['last_name'] : '';
817 - $customer_data = !empty($inj_customer['data']) ? $inj_customer['data'] : '';
818 - $customer_email = !empty($inj_customer['email']) ? $inj_customer['email'] : '';
819 - $country_code = !empty($inj_customer['country']) ? $inj_customer['country'] : '';
820 - $phone_number = !empty($inj_customer['phone']) ? $inj_customer['phone'] : '';
867 + /**
868 + * It is now possible to use a custom ordering.
869 + *
870 + * @since 1.17.1 (J) - 1.7.1 (WP)
871 + */
872 + switch ($filters['ordering'] ?? 'id') {
873 + case 'creation': $ordering = 'o.ts'; break;
874 + case 'checkin': $ordering = 'o.checkin'; break;
875 + case 'checkout': $ordering = 'o.checkout'; break;
876 + default: $ordering = 'o.id';
877 + }
821 878
822 - if ($set_closed) {
823 - $customer_data = JText::translate('VBDBTEXTROOMCLOSED');
824 - }
879 + $q->order($dbo->qn($ordering) . ' ' . (strcasecmp($filters['direction'] ?? 'desc', 'desc') ? 'ASC' : 'DESC'));
825 880
826 - // generate booking SID
827 - $sid = VikBooking::getSecretLink();
881 + $dbo->setQuery($q, 0, ($filters['max_bookings'] ?? 0));
882 + $rows = $dbo->loadAssocList();
828 883
829 - // assign room specific unit
830 - $set_room_indexes = !$set_closed ? VikBooking::autoRoomUnit() : false;
831 - $num_rooms = $num_rooms > 0 ? $num_rooms : 1;
884 + $this->totalBookings = count($rows);
832 885
833 - // occupancy and loop limits
834 - $forend = 1;
835 - $or_forend = 1;
836 - $adults_map = [];
837 - $children_map = [];
838 - if ($set_closed && empty($split_stay_data)) {
839 - $forend = $room['units'];
840 - } elseif ($num_rooms > 1 && $num_rooms <= $room['units'] && empty($split_stay_data)) {
841 - $forend = $num_rooms;
842 - $or_forend = $num_rooms;
843 - // assign adults/children proportionally
844 - if (($adults + $children) < $num_rooms) {
845 - // the number of guests does not make much sense but we build the maps anyway
846 - for ($r = 1; $r <= $or_forend; $r++) {
847 - $adults_map[$r] = $adults;
848 - $children_map[$r] = $children;
849 - }
850 - } else {
851 - $adults_per_room = floor(($adults / $num_rooms));
852 - $adults_left = ($adults % $num_rooms);
853 - $children_per_room = floor(($children / $num_rooms));
854 - $children_left = ($children % $num_rooms);
855 - for ($r = 1; $r <= $or_forend; $r++) {
856 - $adults_map[$r] = $adults_per_room;
857 - $children_map[$r] = $children_per_room;
858 - if ($r == $or_forend) {
859 - $adults_map[$r] += $adults_left;
860 - $children_map[$r] += $children_left;
861 - }
862 - }
863 - }
864 - }
886 + /**
887 + * Calculate the total number of matching records.
888 + *
889 + * @since 1.17.1 (J) - 1.7.1 (WP)
890 + */
891 + if ($this->totalBookings && $this->totalBookings == ($filters['max_bookings'] ?? 0)) {
892 + // set up the query used to count the matching records
893 + $dbo->setQuery($q->clear('select')->clear('offset')->clear('limit')->select('COUNT(1)'));
894 + $this->totalBookings = (int) $dbo->loadResult();
895 + }
865 896
866 - // count total rooms booked
867 - $totalrooms = ($set_closed && $status == 'confirmed' ? count($rooms_pool) : ($num_rooms > 1 && $num_rooms <= $room['units'] ? $num_rooms : 1));
868 - $totalrooms = !empty($split_stay_data) ? count($split_stay_data) : $totalrooms;
897 + return $rows;
898 + }
869 899
870 - // prepare booking record
871 - $booking = new stdClass;
872 - $booking->custdata = $customer_data;
873 - $booking->ts = $now_ts;
874 - $booking->status = $status;
875 - $booking->days = $daysdiff;
876 - $booking->checkin = $checkin_ts;
877 - $booking->checkout = $checkout_ts;
878 - $booking->custmail = $customer_email;
879 - $booking->sid = $sid;
880 - $booking->idpayment = $paymentmeth;
881 - $booking->ujid = (int)$store_ujid;
882 - $booking->roomsnum = $totalrooms;
883 - $booking->total = $set_total > 0 ? $set_total : null;
884 - $booking->lang = VikBooking::guessBookingLangFromCountry($country_code);
885 - $booking->country = $country_code;
886 - $booking->tot_taxes = $set_taxes > 0 ? $set_taxes : null;
887 - $booking->phone = $phone_number;
888 - $booking->closure = ($status == 'standby' ? 0 : ($set_closed || $units_closed ? 1 : 0));
889 - $booking->split_stay = !empty($split_stay_data) ? 1 : 0;
900 + /**
901 + * Returns the total number of bookings matching the last search query made.
902 + *
903 + * @return int
904 + *
905 + * @since 1.17.1 (J) - 1.7.1 (WP)
906 + */
907 + public function getTotBookingsFound()
908 + {
909 + return $this->totalBookings;
910 + }
890 911
891 - // store reservation
892 - if ($status == 'confirmed') {
893 - // occupy the rooms
894 - $insertedbusy = [];
895 - if (empty($split_stay_data)) {
896 - // only when closing other rooms we have an array containing multiple rooms info
897 - foreach ($rooms_pool as $nowroom) {
898 - $nowforend = $set_closed ? $nowroom['units'] : $forend;
899 - for ($b = 1; $b <= $nowforend; $b++) {
900 - $busy_record = new stdClass;
901 - $busy_record->idroom = (int)$nowroom['id'];
902 - $busy_record->checkin = (int)$checkin_ts;
903 - $busy_record->checkout = (int)$checkout_ts;
904 - $busy_record->realback = (int)$realback_ts;
912 + /**
913 + * Updates the total amount paid and the amount of (OTA) compensation. To be used for
914 + * OTA reservations only, previously validated. Runs often after an OTA notification.
915 + *
916 + * @param array $options Associative list of booking values to update.
917 + * @param array $booking Optional associative booking record to update.
918 + *
919 + * @return bool
920 + *
921 + * @since 1.18.3 (J) - 1.8.3 (WP)
922 + */
923 + public function updatePayoutCompensation(array $options, array $booking = [])
924 + {
925 + $dbo = JFactory::getDbo();
905 926
906 - // store busy record
907 - $dbo->insertObject('#__vikbooking_busy', $busy_record, 'id');
927 + if ($booking) {
928 + // update the internal reference
929 + $this->setBooking($booking);
930 + } else {
931 + // get the internal booking record
932 + $booking = $this->getBooking();
933 + }
908 934
909 - if (isset($busy_record->id)) {
910 - $insertedbusy[] = $busy_record->id;
911 - }
912 - }
913 - }
914 - } else {
915 - // for split stay bookings we occupy the rooms on the individual stay dates
916 - foreach ($split_stay_data as $split_stay) {
917 - $busy_record = new stdClass;
918 - $busy_record->idroom = (int)$split_stay['idroom'];
919 - $busy_record->checkin = (int)$split_stay['checkin_ts'];
920 - $busy_record->checkout = (int)$split_stay['checkout_ts'];
921 - $busy_record->realback = (int)$split_stay['realback_ts'];
935 + if (empty($booking['id'])) {
936 + // no booking information available
937 + return false;
938 + }
922 939
923 - // store busy record
924 - $dbo->insertObject('#__vikbooking_busy', $busy_record, 'id');
940 + // build booking record values to update
941 + $booking_record = new stdClass;
942 + $booking_record->id = $booking['id'];
925 943
926 - if (isset($busy_record->id)) {
927 - $insertedbusy[] = $busy_record->id;
928 - }
929 - }
930 - }
944 + // access the booking history object
945 + $history_obj = VikBooking::getBookingHistoryInstance($booking['id']);
931 946
932 - if (!$insertedbusy) {
933 - $this->setError('No records were occupied');
934 - return false;
935 - }
947 + // flag/counter that indicates if any booking record value was updated
948 + $booking_values_counter = 0;
936 949
937 - // store booking record
938 - $dbo->insertObject('#__vikbooking_orders', $booking, 'id');
950 + // check if a payout should be set
951 + if ($options['payout'] ?? null) {
952 + // update the booking total amount paid and register a payout received event
953 + $booking_record->totpaid = (float) $options['payout'];
939 954
940 - if (!isset($booking->id)) {
941 - $this->setError('Could not store the reservation record');
942 - return false;
943 - }
955 + // update cached booking information for history
956 + $booking['totpaid'] = (float) $options['payout'];
957 + $history_obj->setBookingInfo($booking);
944 958
945 - // get the newly generated booking ID
946 - $newoid = $booking->id;
959 + // the payout received event type
960 + $ev_type = 'PO';
947 961
948 - // set the new booking ID
949 - $this->setNewBookingID($newoid);
962 + // load current history
963 + $history_rows = array_reverse($history_obj->loadHistory());
950 964
951 - if (!empty($split_stay_data)) {
952 - // save transient on db for split stay information
953 - VBOFactory::getConfig()->set('split_stay_' . $newoid, json_encode($split_stay_data));
954 - }
965 + // check if there is a valid event with an amount paid greater than zero
966 + $prev_paid = 0;
967 + foreach ($history_rows as $hevent) {
968 + if ($hevent['totpaid'] < 1 || $hevent['totpaid'] == $options['payout'] || $hevent['type'] == $ev_type) {
969 + // skip history records with no amount paid, amount paid equal to payout, or payout events
970 + continue;
971 + }
972 + // amount paid found before payout notification
973 + $prev_paid = $hevent['totpaid'];
974 + break;
975 + }
955 976
956 - // check if some of the rooms booked have shared calendars
957 - VikBooking::updateSharedCalendars($newoid, [$room['id']], $checkin_ts, $checkout_ts);
977 + // sum any previously paid amount
978 + $booking_record->totpaid += $prev_paid;
958 979
959 - // confirmation number
960 - $confirmnumber = VikBooking::generateConfirmNumber($newoid, true);
980 + // update booking record in VBO
981 + if ($dbo->updateObject('#__vikbooking_orders', $booking_record, 'id')) {
982 + // increase counter
983 + $booking_values_counter++;
984 + }
961 985
962 - // store busy records/booking relations
963 - foreach ($insertedbusy as $lid) {
964 - $obusy_record = new stdClass;
965 - $obusy_record->idorder = (int)$newoid;
966 - $obusy_record->idbusy = (int)$lid;
986 + // build event description
987 + $ev_descr = preg_replace('/^[^_]*_/', '', (string) ($booking['channel'] ?? '')) . ': ' . $options['payout'];
967 988
968 - // store busy relation record
969 - $dbo->insertObject('#__vikbooking_ordersbusy', $obusy_record, 'id');
970 - }
989 + // set event extra data
990 + $history_obj->setExtraData([
991 + // this is the total amount paid up until now
992 + 'payout_total' => (float) $options['payout'],
993 + ]);
971 994
972 - // write room records
973 - foreach ($rooms_pool as $rind => $nowroom) {
974 - $room_indexes_usemap = [];
975 - for ($r = 1; $r <= $or_forend; $r++) {
976 - // Assign room specific unit
977 - $info_room_avail = [
978 - 'id' => $newoid,
979 - 'checkin' => (!empty($nowroom['checkin_ts']) ? $nowroom['checkin_ts'] : $checkin_ts),
980 - 'checkout' => (!empty($nowroom['checkout_ts']) ? $nowroom['checkout_ts'] : $checkout_ts),
981 - ];
982 - $room_indexes = $set_room_indexes === true ? VikBooking::getRoomUnitNumsAvailable($info_room_avail, $nowroom['id']) : [];
983 - $use_ind_key = 0;
984 - if ($room_indexes) {
985 - if (!array_key_exists($nowroom['id'], $room_indexes_usemap)) {
986 - $room_indexes_usemap[$nowroom['id']] = $use_ind_key;
987 - } else {
988 - $use_ind_key = $room_indexes_usemap[$nowroom['id']];
989 - }
990 - }
995 + // silence the notification center
996 + $history_obj->setSilentNotificationCenter(true);
991 997
992 - // room custom cost
993 - $or_cust_cost = $cust_cost > 0.00 ? $cust_cost : 0;
994 - $or_cust_cost = $or_forend > 1 && $or_cust_cost > 0 ? round(($or_cust_cost / $or_forend), 2) : $or_cust_cost;
995 - // room cost from website rate plan is always based on one room
996 - $or_room_cost = $room_cost > 0.00 ? $room_cost : 0;
997 - if (!empty($split_stay_data) && $cust_cost > 0) {
998 - // set the average cost per room in case of split stay
999 - $cost_per_room = ($cust_cost / count($split_stay_data));
1000 - $or_cust_cost = round($cost_per_room, 2);
1001 - if (isset($split_stay_data[$rind]) && isset($split_stay_data[$rind]['nights'])) {
1002 - // count the average cost per room depending on the number of nights of stay
1003 - $cost_per_room = $cust_cost / $daysdiff * $split_stay_data[$rind]['nights'];
1004 - $or_cust_cost = round($cost_per_room, 2);
1005 - }
1006 - }
998 + // store event history log
999 + $history_obj->store($ev_type, $ev_descr);
1000 + }
1007 1001
1008 - // room guests
1009 - $room_adults = isset($adults_map[$r]) && empty($split_stay_data) ? $adults_map[$r] : $adults;
1010 - $room_children = isset($children_map[$r]) && empty($split_stay_data) ? $children_map[$r] : $children;
1002 + // check if the commission (compensation) amount should be set
1003 + if ($options['compensation'] ?? null) {
1004 + // update the total commissions amount
1005 + $booking_record->cmms = (float) $options['compensation'];
1011 1006
1012 - // store room record
1013 - $room_record = new stdClass;
1014 - $room_record->idorder = (int)$newoid;
1015 - $room_record->idroom = (int)$nowroom['id'];
1016 - $room_record->adults = $room_adults;
1017 - $room_record->children = $room_children;
1018 - $room_record->idtar = !empty($id_tariff) ? $id_tariff : null;
1019 - $room_record->t_first_name = $t_first_name;
1020 - $room_record->t_last_name = $t_last_name;
1021 - $room_record->roomindex = count($room_indexes) ? (int)$room_indexes[$use_ind_key] : null;
1022 - $room_record->cust_cost = $cust_cost > 0.00 ? $or_cust_cost : null;
1023 - $room_record->cust_idiva = $cust_cost > 0.00 && !empty($id_tax) ? $id_tax : null;
1024 - $room_record->room_cost = $room_cost > 0.00 ? $or_room_cost : null;
1007 + // update cached booking information for history
1008 + $booking['cmms'] = (float) $options['compensation'];
1009 + $history_obj->setBookingInfo($booking);
1025 1010
1026 - $dbo->insertObject('#__vikbooking_ordersrooms', $room_record, 'id');
1011 + // update booking record in VBO
1012 + if ($dbo->updateObject('#__vikbooking_orders', $booking_record, 'id')) {
1013 + // increase counter
1014 + $booking_values_counter++;
1015 + }
1027 1016
1028 - if (!isset($room_record->id)) {
1029 - $this->setError('Could not store room reservation record for booking ID ' . $room_record->idorder);
1030 - continue;
1031 - }
1017 + // set a generic CM event type
1018 + $ev_type = 'CM';
1032 1019
1033 - // Assign room specific unit
1034 - if ($room_indexes) {
1035 - $room_indexes_usemap[$nowroom['id']]++;
1036 - }
1037 - }
1038 - }
1039 - } elseif ($status == 'standby') {
1040 - // store booking record
1041 - $dbo->insertObject('#__vikbooking_orders', $booking, 'id');
1020 + // build event description
1021 + $ev_descr = 'OTA Commissions: ' . $options['compensation'];
1042 1022
1043 - if (!isset($booking->id)) {
1044 - $this->setError('Could not store the reservation record');
1045 - return false;
1046 - }
1023 + // set event extra data
1024 + $history_obj->setExtraData([
1025 + 'cmms' => (float) $options['compensation'],
1026 + ]);
1047 1027
1048 - // get the newly generated booking ID
1049 - $newoid = $booking->id;
1028 + // silence the notification center
1029 + $history_obj->setSilentNotificationCenter(true);
1050 1030
1051 - // set the new booking ID
1052 - $this->setNewBookingID($newoid);
1031 + // store event history log
1032 + $history_obj->store($ev_type, $ev_descr);
1033 + }
1053 1034
1054 - if (!empty($split_stay_data)) {
1055 - // save transient on db for split stay information
1056 - VBOFactory::getConfig()->set('split_stay_' . $newoid, json_encode($split_stay_data));
1057 - }
1035 + return (bool) $booking_values_counter;
1036 + }
1058 1037
1059 - // write room records
1060 - foreach ($rooms_pool as $rind => $nowroom) {
1061 - for ($r = 1; $r <= $or_forend; $r++) {
1062 - // room custom cost
1063 - $or_cust_cost = $cust_cost > 0.00 ? $cust_cost : 0;
1064 - $or_cust_cost = $or_forend > 1 && $or_cust_cost > 0 ? round(($or_cust_cost / $or_forend), 2) : $or_cust_cost;
1065 - // room cost from website rate plan is always based on one room
1066 - $or_room_cost = $room_cost > 0.00 ? $room_cost : 0;
1067 - if (!empty($split_stay_data) && $cust_cost > 0) {
1068 - // set the average cost per room in case of split stay
1069 - $cost_per_room = ($cust_cost / count($split_stay_data));
1070 - $or_cust_cost = round($cost_per_room, 2);
1071 - if (isset($split_stay_data[$rind]) && isset($split_stay_data[$rind]['nights'])) {
1072 - // count the average cost per room depending on the number of nights of stay
1073 - $cost_per_room = $cust_cost / $daysdiff * $split_stay_data[$rind]['nights'];
1074 - $or_cust_cost = round($cost_per_room, 2);
1075 - }
1076 - }
1038 + /**
1039 + * Updates the value for the booking OTA type data payload.
1040 + *
1041 + * @param array $data OTA type-data payload to set.
1042 + * @param ?int $bookingId Optional booking ID to update.
1043 + *
1044 + * @return bool
1045 + *
1046 + * @throws Exception
1047 + *
1048 + * @since 1.18.8 (J) - 1.8.8 (WP)
1049 + */
1050 + public function updateOTATypeData(array $data, ?int $bookingId = null)
1051 + {
1052 + $dbo = JFactory::getDbo();
1077 1053
1078 - // room guests
1079 - $room_adults = isset($adults_map[$r]) && empty($split_stay_data) ? $adults_map[$r] : $adults;
1080 - $room_children = isset($children_map[$r]) && empty($split_stay_data) ? $children_map[$r] : $children;
1054 + if (!$bookingId) {
1055 + // get the internal booking record
1056 + $booking = $this->getBooking();
1057 + $bookingId = $booking['id'] ?? null;
1058 + }
1081 1059
1082 - // store room record
1083 - $room_record = new stdClass;
1084 - $room_record->idorder = (int)$newoid;
1085 - $room_record->idroom = (int)$nowroom['id'];
1086 - $room_record->adults = $room_adults;
1087 - $room_record->children = $room_children;
1088 - $room_record->idtar = !empty($id_tariff) ? $id_tariff : null;
1089 - $room_record->t_first_name = $t_first_name;
1090 - $room_record->t_last_name = $t_last_name;
1091 - $room_record->cust_cost = $cust_cost > 0.00 ? $or_cust_cost : null;
1092 - $room_record->cust_idiva = $cust_cost > 0.00 && !empty($id_tax) ? $id_tax : null;
1093 - $room_record->room_cost = $room_cost > 0.00 ? $or_room_cost : null;
1060 + if (empty($bookingId)) {
1061 + throw new Exception('Missing booking ID.', 400);
1062 + }
1094 1063
1095 - $dbo->insertObject('#__vikbooking_ordersrooms', $room_record, 'id');
1064 + // update booking record
1065 + $dbo->setQuery(
1066 + $dbo->getQuery(true)
1067 + ->update($dbo->qn('#__vikbooking_orders'))
1068 + ->set($dbo->qn('ota_type_data') . ' = ' . $dbo->q(json_encode($data)))
1069 + ->where($dbo->qn('id') . ' = ' . $bookingId)
1070 + );
1071 + $dbo->execute();
1096 1072
1097 - if (!isset($room_record->id)) {
1098 - $this->setError('Could not store room reservation record for booking ID ' . $room_record->idorder);
1099 - continue;
1100 - }
1073 + return (bool) $dbo->getAffectedRows();
1074 + }
1101 1075
1102 - if (empty($split_stay_data)) {
1103 - // lock room for pending status
1104 - $tmplock_record = new stdClass;
1105 - $tmplock_record->idroom = (int)$room['id'];
1106 - $tmplock_record->checkin = $checkin_ts;
1107 - $tmplock_record->checkout = $checkout_ts;
1108 - $tmplock_record->until = VikBooking::getMinutesLock(true);
1109 - $tmplock_record->realback = $realback_ts;
1110 - $tmplock_record->idorder = (int)$newoid;
1076 + /**
1077 + * Updates the registration status for the provided booking.
1078 + *
1079 + * @param int $status Registration status to set.
1080 + * @param ?int $bookingId Optional booking ID to update.
1081 + *
1082 + * @return bool
1083 + *
1084 + * @throws Exception
1085 + *
1086 + * @since 1.18.15 (J) - 1.8.15 (WP)
1087 + */
1088 + public function updateRegistration(int $status, ?int $bookingId = null)
1089 + {
1090 + $dbo = JFactory::getDbo();
1111 1091
1112 - $dbo->insertObject('#__vikbooking_tmplock', $tmplock_record, 'id');
1113 - }
1114 - }
1115 - }
1092 + if (!$bookingId) {
1093 + // get the internal booking record
1094 + $booking = $this->getBooking();
1095 + $bookingId = $booking['id'] ?? null;
1096 + }
1116 1097
1117 - if (!empty($split_stay_data)) {
1118 - // lock rooms for pending status on proper stay dates
1119 - foreach ($split_stay_data as $split_stay) {
1120 - $tmplock_record = new stdClass;
1121 - $tmplock_record->idroom = (int)$split_stay['idroom'];
1122 - $tmplock_record->checkin = (int)$split_stay['checkin_ts'];
1123 - $tmplock_record->checkout = (int)$split_stay['checkout_ts'];
1124 - $tmplock_record->until = VikBooking::getMinutesLock(true);
1125 - $tmplock_record->realback = (int)$split_stay['realback_ts'];
1126 - $tmplock_record->idorder = (int)$newoid;
1098 + if (empty($bookingId)) {
1099 + throw new Exception('Missing booking ID.', 400);
1100 + }
1127 1101
1128 - $dbo->insertObject('#__vikbooking_tmplock', $tmplock_record, 'id');
1129 - }
1130 - }
1131 - }
1102 + $knownStatuses = [-1, 0, 1, 2];
1132 1103
1133 - $newoid = $this->getNewBookingID();
1134 - if (!$newoid) {
1135 - return false;
1136 - }
1104 + if (!in_array($status, $knownStatuses, true)) {
1105 + throw new InvalidArgumentException('Invalid registration status.', 400);
1106 + }
1137 1107
1138 - // assign booking to customer
1139 - if (!$cpin->getNewCustomerId() && !empty($customer_id)) {
1140 - $cpin->setNewPin($customer_pin);
1141 - $cpin->setNewCustomerId($customer_id);
1142 - }
1143 - $cpin->saveCustomerBooking($newoid);
1108 + // update booking record
1109 + $dbo->setQuery(
1110 + $dbo->getQuery(true)
1111 + ->update($dbo->qn('#__vikbooking_orders'))
1112 + ->set($dbo->qn('checked') . ' = ' . $status)
1113 + ->where($dbo->qn('id') . ' = ' . $bookingId)
1114 + );
1115 + $dbo->execute();
1144 1116
1145 - // Booking History
1146 - $forced_reason = !empty($forced_reason) ? " {$forced_reason}" : $forced_reason;
1147 - VikBooking::getBookingHistoryInstance()->setBid($newoid)->store('NB', "({$now_user->name}){$forced_reason}");
1117 + return (bool) $dbo->getAffectedRows();
1118 + }
1148 1119
1149 - if ($status == 'confirmed' || ($status == 'standby' && class_exists('VCMRequestAvailability'))) {
1150 - // Invoke Channel Manager
1151 - $vcm_autosync = VikBooking::vcmAutoUpdate();
1152 - if ($vcm_autosync > 0) {
1153 - $vcm_obj = VikBooking::getVcmInvoker();
1154 - $vcm_obj->setOids([$newoid])->setSyncType('new');
1155 - $sync_result = $vcm_obj->doSync();
1156 - if ($sync_result === false) {
1157 - // set error message
1158 - $vcm_err = $vcm_obj->getError();
1159 - $this->setError(JText::translate('VBCHANNELMANAGERRESULTKO') . (!empty($vcm_err) ? ' - ' . $vcm_err : ''));
1120 + /**
1121 + * Updates the booking-level phone number or alias phone number (masked phone).
1122 + *
1123 + * @param ?string $phone The phone number value to set, even null to unset.
1124 + * @param bool $isAlias Whether to update the real or alias phone number.
1125 + * @param ?int $bookingId Optional booking ID to update.
1126 + *
1127 + * @return bool
1128 + *
1129 + * @throws Exception
1130 + *
1131 + * @since 1.18.16 (J) - 1.8.16 (WP)
1132 + */
1133 + public function updatePhoneNumber(?string $phone, bool $isAlias = false, ?int $bookingId = null)
1134 + {
1135 + $dbo = JFactory::getDbo();
1160 1136
1161 - // return true because the booking was actually stored
1162 - return true;
1163 - }
1164 - } elseif (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'synch.vikbooking.php')) {
1165 - // set the necessary action to invoke VCM
1166 - $vcm_sync_url = 'index.php?option=com_vikbooking&task=invoke_vcm&stype=new&cid[]=' . $newoid . '&returl=' . urlencode('index.php?option=com_vikbooking&task=calendar&cid[]=' . $room['id']);
1137 + if (!$bookingId) {
1138 + // get the internal booking record
1139 + $booking = $this->getBooking();
1140 + $bookingId = $booking['id'] ?? null;
1141 + }
1167 1142
1168 - $this->setChannelManagerAction(JText::translate('VBCHANNELMANAGERINVOKEASK') . ' <button type="button" class="btn btn-primary" onclick="document.location.href=\'' . $vcm_sync_url . '\';">' . JText::translate('VBCHANNELMANAGERSENDRQ') . '</button>');
1169 - }
1170 - }
1143 + if (empty($bookingId)) {
1144 + throw new Exception('Missing booking ID.', 400);
1145 + }
1171 1146
1172 - return true;
1173 - }
1147 + $dbo->setQuery(
1148 + $dbo->getQuery(true)
1149 + ->update('#__vikbooking_orders')
1150 + ->set($dbo->qn($isAlias ? 'phone_alias' : 'phone') . ' = ' . ($phone ? $dbo->q($phone) : 'NULL'))
1151 + ->where($dbo->qn('id') . ' = ' . (int) $bookingId)
1152 + );
1153 + $dbo->execute();
1154 +
1155 + return (bool) $dbo->getAffectedRows();
1156 + }
1157 +
1158 + /**
1159 + * Modifies the requested booking ID according to the provided options.
1160 + * This method does not support all rate plan options like for the creation of a
1161 + * new booking. This is a method for making quick updates concerning a room switch,
1162 + * a change of stay dates, new booking total amount, guests, add extra services etc..
1163 + * For modifying a reservation with the same data as for the creation, see update().
1164 + *
1165 + * @param array $options List of details to perform the modification.
1166 + *
1167 + * @return bool
1168 + *
1169 + * @since 1.16.10 (J) - 1.6.10 (WP)
1170 + */
1171 + public function modify(array $options)
1172 + {
1173 + $dbo = JFactory::getDbo();
1174 +
1175 + // access the previous booking details
1176 + $prev_booking = $this->getBooking();
1177 +
1178 + // access the current rooms booked
1179 + $roomBooking = $this->getRoomBooking();
1180 +
1181 + // gather modification options
1182 + $booking_id = $options['booking_id'] ?? $prev_booking['id'] ?? 0;
1183 +
1184 + if (!$booking_id) {
1185 + $this->setError('Missing booking ID.');
1186 + return false;
1187 + }
1188 +
1189 + if (!$prev_booking) {
1190 + // load current booking record if not injected
1191 + $prev_booking = VikBooking::getBookingInfoFromID($booking_id);
1192 + if (!$prev_booking) {
1193 + $this->setError('Booking not found.');
1194 + return false;
1195 + }
1196 + }
1197 +
1198 + if (!$roomBooking) {
1199 + // load current rooms booked
1200 + $roomBooking = VikBooking::loadOrdersRoomsData($booking_id);
1201 + }
1202 +
1203 + // do not touch this array property because it's used by VCM
1204 + $prev_booking['rooms_info'] = $roomBooking;
1205 +
1206 + // list of operations to trigger/perform
1207 + $trigger_operations = [];
1208 +
1209 + // list of history description rows
1210 + $history_descr_rows = [];
1211 +
1212 + // access availability helper
1213 + $av_helper = VikBooking::getAvailabilityInstance(true);
1214 +
1215 + // calculate the new stay dates, if different
1216 + $diff_stay_dates = false;
1217 + $set_checkin = date('Y-m-d', $prev_booking['checkin']);
1218 + $set_checkout = date('Y-m-d', $prev_booking['checkout']);
1219 + if (($options['checkin'] ?? null)) {
1220 + // date is expected in military format
1221 + $diff_stay_dates = $diff_stay_dates || ($options['checkin'] != $set_checkin);
1222 + $set_checkin = $options['checkin'];
1223 + }
1224 + if (($options['checkout'] ?? null)) {
1225 + // date is expected in military format
1226 + $diff_stay_dates = $diff_stay_dates || ($options['checkout'] != $set_checkout);
1227 + $set_checkout = $options['checkout'];
1228 + }
1229 +
1230 + // ensure the stay dates are valid
1231 + if (JFactory::getDate($set_checkin) >= JFactory::getDate($set_checkout)) {
1232 + $this->setError('Invalid stay dates provided.');
1233 + return false;
1234 + }
1235 +
1236 + // ensure we are not changing dates for a split-stay reservation
1237 + if ($diff_stay_dates && !empty($prev_booking['split_stay'])) {
1238 + // we receive the stay dates at booking record, so we cannot proceed with the update
1239 + $this->setError('Cannot modify the stay dates for a split-stay reservation. Please do it manually.');
1240 + return false;
1241 + }
1242 +
1243 + // set dates involved
1244 + $av_helper->setStayDates($set_checkin, $set_checkout);
1245 +
1246 + // count new nights of stay
1247 + $set_nights = $av_helper->countNightsOfStay();
1248 +
1249 + // gather stay timestamps
1250 + list($set_checkin_ts, $set_checkout_ts) = $av_helper->getStayDates(true);
1251 +
1252 + // load the current busy record IDs before any modification, if any
1253 + $dbo->setQuery(
1254 + $dbo->getQuery(true)
1255 + ->select('*')
1256 + ->from($dbo->qn('#__vikbooking_ordersbusy'))
1257 + ->where($dbo->qn('idorder') . ' = ' . (int) $booking_id)
1258 + );
1259 + $busy_ids = array_column($dbo->loadAssocList(), 'idbusy');
1260 +
1261 + // first off, check if any room switch was requested (recommended one switch at most)
1262 + $switching_details = [];
1263 + if ($options['switch_rooms'] ?? []) {
1264 + // get all room IDs for the switch that were not booked already
1265 + $booked_rooms = array_column($roomBooking, 'idroom');
1266 + $new_missing_rooms = array_values(array_diff((array) $options['switch_rooms'], $booked_rooms));
1267 +
1268 + // scan all rooms requested for the switch that were not booked already
1269 + foreach ($new_missing_rooms as $index => $switch_room_id) {
1270 + if (!isset($roomBooking[$index])) {
1271 + // adding more rooms is not supported
1272 + break;
1273 + }
1274 + // ensure the room switch is allowed (room should be available on the new dates)
1275 + $switched_room_info = VikBooking::getRoomInfo($switch_room_id, ['id', 'name', 'units']);
1276 + if (!$switched_room_info) {
1277 + $this->setError('The requested room could not be found for the switch.');
1278 + return false;
1279 + }
1280 + if (!VikBooking::roomBookable($switch_room_id, 1, $set_checkin_ts, $set_checkout_ts, $busy_ids)) {
1281 + // abort by setting a descriptive error message
1282 + $this->setError(sprintf(
1283 + 'The room %s is not available from %s to %s, and so the room switch cannot be made.',
1284 + $switched_room_info['name'] ?? '',
1285 + $set_checkin,
1286 + $set_checkout
1287 + ));
1288 + return false;
1289 + }
1290 + }
1291 +
1292 + // scan again all rooms to be switched once we know they are available
1293 + foreach ($new_missing_rooms as $index => $switch_room_id) {
1294 + if (!isset($roomBooking[$index])) {
1295 + // adding more rooms is not supported
1296 + break;
1297 + }
1298 +
1299 + // update room-booking record by switching room ID
1300 + $q = $dbo->getQuery(true)
1301 + ->update($dbo->qn('#__vikbooking_ordersrooms'))
1302 + ->set($dbo->qn('idroom') . ' = ' . (int) $switch_room_id)
1303 + ->where($dbo->qn('idorder') . ' = ' . (int) $booking_id)
1304 + ->where($dbo->qn('idroom') . ' = ' . (int) ($roomBooking[$index]['idroom'] ?? 0));
1305 + $dbo->setQuery($q, 0, 1);
1306 + $dbo->execute();
1307 +
1308 + if ($busy_ids) {
1309 + // update busy records with new stay dates just for the switched room
1310 + $q = $dbo->getQuery(true)
1311 + ->update($dbo->qn('#__vikbooking_busy'))
1312 + ->set($dbo->qn('idroom') . ' = ' . (int) $switch_room_id)
1313 + ->set($dbo->qn('checkin') . ' = ' . $dbo->q($set_checkin_ts))
1314 + ->set($dbo->qn('checkout') . ' = ' . $dbo->q($set_checkout_ts))
1315 + ->set($dbo->qn('realback') . ' = ' . $dbo->q($set_checkout_ts + (VikBooking::getHoursRoomAvail() * 3600)))
1316 + ->where($dbo->qn('id') . ' IN (' . implode(', ', array_map('intval', $busy_ids)) . ')')
1317 + ->where($dbo->qn('idroom') . ' = ' . (int) ($roomBooking[$index]['idroom'] ?? 0));
1318 + $dbo->setQuery($q, 0, 1);
1319 + $dbo->execute();
1320 +
1321 + // register room switching details
1322 + $switching_details[$index] = $switch_room_id;
1323 +
1324 + // register CM sync operation
1325 + $trigger_operations[] = 'vcm_sync';
1326 + }
1327 +
1328 + // register history description row
1329 + $switched_room_info = VikBooking::getRoomInfo($switch_room_id, ['id', 'name', 'units']);
1330 + $prev_room_info = VikBooking::getRoomInfo($roomBooking[$index]['idroom'] ?? 0, ['id', 'name', 'units']);
1331 + $history_descr_rows[] = sprintf('%s switched with %s.', $prev_room_info['name'] ?? '', $switched_room_info['name'] ?? '');
1332 + }
1333 + }
1334 +
1335 + // start query builder for booking record
1336 + $bookingQ = $dbo->getQuery(true)
1337 + ->update($dbo->qn('#__vikbooking_orders'))
1338 + ->where($dbo->qn('id') . ' = ' . (int) $booking_id);
1339 +
1340 + // modify stay dates, if requested
1341 + if ($diff_stay_dates) {
1342 + // ensure all rooms are bookable on the new stay dates
1343 + if ($prev_booking['status'] == 'confirmed') {
1344 + foreach ($roomBooking as $kor => $or) {
1345 + if ($switching_details[$kor] ?? null) {
1346 + // this room index was switched with another room, hence we know it was available
1347 + continue;
1348 + }
1349 + if (!VikBooking::roomBookable($or['idroom'], 1, $set_checkin_ts, $set_checkout_ts, $busy_ids)) {
1350 + // abort
1351 + $abort_room_info = VikBooking::getRoomInfo($or['idroom'], ['id', 'name', 'units']);
1352 + $this->setError(sprintf(
1353 + 'The room %s is not available from %s to %s, and so the stay dates cannot be modified.',
1354 + $abort_room_info['name'] ?? '',
1355 + $set_checkin,
1356 + $set_checkout
1357 + ));
1358 + return false;
1359 + }
1360 + }
1361 + }
1362 +
1363 + // set booking values to update
1364 + $bookingQ->set($dbo->qn('checkin') . ' = ' . $dbo->q($set_checkin_ts));
1365 + $bookingQ->set($dbo->qn('checkout') . ' = ' . $dbo->q($set_checkout_ts));
1366 + $bookingQ->set($dbo->qn('days') . ' = ' . $dbo->q($set_nights));
1367 +
1368 + // update busy records, if any (reservation status could be confirmed)
1369 + if ($busy_ids) {
1370 + // update busy records with new stay dates for all rooms
1371 + $dbo->setQuery(
1372 + $dbo->getQuery(true)
1373 + ->update($dbo->qn('#__vikbooking_busy'))
1374 + ->set($dbo->qn('checkin') . ' = ' . $dbo->q($set_checkin_ts))
1375 + ->set($dbo->qn('checkout') . ' = ' . $dbo->q($set_checkout_ts))
1376 + ->set($dbo->qn('realback') . ' = ' . $dbo->q($set_checkout_ts + (VikBooking::getHoursRoomAvail() * 3600)))
1377 + ->where($dbo->qn('id') . ' IN (' . implode(', ', array_map('intval', $busy_ids)) . ')')
1378 + );
1379 + $dbo->execute();
1380 +
1381 + // register CM sync operation
1382 + $trigger_operations[] = 'vcm_sync';
1383 + }
1384 +
1385 + // register operation to trigger the shared calendars
1386 + $trigger_operations[] = 'shared_calendars';
1387 + }
1388 +
1389 + // update number of guests, if requested
1390 + if (($options['guests']['adults'] ?? null) || ($options['guests']['children'] ?? null)) {
1391 + // update the requested number of guests ONLY on the first room booked
1392 + $q = $dbo->getQuery(true)
1393 + ->update($dbo->qn('#__vikbooking_ordersrooms'))
1394 + ->set($dbo->qn('adults') . ' = ' . (int) ($options['guests']['adults'] ?? $roomBooking[0]['adults']))
1395 + ->set($dbo->qn('children') . ' = ' . (int) ($options['guests']['children'] ?? $roomBooking[0]['children']))
1396 + ->where($dbo->qn('idorder') . ' = ' . (int) $booking_id);
1397 + $dbo->setQuery($q, 0, 1);
1398 + $dbo->execute();
1399 +
1400 + // register history description row
1401 + $history_descr_rows[] = sprintf(
1402 + 'New adults %d, new children %d.',
1403 + (int) ($options['guests']['adults'] ?? $roomBooking[0]['adults']),
1404 + (int) ($options['guests']['children'] ?? $roomBooking[0]['children'])
1405 + );
1406 + }
1407 +
1408 + // check if extra services should be added and calculate the booking cost difference
1409 + $new_extras_cost = 0;
1410 + if (is_array(($options['add_extra_services'] ?? null))) {
1411 + $current_extras = !empty($roomBooking[0]['extracosts']) ? json_decode($roomBooking[0]['extracosts'], true) : [];
1412 + $current_extras = is_array($current_extras) ? $current_extras : [];
1413 + $new_extras = [];
1414 + foreach ($options['add_extra_services'] as $extras) {
1415 + if (!is_array($extras) || (!isset($extras['name']) && !isset($extras['cost']))) {
1416 + // invalid extra service structure
1417 + continue;
1418 + }
1419 +
1420 + // build new extra service
1421 + $new_extra = [
1422 + 'name' => (string) ($extras['name'] ?? 'Custom Extra'),
1423 + 'cost' => (float) ($extras['cost'] ?? 0),
1424 + 'idtax' => null,
1425 + ];
1426 +
1427 + // push custom extra service
1428 + $current_extras[] = $new_extra;
1429 +
1430 + // push the custom extra service in the new list
1431 + $new_extras[] = $new_extra;
1432 + }
1433 +
1434 + if ($new_extras) {
1435 + // update the extra services ONLY on the first room booked
1436 + $q = $dbo->getQuery(true)
1437 + ->update($dbo->qn('#__vikbooking_ordersrooms'))
1438 + ->set($dbo->qn('extracosts') . ' = ' . $dbo->q(json_encode($current_extras)))
1439 + ->where($dbo->qn('idorder') . ' = ' . (int) $booking_id);
1440 + $dbo->setQuery($q, 0, 1);
1441 + $dbo->execute();
1442 +
1443 + // register history description row
1444 + $history_descr_rows[] = sprintf(
1445 + 'New extras: %s.',
1446 + implode(', ', array_column($new_extras, 'name'))
1447 + );
1448 +
1449 + // check if we need to increase the booking total amount
1450 + $new_extras_cost = array_sum(array_column($new_extras, 'cost'));
1451 +
1452 + if ($new_extras_cost > 0 && (float) ($options['cost_difference'] ?? 0) < $new_extras_cost) {
1453 + // increase the "cost difference" due to the newly added extra services
1454 + $options['cost_difference'] = ($options['cost_difference'] ?? 0) + $new_extras_cost;
1455 + }
1456 + }
1457 + }
1458 +
1459 + // check if the booking total amount should change
1460 + if ($options['cost_difference'] ?? null) {
1461 + // this difference should be summed to (or deducted from) the current booking total value
1462 + $bookingQ->set($dbo->qn('total') . ' = ' . ($prev_booking['total'] + (float) $options['cost_difference']));
1463 +
1464 + // register history description row
1465 + $history_descr_rows[] = sprintf(
1466 + 'Booking total cost difference calculated: %d.',
1467 + (float) $options['cost_difference']
1468 + );
1469 +
1470 + // calculate the cost difference just for the rooms
1471 + $rooms_cost_difference = (float) $options['cost_difference'] - $new_extras_cost;
1472 +
1473 + if ($rooms_cost_difference) {
1474 + // this value should be summed to (or deducted from) the current room rate to have a proper calculation
1475 + $new_room_cost = null;
1476 + $room_cost_prop = null;
1477 + if (!empty($roomBooking[0]['cust_cost'])) {
1478 + $new_room_cost = $roomBooking[0]['cust_cost'] + $rooms_cost_difference;
1479 + $room_cost_prop = 'cust_cost';
1480 + } elseif (!empty($roomBooking[0]['room_cost'])) {
1481 + $new_room_cost = $roomBooking[0]['room_cost'] + $rooms_cost_difference;
1482 + $room_cost_prop = 'room_cost';
1483 + }
1484 +
1485 + if ($room_cost_prop) {
1486 + // we can update the room cost for the difference calculated ONLY on the first room booked
1487 + // if no room cost was found, maybe because of a tariff, we would keep just the total changed
1488 + $q = $dbo->getQuery(true)
1489 + ->update($dbo->qn('#__vikbooking_ordersrooms'))
1490 + ->set($dbo->qn($room_cost_prop) . ' = ' . $new_room_cost)
1491 + ->where($dbo->qn('idorder') . ' = ' . (int) $booking_id);
1492 + $dbo->setQuery($q, 0, 1);
1493 + $dbo->execute();
1494 + }
1495 + }
1496 + }
1497 +
1498 + if ($options['extra_notes'] ?? '') {
1499 + // update administrator notes
1500 + $bookingQ->set($dbo->qn('adminnotes') . ' = ' . $dbo->q(trim($prev_booking['adminnotes'] . "\n" . $options['extra_notes'])));
1501 + }
1502 +
1503 + if (($options['custmail'] ?? '') || ($options['customer_email'] ?? '')) {
1504 + // update guest email address at booking level
1505 + $set_cust_mail = $options['custmail'] ?? $options['customer_email'] ?? '';
1506 + if (preg_match("/^[^@]+@[^@]+\.[^@]+$/", $set_cust_mail)) {
1507 + // email pattern is safe
1508 + $bookingQ->set($dbo->qn('custmail') . ' = ' . $dbo->q(trim($set_cust_mail)));
1509 + }
1510 + }
1511 +
1512 + // finally, update the booking record
1513 + try {
1514 + // make sure something to update was set by using the apposite getter magic method
1515 + if ($bookingQ->set) {
1516 + // some booking record values should be updated
1517 + $dbo->setQuery($bookingQ);
1518 + $dbo->execute();
1519 + }
1520 + } catch (Throwable $e) {
1521 + $this->setError($e->getMessage());
1522 + return false;
1523 + }
1524 +
1525 + // update booking history
1526 + $history_obj = VikBooking::getBookingHistoryInstance($booking_id);
1527 +
1528 + $now_user = JFactory::getUser();
1529 + $caller_id = $now_user->name ? "({$now_user->name})" : '';
1530 + if ($this->getCaller()) {
1531 + $caller_id = '(' . $this->getCaller() . ')';
1532 + if ($this->getHistoryData()) {
1533 + $history_obj->setExtraData($this->getHistoryData());
1534 + }
1535 + }
1536 +
1537 + // update Booking History
1538 + $history_obj->store('MB', $caller_id . ($history_descr_rows ? "\n" . implode("\n", $history_descr_rows) : ''));
1539 +
1540 + // check for the operations to perform
1541 + if (in_array('shared_calendars', $trigger_operations)) {
1542 + // unset any previously booked room due to calendar sharing
1543 + VikBooking::cleanSharedCalendarsBusy($booking_id);
1544 + // check if some of the rooms booked have shared calendars
1545 + VikBooking::updateSharedCalendars($booking_id);
1546 + }
1547 +
1548 + if (in_array('vcm_sync', $trigger_operations)) {
1549 + // invoke Channel Manager
1550 + $vcm_autosync = VikBooking::vcmAutoUpdate();
1551 + if ($vcm_autosync > 0) {
1552 + $vcm_obj = VikBooking::getVcmInvoker();
1553 + $vcm_obj->setOids([$booking_id])->setSyncType('modify')->setOriginalBooking($prev_booking);
1554 + $sync_result = $vcm_obj->doSync();
1555 + if ($sync_result === false) {
1556 + // set error message
1557 + $vcm_err = $vcm_obj->getError();
1558 + $this->setError(JText::translate('VBCHANNELMANAGERRESULTKO') . (!empty($vcm_err) ? ' - ' . $vcm_err : ''));
1559 + }
1560 + } elseif (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'synch.vikbooking.php')) {
1561 + // set the necessary action to invoke VCM manually
1562 + $this->setChannelManagerAction(
1563 + JText::translate('VBCHANNELMANAGERINVOKEASK') . ' ' .
1564 + '<form action="index.php?option=com_vikbooking" method="post">' .
1565 + '<input type="hidden" name="option" value="com_vikbooking"/>' .
1566 + '<input type="hidden" name="task" value="invoke_vcm"/>' .
1567 + '<input type="hidden" name="stype" value="modify"/>' .
1568 + '<input type="hidden" name="cid[]" value="' . $booking_id . '"/>' .
1569 + '<input type="hidden" name="origb" value="' . urlencode(json_encode($prev_booking)) . '"/>' .
1570 + '<button type="submit" class="btn btn-primary">' . JText::translate('VBCHANNELMANAGERSENDRQ') . '</button>' .
1571 + '</form>'
1572 + );
1573 + }
1574 + }
1575 +
1576 + if (($options['ota_reporting'] ?? null) && $diff_stay_dates) {
1577 + // perform the OTA reporting action, if allowed
1578 + if (class_exists('VCMOtaReporting') && VCMOtaReporting::getInstance($ord)->stayChangeAllowed()) {
1579 + // check if an OTA reporting action is needed
1580 + $ota_stay_change_data = [];
1581 + foreach ($roomBooking as $kor => $or) {
1582 + // set room data for stay change
1583 + $ota_stay_change_room = [
1584 + 'idroom' => $or['idroom'],
1585 + 'checkin' => $set_checkin,
1586 + 'checkout' => $set_checkout,
1587 + ];
1588 + if (isset($or['modified_price'])) {
1589 + $ota_stay_change_room['price'] = $or['modified_price'];
1590 + }
1591 + // push room data for stay change
1592 + $ota_stay_change_data[] = $ota_stay_change_room;
1593 + }
1594 +
1595 + // notify the OTA through Vik Channel Manager
1596 + $ota_reporting = VCMOtaReporting::getInstance();
1597 + $ota_result = $ota_reporting->notifyStayChange($ota_stay_change_data);
1598 + if (!$ota_result) {
1599 + // register error message
1600 + $this->setError($ota_reporting->getError());
1601 + }
1602 + }
1603 + }
1604 +
1605 + return true;
1606 + }
1607 +
1608 + /**
1609 + * Deletes the requested booking ID and related records.
1610 + *
1611 + * @param array $options List of details to perform the cancellation.
1612 + * @param bool $reCreating True if the booking is being re-created (updated).
1613 + *
1614 + * @return bool
1615 + *
1616 + * @since 1.16.10 (J) - 1.6.10 (WP)
1617 + * @since 1.18.11 (J) - 1.8.11 (WP) added argument $reCreating.
1618 + */
1619 + public function delete(array $options, bool $reCreating = false)
1620 + {
1621 + $dbo = JFactory::getDbo();
1622 +
1623 + $booking_id = $options['booking_id'] ?? 0;
1624 + $canc_reason = $options['cancellation_reason'] ?? '';
1625 + $purge_remove = $options['purge_remove'] ?? false;
1626 +
1627 + if ($reCreating && $this->prevBookingRegistry && $this->prevBookingRegistry->getID() == $booking_id) {
1628 + $booking = $this->prevBookingRegistry->getData();
1629 + } else {
1630 + $booking = VikBooking::getBookingInfoFromID($booking_id);
1631 + }
1632 +
1633 + if (!$booking) {
1634 + $this->setError('Booking not found.');
1635 + return false;
1636 + }
1637 +
1638 + if (!$reCreating && $booking['status'] === 'cancelled' && !$purge_remove) {
1639 + $this->setError(sprintf('Booking ID %d is already cancelled.', $booking['id']));
1640 + return false;
1641 + }
1642 +
1643 + if (!$reCreating && class_exists('VCMFeesCancellation')) {
1644 + // let VCM detect if there are any constraints for the cancellation
1645 + $canc_denied = VCMFeesCancellation::getInstance($booking, $anew = true)->isBookingConstrained();
1646 + if ($canc_denied) {
1647 + // set error message
1648 + $canc_deny_error = VCMFeesCancellation::getInstance()->getError();
1649 + $this->setError($canc_deny_error ?: 'Booking cannot be cancelled due to OTA contraints.');
1650 + return false;
1651 + }
1652 + }
1653 +
1654 + // access the current user
1655 + $now_user = JFactory::getUser();
1656 +
1657 + // whether OTAs should be notified
1658 + $notify_otas = false;
1659 +
1660 + if ($booking['status'] != 'cancelled') {
1661 + if (!$reCreating) {
1662 + // update status to cancelled
1663 + $q = $dbo->getQuery(true)
1664 + ->update($dbo->qn('#__vikbooking_orders'))
1665 + ->set($dbo->qn('status') . ' = ' . $dbo->q('cancelled'))
1666 + ->where($dbo->qn('id') . ' = ' . (int) $booking['id']);
1667 + if (!empty($canc_reason)) {
1668 + $set_canc_reason = (!empty($booking['adminnotes']) ? $booking['adminnotes'] . "\n" : '') . $canc_reason;
1669 + $q->set($dbo->qn('adminnotes') . ' = ' . $dbo->q($set_canc_reason));
1670 + }
1671 + $dbo->setQuery($q);
1672 + $dbo->execute();
1673 + }
1674 +
1675 + // delete temporarily locked records, if any
1676 + $dbo->setQuery(
1677 + $dbo->getQuery(true)
1678 + ->delete($dbo->qn('#__vikbooking_tmplock'))
1679 + ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
1680 + );
1681 + $dbo->execute();
1682 +
1683 + if (!$reCreating && $booking['status'] == 'confirmed') {
1684 + // turn flag on
1685 + $notify_otas = true;
1686 + }
1687 +
1688 + if (!$reCreating) {
1689 + // access history object
1690 + $history_obj = VikBooking::getBookingHistoryInstance($booking['id']);
1691 +
1692 + $caller_id = $now_user->name ? "({$now_user->name})" : '';
1693 + if ($this->getCaller()) {
1694 + $caller_id = '(' . $this->getCaller() . ')';
1695 + if ($this->getHistoryData()) {
1696 + $history_obj->setExtraData($this->getHistoryData());
1697 + }
1698 + }
1699 +
1700 + // update Booking History
1701 + $history_obj->store('CB', $caller_id);
1702 + }
1703 + }
1704 +
1705 + /**
1706 + * In case of pending bookings being cancelled, schedule the release through VCM.
1707 + *
1708 + * @since 1.18.8 (J) - 1.8.8 (WP)
1709 + */
1710 + if ($booking['status'] == 'standby' && method_exists('VCMRequestAvailability', 'setForRelease')) {
1711 + // let the CM schedule the release of the involved and unconfirmed booking IDs, if needed
1712 + VCMRequestAvailability::getInstance()->setForRelease([$booking['id']]);
1713 + }
1714 +
1715 + // always attempt to free records up
1716 + $dbo->setQuery(
1717 + $dbo->getQuery(true)
1718 + ->select('*')
1719 + ->from($dbo->qn('#__vikbooking_ordersbusy'))
1720 + ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
1721 + );
1722 + foreach ($dbo->loadAssocList() as $ob) {
1723 + // delete busy record
1724 + $dbo->setQuery(
1725 + $dbo->getQuery(true)
1726 + ->delete($dbo->qn('#__vikbooking_busy'))
1727 + ->where($dbo->qn('id') . ' = ' . (int) $ob['idbusy'])
1728 + );
1729 + $dbo->execute();
1730 + }
1731 +
1732 + // delete booking-busy-record relations
1733 + $dbo->setQuery(
1734 + $dbo->getQuery(true)
1735 + ->delete($dbo->qn('#__vikbooking_ordersbusy'))
1736 + ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
1737 + );
1738 + $dbo->execute();
1739 +
1740 + // check for purge removal
1741 + if (!$reCreating && $booking['status'] === 'cancelled' && $purge_remove) {
1742 + // delete booking-customer relation
1743 + $dbo->setQuery(
1744 + $dbo->getQuery(true)
1745 + ->delete($dbo->qn('#__vikbooking_customers_orders'))
1746 + ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
1747 + );
1748 + $dbo->execute();
1749 +
1750 + // delete booking-room relations
1751 + $dbo->setQuery(
1752 + $dbo->getQuery(true)
1753 + ->delete($dbo->qn('#__vikbooking_ordersrooms'))
1754 + ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
1755 + );
1756 + $dbo->execute();
1757 +
1758 + // delete booking-history relations
1759 + $dbo->setQuery(
1760 + $dbo->getQuery(true)
1761 + ->delete($dbo->qn('#__vikbooking_orderhistory'))
1762 + ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
1763 + );
1764 + $dbo->execute();
1765 +
1766 + // delete the booking record
1767 + $dbo->setQuery(
1768 + $dbo->getQuery(true)
1769 + ->delete($dbo->qn('#__vikbooking_orders'))
1770 + ->where($dbo->qn('id') . ' = ' . (int) $booking['id'])
1771 + );
1772 + $dbo->execute();
1773 +
1774 + // in case of split stay booking, remove the transient
1775 + if ($booking['split_stay']) {
1776 + VBOFactory::getConfig()->remove('split_stay_' . $booking['id']);
1777 + }
1778 + }
1779 +
1780 + if ($reCreating === true) {
1781 + // when updating a booking record, get rid of all rooms previously assigned
1782 +
1783 + // delete booking-room relations
1784 + $dbo->setQuery(
1785 + $dbo->getQuery(true)
1786 + ->delete($dbo->qn('#__vikbooking_ordersrooms'))
1787 + ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
1788 + );
1789 + $dbo->execute();
1790 +
1791 + // in case of split stay booking, remove the transient
1792 + if ($booking['split_stay']) {
1793 + VBOFactory::getConfig()->remove('split_stay_' . $booking['id']);
1794 + }
1795 + }
1796 +
1797 + if (!$reCreating && $notify_otas) {
1798 + $vcm_autosync = VikBooking::vcmAutoUpdate();
1799 + if ($vcm_autosync > 0) {
1800 + $vcm_obj = VikBooking::getVcmInvoker();
1801 + $vcm_obj->setOids([$booking['id']])->setSyncType('cancel');
1802 + $sync_result = $vcm_obj->doSync();
1803 + if ($sync_result === false) {
1804 + // set error message
1805 + $vcm_err = $vcm_obj->getError();
1806 + $this->setError(JText::translate('VBCHANNELMANAGERRESULTKO') . (!empty($vcm_err) ? ' - ' . $vcm_err : ''));
1807 + }
1808 + }
1809 + }
1810 +
1811 + return true;
1812 + }
1813 +
1814 + /**
1815 + * Sets a booking ID to confirmed according to the provided options.
1816 + *
1817 + * @param array $options List of details to perform the update.
1818 + *
1819 + * @return bool
1820 + *
1821 + * @since 1.17.3 (J) - 1.7.3 (WP)
1822 + */
1823 + public function setConfirmed(array $options)
1824 + {
1825 + $dbo = JFactory::getDbo();
1826 +
1827 + // access the current booking details, if any
1828 + $booking = $this->getBooking();
1829 +
1830 + // access the current rooms booked, if any
1831 + $roomBooking = $this->getRoomBooking();
1832 +
1833 + // gather modification options
1834 + $booking_id = $options['booking_id'] ?? $booking['id'] ?? 0;
1835 +
1836 + if (!$booking_id) {
1837 + $this->setError('Missing booking ID.');
1838 + return false;
1839 + }
1840 +
1841 + if (!$booking) {
1842 + // load current booking record if not injected
1843 + $booking = VikBooking::getBookingInfoFromID($booking_id);
1844 + if (!$booking) {
1845 + $this->setError('Booking not found.');
1846 + return false;
1847 + }
1848 + }
1849 +
1850 + if (!$roomBooking) {
1851 + // load current rooms booked
1852 + $roomBooking = VikBooking::loadOrdersRoomsData($booking_id);
1853 + }
1854 +
1855 + // make sure the booking status is not already confirmed
1856 + if (!strcasecmp($booking['status'], 'confirmed')) {
1857 + $this->setError('Booking is already confirmed.');
1858 + return false;
1859 + }
1860 +
1861 + // memorize the original booking status for VCM in case of OTA booking
1862 + $original_book_status = null;
1863 + if (!empty($booking['idorderota']) && !empty($booking['channel'])) {
1864 + $original_book_status = $booking['status'];
1865 + }
1866 +
1867 + // availability helper
1868 + $av_helper = VikBooking::getAvailabilityInstance(true);
1869 +
1870 + // room stay dates in case of split stay
1871 + $room_stay_dates = [];
1872 + if ($booking['split_stay']) {
1873 + $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $booking['id'], []);
1874 + }
1875 +
1876 + // make sure all rooms are available for confirmation
1877 + $turnover_secs = VikBooking::getHoursRoomAvail() * 3600;
1878 + $realback = $turnover_secs + $booking['checkout'];
1879 + $allbook = true;
1880 + $notavail = [];
1881 +
1882 + /**
1883 + * We need to calculate a minus operator for each room that was booked more than once.
1884 + * In case we are confirming a booking for more than one unit of the same room, we need to
1885 + * make sure the calculation is made properly, as only one unit of that room could be free.
1886 + */
1887 + $units_minus_oper = [];
1888 + foreach ($roomBooking as $ind => $or) {
1889 + if (!isset($units_minus_oper[$or['idroom']])) {
1890 + $units_minus_oper[$or['idroom']] = -1;
1891 + }
1892 + // increase counter
1893 + $units_minus_oper[$or['idroom']]++;
1894 + if (!empty($room_stay_dates)) {
1895 + // split stay rooms never have the same stay dates, but they should also be different rooms
1896 + $units_minus_oper[$or['idroom']] = 0;
1897 + }
1898 + }
1899 +
1900 + // list of room-units pool
1901 + $roomUnits = [];
1902 +
1903 + // check availability for each room involved
1904 + foreach ($roomBooking as $ind => $or) {
1905 + // determine proper values for this room
1906 + $room_stay_checkin = $booking['checkin'];
1907 + $room_stay_checkout = $booking['checkout'];
1908 + $room_stay_nights = $booking['days'];
1909 + if ($booking['split_stay'] && $room_stay_dates && isset($room_stay_dates[$ind]) && $room_stay_dates[$ind]['idroom'] == $or['idroom']) {
1910 + $room_stay_checkin = $room_stay_dates[$ind]['checkin_ts'] ?: $room_stay_dates[$ind]['checkin'];
1911 + $room_stay_checkout = $room_stay_dates[$ind]['checkout_ts'] ?: $room_stay_dates[$ind]['checkout'];
1912 + $room_stay_nights = $av_helper->countNightsOfStay($room_stay_checkin, $room_stay_checkout);
1913 + // inject nights calculated for this room
1914 + $room_stay_dates[$ind]['nights'] = $room_stay_nights;
1915 + }
1916 +
1917 + // get room record
1918 + $room_record = VikBooking::getRoomInfo($or['idroom']);
1919 +
1920 + // set room (total) units
1921 + $roomUnits[$or['idroom']] = (int) ($room_record['units'] ?? 1);
1922 +
1923 + // check if the room is available
1924 + if (!VikBooking::roomBookable($or['idroom'], (($room_record['units'] ?? 0) - $units_minus_oper[$or['idroom']]), $room_stay_checkin, $room_stay_checkout)) {
1925 + $allbook = false;
1926 + $notavail[] = $room_record['name'] ?? '?';
1927 + }
1928 + }
1929 +
1930 + // ensure all rooms involved were available or forced to be
1931 + if (!$allbook && !($options['force_availability'] ?? false)) {
1932 + $this->setError(sprintf('Some rooms are no longer available: %s', implode(', ', $notavail)));
1933 + return false;
1934 + }
1935 +
1936 + // occupy the involved rooms on the db
1937 + foreach ($roomBooking as $ind => $or) {
1938 + // determine proper values for this room
1939 + $room_stay_checkin = $booking['checkin'];
1940 + $room_stay_checkout = $booking['checkout'];
1941 + $room_stay_realback = $realback;
1942 + if ($booking['split_stay'] && $room_stay_dates && isset($room_stay_dates[$ind]) && $room_stay_dates[$ind]['idroom'] == $or['idroom']) {
1943 + $room_stay_checkin = $room_stay_dates[$ind]['checkin_ts'] ?: $room_stay_dates[$ind]['checkin'];
1944 + $room_stay_checkout = $room_stay_dates[$ind]['checkout_ts'] ?: $room_stay_dates[$ind]['checkout'];
1945 + $room_stay_realback = $turnover_secs + $room_stay_checkout;
1946 + }
1947 +
1948 + // determine the room units to occupy (to support closure re-confirmations)
1949 + $bookUnitsCount = (int) (intval($booking['closure'] ?? 0) ? ($roomUnits[$or['idroom']] ?? 1) : 1);
1950 +
1951 + for ($bi = 1; $bi <= $bookUnitsCount; $bi++) {
1952 + // build busy record
1953 + $busy_record = new stdClass;
1954 + $busy_record->idroom = (int) $or['idroom'];
1955 + $busy_record->checkin = (int) $room_stay_checkin;
1956 + $busy_record->checkout = (int) $room_stay_checkout;
1957 + $busy_record->realback = (int) $room_stay_realback;
1958 +
1959 + // store busy record and obtain the newly created ID
1960 + $dbo->insertObject('#__vikbooking_busy', $busy_record, 'id');
1961 + $lid = $busy_record->id ?? 0;
1962 +
1963 + // build busy relation record
1964 + $obusy_record = new stdClass;
1965 + $obusy_record->idorder = (int) $booking['id'];
1966 + $obusy_record->idbusy = (int) $lid;
1967 +
1968 + // store busy relation record
1969 + $dbo->insertObject('#__vikbooking_ordersbusy', $obusy_record, 'id');
1970 + }
1971 + }
1972 +
1973 + // delete temporarily locked records, if any
1974 + $dbo->setQuery(
1975 + $dbo->getQuery(true)
1976 + ->delete($dbo->qn('#__vikbooking_tmplock'))
1977 + ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
1978 + );
1979 + $dbo->execute();
1980 +
1981 + // update booking status (and notes, if any)
1982 + $q = $dbo->getQuery(true)
1983 + ->update($dbo->qn('#__vikbooking_orders'))
1984 + ->set($dbo->qn('status') . ' = ' . $dbo->q('confirmed'))
1985 + ->where($dbo->qn('id') . ' = ' . (int) $booking['id']);
1986 + if ($options['extra_notes'] ?? '') {
1987 + // update administrator notes
1988 + $q->set($dbo->qn('adminnotes') . ' = ' . $dbo->q(trim($booking['adminnotes'] . "\n" . $options['extra_notes'])));
1989 + }
1990 + $dbo->setQuery($q);
1991 + $dbo->execute();
1992 +
1993 + if (!empty($booking['idquote'])) {
1994 + // let the quote model handle the release of other locked room records (OTAs included), if any
1995 + VBOMvcModel::getInstance('quote')->releaseUnconfirmedSolutions((int) $booking['idquote'], (int) $booking['id']);
1996 + }
1997 +
1998 + // set booking confirmation number
1999 + $confirmnumber = VikBooking::generateConfirmNumber($booking['id'], true);
2000 +
2001 + // assign room specific unit(s)
2002 + $set_room_indexes = VikBooking::autoRoomUnit();
2003 + $room_indexes_usemap = [];
2004 +
2005 + foreach ($roomBooking as $kor => $or) {
2006 + // determine proper values for this room
2007 + $room_stay_checkin = $booking['checkin'];
2008 + $room_stay_checkout = $booking['checkout'];
2009 + $room_stay_nights = $booking['days'];
2010 + if ($booking['split_stay'] && $room_stay_dates && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
2011 + $room_stay_checkin = $room_stay_dates[$kor]['checkin_ts'] ?: $room_stay_dates[$kor]['checkin'];
2012 + $room_stay_checkout = $room_stay_dates[$kor]['checkout_ts'] ?: $room_stay_dates[$kor]['checkout'];
2013 + $room_stay_nights = $room_stay_dates[$kor]['nights'];
2014 + }
2015 +
2016 + // assign room specific unit
2017 + if ($set_room_indexes === true) {
2018 + $room_indexes = VikBooking::getRoomUnitNumsAvailable($booking, $or['idroom']);
2019 + $use_ind_key = 0;
2020 + if ($room_indexes) {
2021 + if (!isset($room_indexes_usemap[$or['idroom']])) {
2022 + $room_indexes_usemap[$or['idroom']] = $use_ind_key;
2023 + } else {
2024 + $use_ind_key = $room_indexes_usemap[$or['idroom']];
2025 + }
2026 +
2027 + // update room-reservation record by assigning the room index (unit)
2028 + $dbo->setQuery(
2029 + $dbo->getQuery(true)
2030 + ->update($dbo->qn('#__vikbooking_ordersrooms'))
2031 + ->set($dbo->qn('roomindex') . ' = ' . (int) $room_indexes[$use_ind_key])
2032 + ->where($dbo->qn('id') . ' = ' . (int) $or['id'])
2033 + );
2034 + $dbo->execute();
2035 +
2036 + // increase index counter
2037 + $room_indexes_usemap[$or['idroom']]++;
2038 + }
2039 + }
2040 + }
2041 +
2042 + // check if some of the rooms booked have shared calendars
2043 + VikBooking::updateSharedCalendars($booking['id'], array_column($roomBooking, 'idroom'), $booking['checkin'], $booking['checkout']);
2044 +
2045 + // update booking history
2046 + $history_obj = VikBooking::getBookingHistoryInstance($booking['id']);
2047 +
2048 + $now_user = JFactory::getUser();
2049 + $caller_id = $now_user->name ? "({$now_user->name})" : '';
2050 + if ($this->getCaller()) {
2051 + $caller_id = '(' . $this->getCaller() . ')';
2052 + if ($this->getHistoryData()) {
2053 + $history_obj->setExtraData($this->getHistoryData());
2054 + }
2055 + }
2056 +
2057 + // update Booking History
2058 + $history_obj->store('TC', $caller_id);
2059 +
2060 + // Invoke Channel Manager
2061 + $vcm_autosync = VikBooking::vcmAutoUpdate();
2062 + if ($vcm_autosync > 0) {
2063 + $vcm_obj = VikBooking::getVcmInvoker();
2064 + $vcm_obj->setOids([$booking['id']])->setSyncType('new')->setOriginalStatuses([$original_book_status]);
2065 + $sync_result = $vcm_obj->doSync();
2066 + if ($sync_result === false) {
2067 + // set error message
2068 + $vcm_err = $vcm_obj->getError();
2069 + $this->setError(JText::translate('VBCHANNELMANAGERRESULTKO') . (!empty($vcm_err) ? ' - ' . $vcm_err : ''));
2070 + }
2071 + } elseif (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'synch.vikbooking.php')) {
2072 + // set the necessary action to invoke VCM
2073 + $vcm_sync_url = 'index.php?option=com_vikbooking&task=invoke_vcm&stype=new&cid[]=' . $booking['id'] . '&returl=' . urlencode('index.php?option=com_vikbooking&task=editorder&cid[]=' . $booking['id']);
2074 +
2075 + $this->setChannelManagerAction(JText::translate('VBCHANNELMANAGERINVOKEASK') . ' <button type="button" class="btn btn-primary" onclick="document.location.href=\'' . $vcm_sync_url . '\';">' . JText::translate('VBCHANNELMANAGERSENDRQ') . '</button>');
2076 + }
2077 +
2078 + // check if the guest should be notified via email
2079 + if ($options['notify'] ?? null) {
2080 + // send email notification to guest
2081 + VikBooking::sendBookingEmail($booking['id'], ['guest']);
2082 +
2083 + // SMS skipping the administrator
2084 + VikBooking::sendBookingSMS($booking['id'], ['admin']);
2085 + }
2086 +
2087 + return true;
2088 + }
2089 +
2090 + /**
2091 + * Tells if the booking record set can be modified and/or cancelled.
2092 + *
2093 + * @return array
2094 + *
2095 + * @since 1.16.10 (J) - 1.6.10 (WP)
2096 + */
2097 + public function getAlterationDetails()
2098 + {
2099 + $dbo = JFactory::getDbo();
2100 +
2101 + $booking = $this->getBooking();
2102 + $roomBooking = $this->getRoomBooking();
2103 +
2104 + if (!$booking || !$roomBooking) {
2105 + $this->setError('Missing booking or room booking record details.');
2106 + return [];
2107 + }
2108 +
2109 + // gather room booking tariffs
2110 + $tars = [];
2111 + foreach ($roomBooking as $kor => $or) {
2112 + $num = $kor + 1;
2113 + if (!empty($order['pkg']) || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
2114 + // package or custom cost set from the back-end
2115 + continue;
2116 + }
2117 +
2118 + // get room tariff details
2119 + $dbo->setQuery(
2120 + $dbo->getQuery(true)
2121 + ->select($dbo->qn('t') . '.*')
2122 + ->select([
2123 + $dbo->qn('p.name'),
2124 + $dbo->qn('p.free_cancellation'),
2125 + $dbo->qn('p.canc_deadline'),
2126 + $dbo->qn('p.canc_policy'),
2127 + ])
2128 + ->from($dbo->qn('#__vikbooking_dispcost', 't'))
2129 + ->leftJoin($dbo->qn('#__vikbooking_prices', 'p') . ' ON ' . $dbo->qn('t.idprice') . ' = ' . $dbo->qn('p.id'))
2130 + ->where($dbo->qn('t.id') . ' = ' . (int) ($or['idtar'] ?? 0))
2131 + );
2132 + $tar = $dbo->loadAssoc();
2133 +
2134 + if ($tar) {
2135 + // push room booking tariff
2136 + $tars[$num] = $tar;
2137 + }
2138 + }
2139 +
2140 + // count days to arrival
2141 + $days_to_arrival = 0;
2142 + $now_info = getdate();
2143 + $checkin_info = getdate($booking['checkin'] ?? 0);
2144 + if ($now_info[0] < $checkin_info[0]) {
2145 + while ($now_info[0] < $checkin_info[0]) {
2146 + if (!($now_info['mday'] != $checkin_info['mday'] || $now_info['mon'] != $checkin_info['mon'] || $now_info['year'] != $checkin_info['year'])) {
2147 + break;
2148 + }
2149 + $days_to_arrival++;
2150 + $now_info = getdate(mktime(0, 0, 0, $now_info['mon'], ($now_info['mday'] + 1), $now_info['year']));
2151 + }
2152 + }
2153 +
2154 + // check if the rate plan(s) are refundable
2155 + $is_refundable = 0;
2156 + $daysadv_refund_arr = [];
2157 + $daysadv_refund = 0;
2158 + $canc_policy = '';
2159 + foreach ($tars as $num => $tar) {
2160 + if (!$tar['free_cancellation']) {
2161 + // if at least one rate plan is non-refundable, the whole reservation cannot be cancelled
2162 + $is_refundable = 0;
2163 + $daysadv_refund_arr = [];
2164 + break;
2165 + }
2166 + $is_refundable = 1;
2167 + $daysadv_refund_arr[] = $tar['canc_deadline'];
2168 + }
2169 +
2170 + // get the rate plan with the lowest cancellation deadline
2171 + $daysadv_refund = $daysadv_refund_arr ? min($daysadv_refund_arr) : $daysadv_refund;
2172 + if ($daysadv_refund > 0) {
2173 + foreach ($tars as $num => $tar) {
2174 + if ($tar['free_cancellation'] && $tar['canc_deadline'] == $daysadv_refund) {
2175 + // get the cancellation policy from the first rate plan with free cancellation and same cancellation deadline
2176 + $canc_policy = $tar['canc_policy'];
2177 + break;
2178 + }
2179 + }
2180 + }
2181 +
2182 + // access global settings to determine the alterations available
2183 + $resmodcanc = VikBooking::getReservationModCanc();
2184 + $resmodcanc = !$days_to_arrival ? 0 : $resmodcanc;
2185 + $resmodcancmin = VikBooking::getReservationModCancMin();
2186 +
2187 + // build alteration deadline date
2188 + $checkin_dt = JFactory::getDate(date('Y-m-d', ($booking['checkin'] ?? 0)));
2189 + $checkin_dt->modify("-{$resmodcancmin} days");
2190 + $alteration_deadline = $checkin_dt->format('Y-m-d');
2191 +
2192 + return [
2193 + 'refundable' => ($resmodcanc > 1 && $resmodcanc != 2 && $is_refundable > 0 && $daysadv_refund <= $days_to_arrival && $days_to_arrival >= $resmodcancmin),
2194 + 'modifiable' => ($resmodcanc > 1 && $resmodcanc != 3 && $days_to_arrival >= $resmodcancmin),
2195 + 'alteration_disabled' => $resmodcanc === 0,
2196 + 'request_alteration' => $resmodcanc === 1,
2197 + 'cancellation_policy' => $canc_policy ?: null,
2198 + 'alteration_deadline' => $alteration_deadline,
2199 + ];
2200 + }
2201 +
2202 + /**
2203 + * Tells whether a given booking ID can be moved to new stay dates. Ignores rooms individual stay
2204 + * dates or split stay reservations. Should be called when changing the dates for a whole booking.
2205 + *
2206 + * @param int $booking_id The booking ID to modify.
2207 + * @param int|string $new_checkin The new check-in date (timestamp or date string with no time).
2208 + * @param int|string $new_checkout The new check-out date (timestamp or date string with no time).
2209 + *
2210 + * @return bool True if all booking rooms are available on the new dates.
2211 + *
2212 + * @throws Exception
2213 + *
2214 + * @since 1.18.2 (J) - 1.8.2 (WP)
2215 + */
2216 + public function bookingModifiable(int $booking_id, $new_checkin, $new_checkout)
2217 + {
2218 + // attempt to access previous booking registry to reduce queries
2219 + if ($this->prevBookingRegistry && $this->prevBookingRegistry->getID() == $booking_id) {
2220 + $booking_rooms = $this->prevBookingRegistry->getRooms();
2221 + } else {
2222 + // load all booking rooms
2223 + $booking_rooms = VikBooking::loadOrdersRoomsData($booking_id);
2224 + }
2225 +
2226 + if (!$booking_rooms) {
2227 + throw new Exception('Could not find any rooms booked within the reservation.', 500);
2228 + }
2229 +
2230 + if (empty($new_checkin) || empty($new_checkout)) {
2231 + throw new Exception('Missing stay dates.', 500);
2232 + }
2233 +
2234 + // load the booking occupied record IDs, if any
2235 + $busy_ids = VikBooking::loadBookingBusyIds($booking_id);
2236 +
2237 + // load check-in and check-out times
2238 + list($checkin_h, $checkin_m, $checkout_h, $checkout_m) = $this->loadCheckinOutTimes();
2239 +
2240 + // get the new stay timestamps
2241 + $new_checkin = is_numeric($new_checkin) ? date('Y-m-d', $new_checkin) : $new_checkin;
2242 + $new_checkout = is_numeric($new_checkout) ? date('Y-m-d', $new_checkout) : $new_checkout;
2243 + $from_ts = VikBooking::getDateTimestamp($new_checkin, $checkin_h, $checkin_m);
2244 + $to_ts = VikBooking::getDateTimestamp($new_checkout, $checkout_h, $checkout_m);
2245 +
2246 + if ($to_ts <= $from_ts) {
2247 + throw new Exception('Invalid stay dates provided.', 500);
2248 + }
2249 +
2250 + // count room equal units
2251 + $rooms_units_counter = [];
2252 + foreach ($booking_rooms as $booking_room) {
2253 + $rooms_units_counter[$booking_room['idroom']] = ($rooms_counter[$booking_room['idroom']] ?? -1) + 1;
2254 + }
2255 +
2256 + // check if the booking modification involves other rooms
2257 + $roomsPool = $this->getRoomsPool();
2258 + $oldRoomIds = array_map('intval', array_column($booking_rooms, 'idroom'));
2259 + $newRoomIds = array_values(array_unique(array_map('intval', array_column($roomsPool, 'id'))));
2260 + $diffIds = array_diff($newRoomIds, $oldRoomIds);
2261 + foreach ($diffIds as $newId) {
2262 + $unitsRequested = 0;
2263 + $totUnits = 0;
2264 + foreach ($roomsPool as $roomData) {
2265 + if ($newId == $roomData['id']) {
2266 + $unitsRequested++;
2267 + $totUnits = (int) ($roomData['units'] ?? 1);
2268 + }
2269 + }
2270 + if (!$unitsRequested) {
2271 + continue;
2272 + }
2273 +
2274 + // update units counter map for multiple units requested
2275 + $rooms_units_counter[$newId] = $unitsRequested - 1;
2276 +
2277 + // push additional room for availability validation
2278 + $booking_rooms[] = [
2279 + 'idroom' => $newId,
2280 + 'tot_units' => $totUnits,
2281 + ];
2282 + }
2283 +
2284 + // check the availability for each room booked
2285 + $rooms_parsed = [];
2286 + foreach ($booking_rooms as $booking_room) {
2287 + if (in_array($booking_room['idroom'], $rooms_parsed)) {
2288 + continue;
2289 + }
2290 +
2291 + // count effective units to check
2292 + $effective_units = $booking_room['tot_units'] - $rooms_units_counter[$booking_room['idroom']];
2293 +
2294 + // check if the room is available on the new dates
2295 + if (!VikBooking::roomBookable($booking_room['idroom'], $effective_units, $from_ts, $to_ts, $busy_ids)) {
2296 + // the room is occupied
2297 + return false;
2298 + }
2299 +
2300 + // push room just checked
2301 + $rooms_parsed[] = $booking_room['idroom'];
2302 + }
2303 +
2304 + return true;
2305 + }
2306 +
2307 + /**
2308 + * Attempts to invoke the payment processor assigned to the current booking.
2309 + *
2310 + * @param array $card Optional credit card details to bind.
2311 + *
2312 + * @return object The payment processor dispatcher instance.
2313 + *
2314 + * @throws Exception
2315 + *
2316 + * @since 1.16.10 (J) - 1.6.10 (WP)
2317 + * @since 1.17.6 (J) - 1.7.6 (WP) added "tn_metadata" details.
2318 + */
2319 + public function getPaymentProcessor(array $card = [])
2320 + {
2321 + $booking = $this->getProperties();
2322 + $processor = null;
2323 + $payment = [];
2324 +
2325 + if (!$booking) {
2326 + throw new Exception('Missing booking details', 500);
2327 + }
2328 +
2329 + if (!empty($booking['idpayment'])) {
2330 + $payment = VikBooking::getPayment($booking['idpayment']);
2331 + }
2332 +
2333 + if (!$payment) {
2334 + throw new Exception('Missing payment method details', 500);
2335 + }
2336 +
2337 + // set payment details internally
2338 + $this->set('_payment_info', $payment);
2339 +
2340 + if ($card) {
2341 + // inject CC details for the payment processor
2342 + $booking['card'] = $card;
2343 + }
2344 +
2345 + // get the booking customer record, if any
2346 + $customer = $this->getCustomer();
2347 + if (!$customer) {
2348 + $customer = VikBooking::getCPinInstance()->getCustomerFromBooking($booking['id']);
2349 + }
2350 +
2351 + // build and inject transaction metadata
2352 + $booking['tn_metadata'] = [
2353 + 'booking_id' => $booking['id'],
2354 + 'source' => (($booking['channel'] ?? '') ?: 'Website'),
2355 + 'ota_booking_id' => (($booking['idorderota'] ?? '') ?: ''),
2356 + 'guest_name' => implode(' ', array_filter([($customer['first_name'] ?? ''), ($customer['last_name'] ?? '')])),
2357 + 'guest_email' => $customer['email'] ?? null,
2358 + 'guest_phone' => $customer['phone'] ?? null,
2359 + 'guest_country' => $customer['country'] ?? null,
2360 + ];
2361 +
2362 + /**
2363 + * Trigger event to allow third-party plugins to manipulate the transaction data.
2364 + *
2365 + * @since 1.18.5 (J) - 1.8.5 (WP)
2366 + */
2367 + VBOFactory::getPlatform()->getDispatcher()->trigger('onInitPaymentTransaction', [&$booking, &$payment['params'], $customer]);
2368 +
2369 + if (VBOPlatformDetection::isWordPress()) {
2370 + /**
2371 + * @wponly The payment gateway is loaded
2372 + * through the apposite dispatcher.
2373 + */
2374 + JLoader::import('adapter.payment.dispatcher');
2375 + $processor = JPaymentDispatcher::getInstance('vikbooking', $payment['file'], $booking, $payment['params']);
2376 + } elseif (VBOPlatformDetection::isJoomla()) {
2377 + /**
2378 + * @joomlaonly The Payment Factory library will invoke the gateway.
2379 + */
2380 + require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'payments' . DIRECTORY_SEPARATOR . 'libraries' . DIRECTORY_SEPARATOR . 'factory.php';
2381 + $processor = VBOPaymentFactory::getPaymentInstance($payment['file'], $booking, $payment['params']);
2382 + }
2383 +
2384 + if (!$processor) {
2385 + throw new Exception('Could not invoke the payment processor', 500);
2386 + }
2387 +
2388 + // return the valid payment processor instance
2389 + return $processor;
2390 + }
2391 +
2392 + /**
2393 + * Gets the reservation's payment method name.
2394 + *
2395 + * @return string
2396 + *
2397 + * @since 1.16.10 (J) - 1.6.10 (WP)
2398 + */
2399 + public function getPaymentName()
2400 + {
2401 + // access the reserved property
2402 + $payment = (array) $this->get('_payment_info', []);
2403 +
2404 + if (!$payment) {
2405 + return '';
2406 + }
2407 +
2408 + return $payment['name'] ?? '';
2409 + }
2410 +
2411 + /**
2412 + * Attempts to get the most recent transaction data for an off-session capturing.
2413 + *
2414 + * @param string $tn_driver Optional payment processor driver name.
2415 + *
2416 + * @return object[] Eligible transaction data list or empty array.
2417 + *
2418 + * @since 1.18.0 (J) - 1.8.0 (WP)
2419 + */
2420 + public function getOffSessionTransactionData(string $tn_driver = '')
2421 + {
2422 + // transaction data validation callback
2423 + $tn_data_callback = function($data) use ($tn_driver) {
2424 + return (is_object($data) && isset($data->driver) && (!$tn_driver || basename($data->driver, '.php') == basename($tn_driver, '.php')) && ($data->future_usage ?? null));
2425 + };
2426 +
2427 + // get previous transactions (in date ascending order)
2428 + $prev_tn_data = (array) VikBooking::getBookingHistoryInstance($this->get('id', 0))->getEventsWithData(['P0', 'PN'], $tn_data_callback);
2429 +
2430 + if (!$prev_tn_data) {
2431 + return [];
2432 + }
2433 +
2434 + // return the eligible transaction data list in reverse order
2435 + return array_reverse(array_values(array_filter(array_map(function($data) {
2436 + if (is_array($data)) {
2437 + // cast to object
2438 + $data = (object) $data;
2439 + }
2440 + return is_object($data) ? $data : null;
2441 + }, $prev_tn_data))));
2442 + }
2443 +
2444 + /**
2445 + * Attempts to get the credit card value pairs from the current booking.
2446 + *
2447 + * @return array Associative list of CC value-pairs, if any.
2448 + *
2449 + * @since 1.16.10 (J) - 1.6.10 (WP)
2450 + */
2451 + public function getCardValuePairs()
2452 + {
2453 + $booking_info = $this->getProperties();
2454 +
2455 + if (empty($booking_info['paymentlog']) && empty($booking_info['idorderota'])) {
2456 + // do not proceed when no CC data is available, or cannot be obtained via API
2457 + return [];
2458 + }
2459 +
2460 + // build complete credit card payload, if available
2461 + $cc_payload_str = '';
2462 +
2463 + // extract CC data from payment logs by ensuring they're not null
2464 + $booking_info['paymentlog'] = (string) $booking_info['paymentlog'];
2465 + if (stripos($booking_info['paymentlog'], 'card number') !== false && strpos($booking_info['paymentlog'], '*') !== false) {
2466 + // matched a log for an OTA CC
2467 + $cc_payload_str = $booking_info['paymentlog'];
2468 + } elseif (preg_match("/(([\d\*]{4,4}\s*){4,4})|(([\d\*]{4,6}\s*){3,3})/", $booking_info['paymentlog'])) {
2469 + // matched a credit card
2470 + $cc_payload_str = $booking_info['paymentlog'];
2471 + }
2472 +
2473 + // check if this is an OTA reservation with remotely decoded CC details required
2474 + $remote_cc_data = [];
2475 + if (!empty($booking_info['idorderota']) && !empty($booking_info['channel'])) {
2476 + // channel source
2477 + $channel_source = (string)$booking_info['channel'];
2478 + if (strpos($booking_info['channel'], '_') !== false) {
2479 + $channelparts = explode('_', $booking_info['channel']);
2480 + $channel_source = $channelparts[0];
2481 + }
2482 +
2483 + // only updated versions of VCM will support remote CC decoding for OTA reservations
2484 + if (class_exists('VCMOtaBooking')) {
2485 + // invoke the OTA Booking helper class from VCM
2486 + $cc_helper = VCMOtaBooking::getInstance([
2487 + 'channel_source' => $channel_source,
2488 + 'ota_id' => $booking_info['idorderota'],
2489 + 'booking' => $booking_info,
2490 + ], $anew = true);
2491 +
2492 + if (method_exists($cc_helper, 'decodeCreditCardDetails')) {
2493 + $remote_cc_data = $cc_helper->decodeCreditCardDetails();
2494 + // make sure the response was valid
2495 + if (!$remote_cc_data || !empty($remote_cc_data['error'])) {
2496 + // we ignore the error by simply resetting the array
2497 + $remote_cc_data = [];
2498 + }
2499 + }
2500 + }
2501 + }
2502 +
2503 + // check if we have already a full VCC
2504 + if (($remote_cc_data['card_number'] ?? '') && strlen(preg_replace('/[^0-9]/', '', (string) $remote_cc_data['card_number'])) >= 15) {
2505 + // do not merge any local data and return the full VCC details
2506 + return $remote_cc_data;
2507 + }
2508 +
2509 + // merge remotely decoded CC details with parsed payment log (if any)
2510 + return array_merge($remote_cc_data, $this->parseCreditCardValuePairs($cc_payload_str, $remote_cc_data));
2511 + }
2512 +
2513 + /**
2514 + * Given a raw string of credit card key-value pairs from payments log,
2515 + * parse the corresponding keys and values into an associative array.
2516 + * In case of conflicting keys with the remotely decoded CC details,
2517 + * attempts to replace the masked numbers with asterisks.
2518 + *
2519 + * @param string $cc_payload the raw CC details from payment logs.
2520 + * @param array $remote_cc_data associative array of decoded CC data.
2521 + *
2522 + * @return array associative or empty array.
2523 + *
2524 + * @since 1.16.10 (J) - 1.6.10 (WP) moved from widget Virtual Terminal.
2525 + */
2526 + protected function parseCreditCardValuePairs($cc_payload, array $remote_cc_data = [])
2527 + {
2528 + $cc_value_pairs = [];
2529 +
2530 + if (empty($cc_payload)) {
2531 + return $cc_value_pairs;
2532 + }
2533 +
2534 + $cc_lines = preg_split("/(\r\n|\n|\r)/", $cc_payload);
2535 +
2536 + foreach ($cc_lines as $cc_line) {
2537 + if (strpos($cc_line, ':') === false) {
2538 + continue;
2539 + }
2540 +
2541 + $cc_line_parts = explode(':', $cc_line);
2542 +
2543 + if (empty($cc_line_parts[0]) || !strlen(trim($cc_line_parts[1]))) {
2544 + continue;
2545 + }
2546 +
2547 + $key = str_replace(' ', '_', strtolower($cc_line_parts[0]));
2548 + $value = trim($cc_line_parts[1]);
2549 +
2550 + if (isset($cc_value_pairs[$key])) {
2551 + /**
2552 + * Do not overwrite existing keys because this probably means that the
2553 + * credit card was updated by an OTA like Booking.com, hence the payment
2554 + * logs string in VBO may contain the information of two different cards.
2555 + * New credit card details are always pre-pended by VCM in the payment logs.
2556 + */
2557 + continue;
2558 + }
2559 +
2560 + if (!empty($remote_cc_data[$key]) && is_string($remote_cc_data[$key]) && strpos($value, '*') !== false) {
2561 + // replace masked numbers with remote content
2562 + $value = $this->replaceMaskedNumbers($value, $remote_cc_data[$key]);
2563 + }
2564 +
2565 + $cc_value_pairs[$key] = $value;
2566 + }
2567 +
2568 + return $cc_value_pairs;
2569 + }
2570 +
2571 + /**
2572 + * Given a local and a remote credit card number string with
2573 + * masked symbols, replaces the values in the corresponding
2574 + * positions with the unmasked numbers.
2575 + *
2576 + * @param string $local current string with masked values.
2577 + * @param string $remote remote string with unmasked values.
2578 + *
2579 + * @return string the local string with unmasked values.
2580 + *
2581 + * @since 1.16.10 (J) - 1.6.10 (WP) moved from widget Virtual Terminal.
2582 + */
2583 + protected function replaceMaskedNumbers($local, $remote)
2584 + {
2585 + // split anything but numbers
2586 + $numbers = preg_split("/([^0-9]+)/", trim($remote));
2587 +
2588 + if ($numbers) {
2589 + // filter empty values
2590 + $numbers = array_filter($numbers);
2591 + }
2592 +
2593 + if (!$numbers) {
2594 + // unable to proceed
2595 + return $local;
2596 + }
2597 +
2598 + // split anything but stars (asterisks)
2599 + $stars = preg_split("/([^\*]+)/", trim($local));
2600 +
2601 + if ($stars) {
2602 + // filter empty values
2603 + $stars = array_filter($stars);
2604 + }
2605 +
2606 + if (!$stars) {
2607 + // unable to proceed
2608 + return $local;
2609 + }
2610 +
2611 + // replace masked symbols with numbers at their first occurrence
2612 + foreach ($numbers as $k => $unmasked) {
2613 + if (!isset($stars[$k])) {
2614 + continue;
2615 + }
2616 +
2617 + $masked_pos = strpos($local, $stars[$k]);
2618 +
2619 + if ($masked_pos === false) {
2620 + continue;
2621 + }
2622 +
2623 + $local = substr_replace($local, $unmasked, $masked_pos, strlen($stars[$k]));
2624 + }
2625 +
2626 + // return the string with possibly unmasked values
2627 + return $local;
2628 + }
2629 +
2630 + /**
2631 + * Tells whether the booking can be created. By default this
2632 + * is only allowed from the administrator section of the site.
2633 + *
2634 + * @return bool
2635 + */
2636 + protected function canCreate()
2637 + {
2638 + return $this->get('_isAdministrator') || JFactory::getApplication()->isClient('administrator');
2639 + }
2640 +
2641 + /**
2642 + * Gets and sets the tariff ID if a rate plan was set.
2643 + *
2644 + * @param ?array $roomData Optional room data to evaluate.
2645 + *
2646 + * @return int The tariff ID found, or 0.
2647 + *
2648 + * @since 1.18.8 (J) - 1.8.8 (WP) added argument $roomData for multi-room booking context.
2649 + */
2650 + protected function loadTariffID(?array $roomData = null)
2651 + {
2652 + $dbo = JFactory::getDbo();
2653 +
2654 + $id_tariff = 0;
2655 +
2656 + // access current room data
2657 + $room = $roomData ?: $this->getRoom();
2658 +
2659 + // current nights of stay
2660 + $daysdiff = (int) (($room['nights'] ?? 0) ?: $this->get('nights', 1));
2661 +
2662 + // make sure we have a rate plan ID with a cost set
2663 + if (!empty($room['id']) && !empty($room['id_price']) && !empty($room['room_cost']) && !boolval($this->get('set_closed', 0)) && !$this->get('split_stay', [])) {
2664 + // load tariff for given room, rate plan and nights of stay
2665 + $dbo->setQuery(
2666 + $dbo->getQuery(true)
2667 + ->select($dbo->qn('id'))
2668 + ->from($dbo->qn('#__vikbooking_dispcost'))
2669 + ->where($dbo->qn('idroom') . ' = ' . (int) $room['id'])
2670 + ->where($dbo->qn('days') . ' = ' . $daysdiff)
2671 + ->where($dbo->qn('idprice') . ' = ' . (int) $room['id_price'])
2672 + );
2673 +
2674 + $id_tariff = (int) $dbo->loadResult();
2675 + }
2676 +
2677 + // set current tariff ID
2678 + $this->set('id_tariff', $id_tariff);
2679 +
2680 + // return the current tariff ID
2681 + return $id_tariff;
2682 + }
2683 +
2684 + /**
2685 + * Applies the turnover time to the checkout timestamp and sets its value.
2686 + *
2687 + * @return int the turnover seconds applied.
2688 + */
2689 + protected function applyTurnover()
2690 + {
2691 + $turnover_secs = 0;
2692 + $checkout = $this->get('checkout', 0);
2693 +
2694 + if ($checkout) {
2695 + // turnover time
2696 + $turnover_secs = VikBooking::getHoursRoomAvail() * 3600;
2697 +
2698 + $this->set('checkout_real', ($checkout + $turnover_secs));
2699 + }
2700 +
2701 + $this->set('turnover_secs', $turnover_secs);
2702 +
2703 + return $turnover_secs;
2704 + }
2705 +
2706 + /**
2707 + * Returns an associative list of all rooms data.
2708 + *
2709 + * @return array
2710 + *
2711 + * @since 1.18.8 (J) - 1.8.8 (WP)
2712 + */
2713 + protected function loadAllRoomsData()
2714 + {
2715 + if (!$this->allRooms) {
2716 + // load once and cache internally
2717 + $this->allRooms = VikBooking::getAvailabilityInstance(true)->loadRooms();
2718 + }
2719 +
2720 + return $this->allRooms;
2721 + }
2722 +
2723 + /**
2724 + * Returns the details of a specific room ID.
2725 + *
2726 + * @param ?int $rid Optional room ID to fetch.
2727 + *
2728 + * @return array The record found or empty array.
2729 + */
2730 + protected function getRoomDetails(?int $rid = null)
2731 + {
2732 + $all_rooms = $this->loadAllRoomsData();
2733 +
2734 + if (!$rid) {
2735 + $inj_room = $this->getRoom();
2736 + $rid = $inj_room['id'] ?? 0;
2737 + }
2738 +
2739 + if ($rid && isset($all_rooms[$rid])) {
2740 + return $all_rooms[$rid];
2741 + }
2742 +
2743 + return [];
2744 + }
2745 +
2746 + /**
2747 + * Gets the list of rooms involved in the reservation in case of
2748 + * closures or if multiple rooms were set for booking.
2749 + *
2750 + * @return array The list of rooms involved.
2751 + */
2752 + protected function getRoomsPool()
2753 + {
2754 + // preload all rooms
2755 + $av_helper = VikBooking::getAvailabilityInstance(true);
2756 + $all_rooms = $this->loadAllRoomsData();
2757 +
2758 + // access the first room set for booking
2759 + $room = $this->getRoom();
2760 + if (empty($room['id'])) {
2761 + return [];
2762 + }
2763 + $room = $this->getRoomDetails($room['id']);
2764 +
2765 + // gather values
2766 + $set_close_others = (array) $this->get('close_others', []);
2767 + $split_stay_data = $this->get('split_stay', []);
2768 + $set_closed = (int) $this->get('set_closed');
2769 + $turnover_secs = $this->get('turnover_secs', 0);
2770 + $hcheckin = $this->get('checkin_h', 12);
2771 + $mcheckin = $this->get('checkin_m', 0);
2772 + $hcheckout = $this->get('checkout_h', 10);
2773 + $mcheckout = $this->get('checkout_m', 0);
2774 +
2775 + // build containers
2776 + $rooms_pool = [];
2777 + $closeothers = [];
2778 +
2779 + if ($set_close_others && $set_closed) {
2780 + // prepend current room for closing
2781 + array_unshift($set_close_others, $room['id']);
2782 + }
2783 + $set_close_others = array_unique($set_close_others);
2784 +
2785 + foreach ($set_close_others as $closeid) {
2786 + if (empty($closeid)) {
2787 + continue;
2788 + }
2789 + if ((int) $closeid === -1) {
2790 + // close all rooms
2791 + $closeothers = [];
2792 + foreach ($all_rooms as $cr) {
2793 + array_push($closeothers, $cr);
2794 + }
2795 + break;
2796 + }
2797 + foreach ($all_rooms as $cr) {
2798 + if ((int) $cr['id'] == (int) $closeid) {
2799 + // push the main room or one of the other rooms requested for closure
2800 + array_push($closeothers, $cr);
2801 + break;
2802 + }
2803 + }
2804 + }
2805 +
2806 + if (!$closeothers || !$set_closed) {
2807 + // check if we are in a multi-room booking context
2808 + $multi_rooms = $this->getRooms();
2809 + if (count($multi_rooms) > 1) {
2810 + // multiple rooms set for booking
2811 + $rooms_pool = array_values(array_filter(array_map(function($roomData) {
2812 + return $this->getRoomDetails($roomData['id'] ?? -1);
2813 + }, $multi_rooms)));
2814 + } else {
2815 + // single room involved
2816 + $rooms_pool = [$room];
2817 + }
2818 + } else {
2819 + // set all rooms involved
2820 + $rooms_pool = $closeothers;
2821 + }
2822 +
2823 + // check split stay rooms booking
2824 + if (!empty($split_stay_data)) {
2825 + // reset pool and set it with the split stay rooms
2826 + $rooms_pool = [];
2827 + foreach ($split_stay_data as $sps_k => $split_stay) {
2828 + if (!isset($all_rooms[$split_stay['idroom']])) {
2829 + continue;
2830 + }
2831 + // calculate and set the exact check-in and check-out timestamps for this split-room
2832 + $split_stay['checkin_ts'] = VikBooking::getDateTimestamp($split_stay['checkin'], $hcheckin, $mcheckin);
2833 + $split_stay['checkout_ts'] = VikBooking::getDateTimestamp($split_stay['checkout'], $hcheckout, $mcheckout);
2834 + $split_stay['realback_ts'] = $turnover_secs + $split_stay['checkout_ts'];
2835 + $split_stay['nights'] = $av_helper->countNightsOfStay($split_stay['checkin_ts'], $split_stay['checkout_ts']);
2836 + $split_stay_data[$sps_k] = $split_stay;
2837 + // push room data to pool after storing additional information
2838 + $room_data = $all_rooms[$split_stay['idroom']];
2839 + $room_data['checkin_ts'] = $split_stay['checkin_ts'];
2840 + $room_data['checkout_ts'] = $split_stay['checkout_ts'];
2841 + $rooms_pool[] = $room_data;
2842 + }
2843 + if (!$rooms_pool) {
2844 + $this->setError('No valid rooms for the split stay booking');
2845 + return [];
2846 + }
2847 + // update split stay data manipulated
2848 + $this->set('split_stay', $split_stay_data);
2849 + }
2850 +
2851 + return $rooms_pool;
2852 + }
2853 +
2854 + /**
2855 + * Checks if the rooms are available on the requested dates.
2856 + *
2857 + * @param array $rooms_pool Pool of rooms involved in the reservation.
2858 + *
2859 + * @return bool
2860 + *
2861 + * @since 1.18.8 (J) - 1.8.8 (WP) added argument $rooms_pool.
2862 + */
2863 + protected function checkRoomsAvailability(array $rooms_pool)
2864 + {
2865 + if (empty($rooms_pool[0]['id'])) {
2866 + // invalid argument
2867 + return false;
2868 + }
2869 +
2870 + // gather variables
2871 + $split_stay_data = $this->get('split_stay', []);
2872 + $set_closed = $this->get('set_closed', 0);
2873 + $num_rooms = $this->isMultiRoom() ? count($rooms_pool) : $this->get('num_rooms', 1);
2874 + $default_checkin = $this->get('checkin', 0);
2875 + $default_checkout = $this->get('checkout', 0);
2876 + $check_locked = (bool) $this->get('check_locked', 0);
2877 +
2878 + // determine availability
2879 + $rooms_available = true;
2880 +
2881 + // check if we are dealing with split-stay data or regular booking values
2882 + if (empty($split_stay_data)) {
2883 + // make sure the rooms are available
2884 + if ($this->isMultiRoom() && !$set_closed) {
2885 + // multi-room booking context with support for stay dates at room-level
2886 + $roomBookings = $this->getRooms();
2887 + $roomStayData = [];
2888 + $roomUnitsUse = [];
2889 + $roomNamesMap = [];
2890 + foreach ($rooms_pool as $index => $room) {
2891 + $roomUnitsUse[$room['id']] = ($roomUnitsUse[$room['id']] ?? 0) + 1;
2892 + $roomNamesMap[$room['id']] = $room['name'] ?? $room['id'];
2893 + $roomStayData[] = [
2894 + 'idroom' => $room['id'],
2895 + 'units' => ($room['units'] ?? 1),
2896 + 'checkin' => $roomBookings[$index]['checkin'] ?? $default_checkin,
2897 + 'checkout' => $roomBookings[$index]['checkout'] ?? $default_checkout,
2898 + ];
2899 + }
2900 + foreach ($roomStayData as $roomStay) {
2901 + // check the remaining availability for the number of room units booked
2902 + $unitsBooked = $roomUnitsUse[$roomStay['idroom']];
2903 + $check_units = $roomStay['units'] - $unitsBooked + 1;
2904 + if ($check_locked) {
2905 + // check if the room is available and not temporarily locked
2906 + $rooms_available = VikBooking::roomNotLocked($roomStay['idroom'], $check_units, $roomStay['checkin'], $roomStay['checkout'], true);
2907 + } else {
2908 + // check if the room is available
2909 + $rooms_available = VikBooking::roomBookable($roomStay['idroom'], $check_units, $roomStay['checkin'], $roomStay['checkout']);
2910 + }
2911 + if (!$rooms_available) {
2912 + // set an error and abort
2913 + $this->setError(sprintf(
2914 + 'Room "%s" is not available from %s to %s. Looking for %d free unit(s) over a total of %d.',
2915 + $roomNamesMap[$roomStay['idroom']],
2916 + date('Y-m-d H:i', $roomStay['checkin']),
2917 + date('Y-m-d H:i', $roomStay['checkout']),
2918 + $unitsBooked,
2919 + $roomStay['units']
2920 + ));
2921 + break;
2922 + }
2923 + }
2924 + } else {
2925 + // single-room booking context
2926 + $check_units = $rooms_pool[0]['units'] ?? 1;
2927 + if ($num_rooms > 1 && $num_rooms <= $check_units && !$set_closed) {
2928 + // only when non closing the room we check the availability for the units requested for booking
2929 + $check_units = $check_units - $num_rooms + 1;
2930 + }
2931 + if ($check_locked) {
2932 + // check if the room is available and not temporarily locked
2933 + $rooms_available = VikBooking::roomNotLocked($rooms_pool[0]['id'], $check_units, $default_checkin, $default_checkout, true);
2934 + } else {
2935 + // check if the room is available
2936 + $rooms_available = VikBooking::roomBookable($rooms_pool[0]['id'], $check_units, $default_checkin, $default_checkout);
2937 + }
2938 + }
2939 + } else {
2940 + $all_rooms = $this->loadAllRoomsData();
2941 + // make sure the rooms for the split stay are available
2942 + foreach ($split_stay_data as $split_stay) {
2943 + if (!isset($all_rooms[$split_stay['idroom']])) {
2944 + $rooms_available = false;
2945 + break;
2946 + }
2947 + $rooms_available = $rooms_available && VikBooking::roomBookable($split_stay['idroom'], $all_rooms[$split_stay['idroom']]['units'], $split_stay['checkin_ts'], $split_stay['checkout_ts']);
2948 + }
2949 + }
2950 +
2951 + return $rooms_available;
2952 + }
2953 +
2954 + /**
2955 + * In case the reservation is forced or is a closure, we detect the
2956 + * forced reason to eventually attach it to the booking history.
2957 + *
2958 + * @param bool $rooms_available Whether the rooms are available.
2959 + *
2960 + * @return void
2961 + */
2962 + protected function detectForcedReason($rooms_available = true)
2963 + {
2964 + $split_stay_data = $this->get('split_stay', []);
2965 + $force_booking = $this->get('force_booking', 0);
2966 + $set_closed = $this->get('set_closed', 0);
2967 +
2968 + $forced_reason = $this->get('forced_reason', '');
2969 +
2970 + if (empty($split_stay_data)) {
2971 + // eventually build string for the description of the history event
2972 + if (($force_booking || $set_closed) && !$rooms_available) {
2973 + $forced_reason = JText::translate('VBO_FORCED_BOOKDATES');
2974 + }
2975 + } else {
2976 + $all_rooms = $this->loadAllRoomsData();
2977 + // set "split stay" as the description of the history event
2978 + $forced_reason = JText::translate('VBO_SPLIT_STAY') . "\n";
2979 + foreach ($split_stay_data as $sps_k => $split_stay) {
2980 + // describe the split stay for each room
2981 + if (!isset($all_rooms[$split_stay['idroom']])) {
2982 + continue;
2983 + }
2984 + $room_stay_nights = $split_stay['nights'];
2985 + $forced_reason .= $all_rooms[$split_stay['idroom']]['name'] . ': ' . $room_stay_nights . ' ' . ($room_stay_nights > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY')) . ', ';
2986 + $forced_reason .= $split_stay['checkin'] . ' - ' . $split_stay['checkout'] . "\n";
2987 + }
2988 + $forced_reason = rtrim($forced_reason, "\n");
2989 + }
2990 +
2991 + $this->set('forced_reason', $forced_reason);
2992 + }
2993 +
2994 + /**
2995 + * Stores the customer information to a new or existing record.
2996 + * In case of success, the customer ID property is updated.
2997 + * The customer shall be stored before the reservation records.
2998 + *
2999 + * @return bool
3000 + */
3001 + protected function storeCustomer()
3002 + {
3003 + $dbo = JFactory::getDbo();
3004 +
3005 + $inj_customer = $this->getCustomer();
3006 + $first_name = !empty($inj_customer['first_name']) ? $inj_customer['first_name'] : '';
3007 + $last_name = !empty($inj_customer['last_name']) ? $inj_customer['last_name'] : '';
3008 + $custdata = !empty($inj_customer['data']) ? $inj_customer['data'] : '';
3009 + $email = !empty($inj_customer['email']) ? $inj_customer['email'] : '';
3010 + $country = !empty($inj_customer['country']) ? $inj_customer['country'] : '';
3011 + $phone = !empty($inj_customer['phone']) ? $inj_customer['phone'] : '';
3012 + $gender = !empty($inj_customer['gender']) ? $inj_customer['gender'] : '';
3013 + $address = !empty($inj_customer['address']) ? $inj_customer['address'] : '';
3014 + $city = !empty($inj_customer['city']) ? $inj_customer['city'] : '';
3015 + $zip = !empty($inj_customer['zip']) ? $inj_customer['zip'] : '';
3016 +
3017 + // custom fields
3018 + $q = "SELECT * FROM `#__vikbooking_custfields` ORDER BY `ordering` ASC;";
3019 + $dbo->setQuery($q);
3020 + $all_cfields = $dbo->loadAssocList();
3021 +
3022 + $customer_cfields = [];
3023 + $customer_extrainfo = [];
3024 + $custdata_parts = explode("\n", $custdata);
3025 + foreach ($custdata_parts as $cdataline) {
3026 + if (!strlen(trim($cdataline))) {
3027 + continue;
3028 + }
3029 + $cdata_parts = explode(':', $cdataline);
3030 + if (count($cdata_parts) < 2 || !strlen(trim($cdata_parts[0])) || !strlen(trim($cdata_parts[1]))) {
3031 + continue;
3032 + }
3033 + foreach ($all_cfields as $cf) {
3034 + $needle = JText::translate($cf['name']);
3035 + if (!empty($needle) && strpos($cdata_parts[0], $needle) !== false && !array_key_exists($cf['id'], $customer_cfields) && $cf['type'] != 'country') {
3036 + $user_input_val = trim($cdata_parts[1]);
3037 + $customer_cfields[$cf['id']] = $user_input_val;
3038 + if (!empty($cf['flag'])) {
3039 + $customer_extrainfo[$cf['flag']] = $user_input_val;
3040 + } elseif ($cf['type'] == 'state') {
3041 + $customer_extrainfo['state'] = $user_input_val;
3042 + }
3043 + break;
3044 + }
3045 + }
3046 + }
3047 +
3048 + if (!empty($gender) && in_array(strtoupper((string) $gender), ['M', 'F'])) {
3049 + // inject the customer gender value
3050 + $customer_extrainfo['gender'] = strtoupper($gender);
3051 + }
3052 +
3053 + if (!empty($address)) {
3054 + // inject extra info
3055 + $customer_extrainfo['address'] = strlen((string) $address) > 200 ? substr($address, 0, 200) : $address;
3056 + }
3057 +
3058 + if (!empty($city)) {
3059 + // inject extra info
3060 + $customer_extrainfo['city'] = strlen((string) $city) > 50 ? substr($city, 0, 50) : $city;
3061 + }
3062 +
3063 + if (!empty($zip)) {
3064 + // inject extra info
3065 + $customer_extrainfo['zip'] = strlen((string) $zip) > 10 ? substr($zip, 0, 10) : $zip;
3066 + }
3067 +
3068 + $cpin = VikBooking::getCPinInstance();
3069 + $cpin->is_admin = true;
3070 + $cpin->setCustomerExtraInfo($customer_extrainfo);
3071 + $cpin->saveCustomerDetails($first_name, $last_name, $email, $phone, $country, $customer_cfields);
3072 +
3073 + $customer_id = $cpin->getNewCustomerId();
3074 + if (!$customer_id) {
3075 + return false;
3076 + }
3077 +
3078 + $inj_customer['id'] = $customer_id;
3079 + $this->setCustomer($inj_customer);
3080 +
3081 + return true;
3082 + }
3083 +
3084 + /**
3085 + * Returns the calculated total booking amount and total taxes.
3086 + * Sets the necessary properties with the calculated amounts.
3087 + *
3088 + * @return array list of booking total amount, taxes and fees.
3089 + */
3090 + protected function calculateTotal()
3091 + {
3092 + $dbo = JFactory::getDbo();
3093 +
3094 + // the values to calculate
3095 + $set_total = 0;
3096 + $set_taxes = 0;
3097 + $set_city_tax = 0;
3098 + $set_fees = 0;
3099 +
3100 + // access all rooms booking data
3101 + $roomsData = $this->getRooms();
3102 +
3103 + if (!$roomsData) {
3104 + return [$set_total, $set_taxes];
3105 + }
3106 +
3107 + // get booking values
3108 + $set_closed = $this->get('set_closed', 0);
3109 + $daysdiff = (int) $this->get('nights', 1);
3110 + $num_rooms = (int) $this->get('num_rooms', 1);
3111 + $split_stay_data = $this->get('split_stay', []);
3112 +
3113 + // iterate all booking rooms data
3114 + foreach ($roomsData as $index => $roomData) {
3115 + // gather booking room values
3116 + $totalpnight = ($roomData['total_or_pnight'] ?? '') ?: 'total';
3117 + $cust_cost = (float) ($roomData['cust_cost'] ?? 0);
3118 + $room_cost = (float) ($roomData['room_cost'] ?? 0);
3119 + $id_price = (int) ($roomData['id_price'] ?? 0);
3120 + $id_tax = (int) ($roomData['id_tax'] ?? 0);
3121 + $guess_tax = $roomData['guess_tax'] ?? false;
3122 +
3123 + // access room details
3124 + $roomDetails = $this->getRoomDetails($roomData['id'] ?? 0);
3125 +
3126 + // check for stay dates at room level
3127 + $roomNightsStay = ($roomData['nights'] ?? 0) ?: $daysdiff;
3128 +
3129 + // calculate room totals
3130 + $roomTotalValue = 0;
3131 + $roomTotalTax = 0;
3132 + $roomTotalCityTax = 0;
3133 + $roomTotalFees = 0;
3134 +
3135 + if ($cust_cost > 0 && !$set_closed) {
3136 + // custom cost can be per night
3137 + if ($totalpnight == 'pnight') {
3138 + $cust_cost = $cust_cost * $roomNightsStay;
3139 + }
3140 + $roomTotalValue = $cust_cost;
3141 +
3142 + if (!$id_tax && $guess_tax) {
3143 + // try to guess the tax rate
3144 + $dbo->setQuery(
3145 + $dbo->getQuery(true)
3146 + ->select($dbo->qn('id'))
3147 + ->from($dbo->qn('#__vikbooking_iva'))
3148 + ->order($dbo->qn('aliq') . ' ASC'),
3149 + 0, 1);
3150 + $guessed_id_tax = $dbo->loadResult();
3151 + if ($guessed_id_tax) {
3152 + // update the id_tax values
3153 + $id_tax = $guessed_id_tax;
3154 + $roomData['id_tax'] = $guessed_id_tax;
3155 + $this->setRoom($roomData, $index);
3156 + }
3157 + }
3158 +
3159 + // apply taxes, if necessary
3160 + if ($id_tax) {
3161 + $taxdata = VBOTaxonomySummary::getTaxRateRecord((int) $id_tax);
3162 + if (!empty($taxdata['aliq'])) {
3163 + $aliq = $taxdata['aliq'];
3164 + if (!VikBooking::ivaInclusa()) {
3165 + // add tax to the total amount
3166 + $subt = 100 + (float)$aliq;
3167 + $roomTotalValue = ($roomTotalValue * $subt / 100);
3168 + /**
3169 + * Tax Cap implementation for prices tax excluded (most common).
3170 + *
3171 + * @since 1.12 (J) - 1.2 (WP)
3172 + */
3173 + if ($taxdata['taxcap'] > 0 && ($roomTotalValue - $cust_cost) > $taxdata['taxcap']) {
3174 + $roomTotalValue = ($cust_cost + $taxdata['taxcap']);
3175 + }
3176 + // calculate tax
3177 + $roomTotalTax = $roomTotalValue - $cust_cost;
3178 + } else {
3179 + // calculate tax
3180 + $cost_minus_tax = VikBooking::sayPackageMinusIva($cust_cost, $id_tax);
3181 + $roomTotalTax += ($cust_cost - $cost_minus_tax);
3182 + }
3183 + }
3184 + }
3185 + } elseif (!empty($id_price) && $room_cost > 0 && !$set_closed && empty($split_stay_data)) {
3186 + // one website rate plan was selected, so we calculate total and taxes
3187 + $roomTotalValue = $room_cost;
3188 +
3189 + // find tax rate assigned to this rate plan
3190 + $taxdata = VBOTaxonomySummary::getTaxRecordFromRatePlan($id_price);
3191 + if (!empty($taxdata['aliq'])) {
3192 + $aliq = $taxdata['aliq'];
3193 + if (!VikBooking::ivaInclusa()) {
3194 + // add tax to the total amount
3195 + $subt = 100 + (float)$aliq;
3196 + $roomTotalValue = ($roomTotalValue * $subt / 100);
3197 + /**
3198 + * Tax Cap implementation for prices tax excluded (most common).
3199 + *
3200 + * @since 1.12 (J) - 1.2 (WP)
3201 + */
3202 + if ($taxdata['taxcap'] > 0 && ($roomTotalValue - $room_cost) > $taxdata['taxcap']) {
3203 + $roomTotalValue = ($room_cost + $taxdata['taxcap']);
3204 + }
3205 + // calculate tax
3206 + $roomTotalTax = $roomTotalValue - $room_cost;
3207 + } else {
3208 + // calculate tax
3209 + $cost_minus_tax = VikBooking::sayPackageMinusIva($room_cost, $taxdata['idiva']);
3210 + $roomTotalTax += ($room_cost - $cost_minus_tax);
3211 + }
3212 + }
3213 +
3214 + // total and taxes should be multiplied by the number of rooms booked when using a website rate plan
3215 + if ($set_closed) {
3216 + $roomTotalValue *= $roomDetails['units'];
3217 + $roomTotalTax *= $roomDetails['units'];
3218 + } elseif (!$this->isMultiRoom() && $num_rooms > 1 && $num_rooms <= $roomDetails['units']) {
3219 + $roomTotalValue *= $num_rooms;
3220 + $roomTotalTax *= $num_rooms;
3221 + }
3222 + }
3223 +
3224 + // check for options/extras at room level
3225 + if ($roomData['options'] ?? null) {
3226 + // load all the eligible options for this room party
3227 + $eligibleOptions = VBORoomHelper::getInstance()->getEligibleOptions($roomData['id'], [
3228 + 'checkin' => date('Y-m-d', ($roomData['checkin'] ?? null) ?: $this->get('checkin', 0)),
3229 + 'checkout' => date('Y-m-d', ($roomData['checkout'] ?? null) ?: $this->get('checkout', 0)),
3230 + 'adults' => $roomData['adults'] ?? $this->get('adults', 1),
3231 + 'children' => $roomData['children'] ?? $this->get('children', 0),
3232 + 'rate_id' => $roomData['id_price'] ?? null,
3233 + ]);
3234 + // obtain a list of eligible option IDs
3235 + $eligibleOptIds = array_column($eligibleOptions, 'id');
3236 + // get all eligible room options/extras that were set for booking
3237 + $roomOptData = array_values(array_filter((array) $roomData['options'], function($roomOpt) use ($eligibleOptIds) {
3238 + if (!is_array($roomOpt) || empty($roomOpt['id'])) {
3239 + return false;
3240 + }
3241 + return in_array($roomOpt['id'], $eligibleOptIds) && (!empty($roomOpt['quantity']) || !empty($roomOpt['age_intervals']));
3242 + }));
3243 + if ($roomOptData) {
3244 + // turn all eligible options into an associative list
3245 + $eligibleOptions = array_combine(array_column($eligibleOptions, 'id'), array_values($eligibleOptions));
3246 + // scan all room options to increase totals
3247 + foreach ($roomOptData as $optData) {
3248 + // obtain room option computed data
3249 + $computedData = $eligibleOptions[$optData['id']] ?? [];
3250 + // calculate option cost
3251 + $roomOptCost = 0;
3252 + if (!empty($optData['age_intervals'])) {
3253 + // children age interval option
3254 + foreach ((array) $optData['age_intervals'] as $childIndex => $ageIntervalIndex) {
3255 + // child number is 0-based
3256 + $childNumber = $childIndex + 1;
3257 + // age interval index is 1-based
3258 + $ageIntervalNumber = $ageIntervalIndex - 1;
3259 + // check if a room cost is known
3260 + $roomOptCost += (float) ($computedData['_computed_children_costs'][$childNumber][$ageIntervalNumber] ?? 0);
3261 + }
3262 + } else {
3263 + // regular option
3264 + $roomOptCost = floatval($computedData['_computed_cost'] ?? 0) * floatval($optData['quantity'] ?? 1);
3265 + }
3266 + // calculate gross and net costs
3267 + $roomOptGross = VikBooking::sayOptionalsPlusIva($roomOptCost, $computedData['idiva'] ?? 0);
3268 + $roomOptNet = VikBooking::sayOptionalsMinusIva($roomOptCost, $computedData['idiva'] ?? 0);
3269 + // increase total value
3270 + $roomTotalValue += $roomOptGross;
3271 + // increase total tax
3272 + $roomTotalTax += ($roomOptGross - $roomOptNet);
3273 + if ($computedData['is_citytax'] ?? null) {
3274 + // increase total city tax
3275 + $roomTotalCityTax += $roomOptNet;
3276 + } elseif ($computedData['is_fee'] ?? null) {
3277 + // increase total fees
3278 + $roomTotalFees += $roomOptNet;
3279 + }
3280 + }
3281 + }
3282 + }
3283 +
3284 + // check for custom extra services at room level
3285 + if ($roomData['extras'] ?? null) {
3286 + $roomExtraCosts = array_values(array_filter((array) ($roomData['extras'] ?? []), function($extraCost) {
3287 + return is_array($extraCost) && !empty($extraCost['name']);
3288 + }));
3289 + if ($roomExtraCosts) {
3290 + // normalize values
3291 + $roomExtraCosts = array_map(function($extraCost) {
3292 + return [
3293 + 'name' => $extraCost['name'],
3294 + 'cost' => (float) ($extraCost['cost'] ?? 0),
3295 + 'idtax' => (int) ($extraCost['idtax'] ?? $extraCost['vat'] ?? 0),
3296 + ];
3297 + }, $roomExtraCosts);
3298 + // scan all room extra services to increase totals
3299 + foreach ($roomExtraCosts as $roomExtraCost) {
3300 + $ecplustax = !empty($roomExtraCost['idtax']) ? VikBooking::sayOptionalsPlusIva($roomExtraCost['cost'], $roomExtraCost['idtax']) : $roomExtraCost['cost'];
3301 + $ecminustax = !empty($roomExtraCost['idtax']) ? VikBooking::sayOptionalsMinusIva($roomExtraCost['cost'], $roomExtraCost['idtax']) : $roomExtraCost['cost'];
3302 + // increase total value
3303 + $roomTotalValue += $ecplustax;
3304 + // increase total tax
3305 + $roomTotalTax += ($ecplustax - $ecminustax);
3306 + }
3307 + }
3308 + }
3309 +
3310 + // increase global totals
3311 + $set_total += $roomTotalValue;
3312 + $set_taxes += $roomTotalTax;
3313 + $set_city_tax += $roomTotalCityTax;
3314 + $set_fees += $roomTotalFees;
3315 + }
3316 +
3317 + // set values
3318 + $this->set('_total', $set_total);
3319 + $this->set('_total_tax', $set_taxes);
3320 + $this->set('_total_city_tax', $set_city_tax);
3321 + $this->set('_total_fees', $set_fees);
3322 +
3323 + // return the list of calculated values
3324 + return [
3325 + $set_total,
3326 + $set_taxes,
3327 + $set_city_tax,
3328 + $set_fees,
3329 + ];
3330 + }
3331 +
3332 + /**
3333 + * Stores (or updates) the booking and room-booking records.
3334 + * If no errors, the newly generated or updated booking id is set.
3335 + *
3336 + * @param array $rooms_pool List of rooms involved.
3337 + * @param ?int $bookingId The booking ID being updated, if any.
3338 + *
3339 + * @return bool
3340 + *
3341 + * @since 1.18.11 (J) - 1.8.11 (WP) added $bookingId argument to support updates.
3342 + */
3343 + protected function storeReservationRecords(array $rooms_pool, ?int $bookingId = null)
3344 + {
3345 + if ($bookingId && !$this->prevBookingRegistry) {
3346 + $this->setError('Missing previous booking registry for update.');
3347 + return false;
3348 + }
3349 +
3350 + $dbo = JFactory::getDbo();
3351 +
3352 + // access properties
3353 + $set_closed = $this->get('set_closed', 0);
3354 + $units_closed = $this->get('units_closed', 0);
3355 + $daysdiff = (int) $this->get('nights', 1);
3356 + $num_rooms = (int) $this->get('num_rooms', 1);
3357 + $adults = (int) $this->get('adults', 1);
3358 + $children = (int) $this->get('children', 0);
3359 + $children_age = (array) $this->get('children_age', []);
3360 + $status = $this->get('status', 'confirmed');
3361 +
3362 + $split_stay_data = $this->get('split_stay', []);
3363 + $room = $this->getRoomDetails();
3364 + if (!$room || !$rooms_pool) {
3365 + return false;
3366 + }
3367 +
3368 + // access all rooms data
3369 + $roomsData = $this->getRooms();
3370 +
3371 + // get current Joomla/WordPress User ID
3372 + $now_user = JFactory::getUser();
3373 + $store_ujid = property_exists($now_user, 'id') ? (int)$now_user->id : 0;
3374 +
3375 + // forced booking reason, status validation and additional data
3376 + $forced_reason = $this->get('forced_reason', '');
3377 + $valid_statuses = ['confirmed', 'standby'];
3378 + $status = in_array($status, $valid_statuses) ? $status : 'confirmed';
3379 + $paymentmeth = $this->get('id_payment', '');
3380 + $auto_paymeth = (bool) $this->get('auto_payment_method', false);
3381 + $set_total = (float) $this->get('_total', 0);
3382 + $set_taxes = (float) $this->get('_total_tax', 0);
3383 + $set_city_tax = (float) $this->get('_total_city_tax', 0);
3384 + $set_fees = (float) $this->get('_total_fees', 0);
3385 +
3386 + if ($bookingId) {
3387 + // updating a booking should not change the creation date
3388 + $now_ts = $this->prevBookingRegistry->getProperty('ts', time());
3389 + } else {
3390 + // booking creation date
3391 + $now_ts = time();
3392 + if ($created_on = $this->get('created_on')) {
3393 + if (is_int($created_on)) {
3394 + // timestamp expected
3395 + $now_ts = $created_on;
3396 + } elseif (preg_match('/^[0-9]{4}\-[0-9]{2}\-[0-9]{2}/', (string) $created_on)) {
3397 + // date in military format expected, with or without the time
3398 + $now_ts = strtotime($created_on);
3399 + }
3400 + }
3401 + }
3402 +
3403 + // stay dates
3404 + $checkin_ts = $this->get('checkin');
3405 + $checkout_ts = $this->get('checkout');
3406 + $realback_ts = $this->get('checkout_real', $checkout_ts);
3407 +
3408 + // customer
3409 + $cpin = VikBooking::getCPinInstance();
3410 + $inj_customer = $this->getCustomer();
3411 + $customer_id = ($inj_customer['id'] ?? null) ?: 0;
3412 + $customer_pin = ($inj_customer['pin'] ?? null) ?: '';
3413 + $t_first_name = ($inj_customer['first_name'] ?? null) ?: '';
3414 + $t_last_name = ($inj_customer['last_name'] ?? null) ?: '';
3415 + $customer_data = ($inj_customer['data'] ?? null) ?: '';
3416 + $customer_email = ($inj_customer['email'] ?? null) ?: '';
3417 + $country_code = ($inj_customer['country'] ?? null) ?: '';
3418 + $phone_number = ($inj_customer['phone'] ?? null) ?: '';
3419 + $phone_alias = ($inj_customer['phone_alias'] ?? null) ?: '';
3420 + $address = ($inj_customer['address'] ?? null) ?: '';
3421 + $city = ($inj_customer['city'] ?? null) ?: '';
3422 + $zip = ($inj_customer['zip'] ?? null) ?: '';
3423 +
3424 + if ($set_closed) {
3425 + // get the possibly custom "customer notes"
3426 + $customer_notes = $customer_data;
3427 + // overwrite "customer notes" to "Room Closed"
3428 + $customer_data = JText::translate('VBDBTEXTROOMCLOSED');
3429 + if ($customer_notes != $customer_data) {
3430 + // append the custom "custom notes" to the administrator notes instead
3431 + $custom_admin_notes = trim($this->get('admin_notes', '') . "\n" . $customer_notes);
3432 + $this->set('admin_notes', $custom_admin_notes);
3433 + }
3434 + }
3435 +
3436 + // check for default customer raw data
3437 + if (!$customer_data && $t_first_name) {
3438 + // build a default raw data string
3439 + $customer_data = "Name: {$t_first_name}\n";
3440 + if ($t_last_name) {
3441 + $customer_data .= "Last Name: {$t_last_name}\n";
3442 + }
3443 + if ($customer_email) {
3444 + $customer_data .= "eMail: {$customer_email}\n";
3445 + }
3446 + if ($phone_number) {
3447 + $customer_data .= "Phone: {$phone_number}\n";
3448 + } elseif ($phone_alias) {
3449 + $customer_data .= "Phone-alias: {$phone_alias}\n";
3450 + }
3451 + if ($country_code) {
3452 + $customer_data .= "Country: {$country_code}\n";
3453 + }
3454 + if ($address) {
3455 + $customer_data .= "Address: {$address}\n";
3456 + }
3457 + if ($city) {
3458 + $customer_data .= "City: {$city}\n";
3459 + }
3460 + if ($zip) {
3461 + $customer_data .= "Zip: {$zip}\n";
3462 + }
3463 + $customer_data = rtrim($customer_data, "\n");
3464 + }
3465 +
3466 + // ensure the country code is in the right format
3467 + if (!empty($country_code)) {
3468 + $country_code = $cpin->get3CharCountry($country_code);
3469 + }
3470 +
3471 + // ensure we have a valid country ISO code
3472 + if (strlen((string) $country_code) > 3) {
3473 + // this looks like an unknown country name
3474 + $country_code = '';
3475 + }
3476 +
3477 + // generate booking SID, unless we are updating
3478 + $sid = !$bookingId ? VikBooking::getSecretLink() : null;
3479 +
3480 + // assign room specific unit
3481 + $set_room_indexes = !$set_closed ? VikBooking::autoRoomUnit() : false;
3482 + $num_rooms = $num_rooms > 0 ? $num_rooms : 1;
3483 +
3484 + // occupancy and loop limits
3485 + $forend = 1;
3486 + $or_forend = 1;
3487 + $adults_map = [];
3488 + $children_map = [];
3489 + if ($set_closed && empty($split_stay_data)) {
3490 + $forend = $room['units'];
3491 + } elseif ($num_rooms > 1 && $num_rooms <= $room['units'] && empty($split_stay_data)) {
3492 + $forend = $num_rooms;
3493 + $or_forend = $num_rooms;
3494 + // assign adults/children proportionally
3495 + if (($adults + $children) < $num_rooms) {
3496 + // the number of guests does not make much sense but we build the maps anyway
3497 + for ($r = 1; $r <= $or_forend; $r++) {
3498 + $adults_map[$r] = $adults;
3499 + $children_map[$r] = $children;
3500 + }
3501 + } else {
3502 + $adults_per_room = floor(($adults / $num_rooms));
3503 + $adults_left = ($adults % $num_rooms);
3504 + $children_per_room = floor(($children / $num_rooms));
3505 + $children_left = ($children % $num_rooms);
3506 + for ($r = 1; $r <= $or_forend; $r++) {
3507 + $adults_map[$r] = $adults_per_room;
3508 + $children_map[$r] = $children_per_room;
3509 + if ($r == $or_forend) {
3510 + $adults_map[$r] += $adults_left;
3511 + $children_map[$r] += $children_left;
3512 + }
3513 + }
3514 + }
3515 + }
3516 +
3517 + if ($this->isMultiRoom()) {
3518 + // adjust values
3519 + $forend = 1;
3520 + $or_forend = 1;
3521 + }
3522 +
3523 + // count total rooms booked
3524 + $totalrooms = (($set_closed && $status == 'confirmed') || $this->isMultiRoom() ? count($rooms_pool) : ($num_rooms > 1 && $num_rooms <= $room['units'] ? $num_rooms : 1));
3525 + $totalrooms = !empty($split_stay_data) ? count($split_stay_data) : $totalrooms;
3526 +
3527 + // attempt to get the default payment method
3528 + if (!$paymentmeth && ($auto_paymeth || $status == 'standby')) {
3529 + // get the default payment method, if any
3530 + $paymentmeth = $this->getDefaultPaymentMethod($auto_paymeth);
3531 + } elseif ($paymentmeth && is_int($paymentmeth)) {
3532 + // convert the payment integer ID into a string with the actual record name
3533 + $paymentmeth = $this->getPaymentMethodValue($paymentmeth);
3534 + }
3535 +
3536 + if ($bookingId) {
3537 + // when updating, delete all room booking records involved
3538 + // to allow the re-generation of the updated records
3539 + $this->delete(['booking_id' => $bookingId], $reCreating = true);
3540 + }
3541 +
3542 + // prepare booking record
3543 + $booking = new stdClass;
3544 +
3545 + if ($bookingId) {
3546 + // inject current booking ID for update
3547 + $booking->id = $bookingId;
3548 + }
3549 +
3550 + // set booking record properties
3551 + $booking->custdata = $customer_data;
3552 + $booking->ts = $now_ts;
3553 + $booking->status = $status;
3554 + $booking->days = $daysdiff;
3555 + $booking->checkin = $checkin_ts;
3556 + $booking->checkout = $checkout_ts;
3557 + $booking->custmail = $customer_email;
3558 + $booking->sid = $sid;
3559 + $booking->idpayment = $paymentmeth;
3560 + $booking->ujid = (int)$store_ujid;
3561 + $booking->roomsnum = $totalrooms;
3562 + $booking->total = $set_total ?: null;
3563 + if ($this->get('admin_notes')) {
3564 + $booking->adminnotes = $this->get('admin_notes', '');
3565 + }
3566 + $booking->lang = VikBooking::guessBookingLangFromCountry($country_code);
3567 + $booking->country = $country_code;
3568 + $booking->tot_taxes = $set_taxes ?: null;
3569 + $booking->tot_city_taxes = $set_city_tax ?: ($this->get('tot_city_taxes') ? ((float) $this->get('tot_city_taxes')) : null);
3570 + $booking->tot_fees = $set_fees ?: ($this->get('tot_fees') ? ((float) $this->get('tot_fees')) : null);
3571 + $booking->tot_damage_dep = $this->get('tot_damage_dep') ? ((float) $this->get('tot_damage_dep')) : null;
3572 + $booking->phone = $phone_number;
3573 + $booking->phone_alias = $phone_alias ?: null;
3574 + $booking->cmms = $this->get('cmms') ? ((float) $this->get('cmms')) : null;
3575 + $booking->closure = ($status == 'standby' ? 0 : ($set_closed || $units_closed ? 1 : 0));
3576 + $booking->payable = $this->get('payable') ? ((float) $this->get('payable')) : null;
3577 + $booking->refund = $this->get('refund') ? ((float) $this->get('refund')) : null;
3578 + $booking->type = $this->get('type') ? ((string) $this->get('type')) : null;
3579 + $booking->ota_type_data = $this->get('ota_type_data') && !is_scalar($this->get('ota_type_data')) ? json_encode($this->get('ota_type_data')) : null;
3580 + $booking->split_stay = !empty($split_stay_data) ? 1 : 0;
3581 + $booking->canc_fee = $this->get('canc_fee') ? ((float) $this->get('canc_fee')) : null;
3582 + $booking->idquote = $this->get('idquote') ? ((int) $this->get('idquote')) : null;
3583 +
3584 + // process reservation
3585 + if ($status == 'confirmed') {
3586 + // occupy room records first, when status is confirmed
3587 + $insertedbusy = [];
3588 + if (empty($split_stay_data)) {
3589 + // only when closing other rooms we have an array containing multiple rooms info
3590 + foreach ($rooms_pool as $index => $nowroom) {
3591 + // determine the number of records to occupy
3592 + $nowforend = $set_closed ? $nowroom['units'] : $forend;
3593 + // determine the timestamps to occupy by supporting room-level stay dates
3594 + $room_checkin_ts = $roomsData[$index]['checkin'] ?? $checkin_ts;
3595 + $room_checkout_ts = $roomsData[$index]['checkout'] ?? $checkout_ts;
3596 + $room_realback_ts = $roomsData[$index]['realback'] ?? $roomsData[$index]['checkout'] ?? $realback_ts;
3597 + // occupy records
3598 + for ($b = 1; $b <= $nowforend; $b++) {
3599 + $busy_record = new stdClass;
3600 + $busy_record->idroom = (int) $nowroom['id'];
3601 + $busy_record->checkin = (int) $room_checkin_ts;
3602 + $busy_record->checkout = (int) $room_checkout_ts;
3603 + $busy_record->realback = (int) $room_realback_ts;
3604 +
3605 + // store busy record
3606 + $dbo->insertObject('#__vikbooking_busy', $busy_record, 'id');
3607 +
3608 + if (isset($busy_record->id)) {
3609 + $insertedbusy[] = $busy_record->id;
3610 + }
3611 + }
3612 + }
3613 + } else {
3614 + // for split stay bookings we occupy the rooms on the individual stay dates
3615 + foreach ($split_stay_data as $split_stay) {
3616 + $busy_record = new stdClass;
3617 + $busy_record->idroom = (int) $split_stay['idroom'];
3618 + $busy_record->checkin = (int) $split_stay['checkin_ts'];
3619 + $busy_record->checkout = (int) $split_stay['checkout_ts'];
3620 + $busy_record->realback = (int) $split_stay['realback_ts'];
3621 +
3622 + // store busy record
3623 + $dbo->insertObject('#__vikbooking_busy', $busy_record, 'id');
3624 +
3625 + if (isset($busy_record->id)) {
3626 + $insertedbusy[] = $busy_record->id;
3627 + }
3628 + }
3629 + }
3630 +
3631 + if (!$insertedbusy) {
3632 + $this->setError('No records were occupied');
3633 + return false;
3634 + }
3635 + }
3636 +
3637 + if ($bookingId) {
3638 + // update booking record
3639 + $dbo->updateObject('#__vikbooking_orders', $booking, 'id');
3640 + } else {
3641 + // store booking record
3642 + $dbo->insertObject('#__vikbooking_orders', $booking, 'id');
3643 + }
3644 +
3645 + if (empty($booking->id)) {
3646 + $this->setError('Could not save reservation record.');
3647 + return false;
3648 + }
3649 +
3650 + // get the newly generated (or just updated) booking ID
3651 + $newoid = $booking->id;
3652 +
3653 + // set the new booking ID
3654 + $this->setNewBookingID($newoid);
3655 +
3656 + if (!empty($split_stay_data)) {
3657 + // save transient on db for split stay information
3658 + VBOFactory::getConfig()->set('split_stay_' . $newoid, json_encode($split_stay_data));
3659 + }
3660 +
3661 + if ($status == 'confirmed') {
3662 + // check if some of the rooms booked have shared calendars
3663 + VikBooking::updateSharedCalendars($newoid, [$room['id']], $checkin_ts, $checkout_ts);
3664 +
3665 + if (!$bookingId) {
3666 + // generate and set confirmation number for the newly created confirmed reservation
3667 + $confirmnumber = VikBooking::generateConfirmNumber($newoid, true);
3668 + }
3669 +
3670 + // store busy record-booking relations
3671 + foreach ($insertedbusy as $lid) {
3672 + $obusy_record = new stdClass;
3673 + $obusy_record->idorder = (int) $newoid;
3674 + $obusy_record->idbusy = (int) $lid;
3675 +
3676 + // store busy relation record
3677 + $dbo->insertObject('#__vikbooking_ordersbusy', $obusy_record, 'id');
3678 + }
3679 + }
3680 +
3681 + // store room booking records
3682 + $room_indexes_usemap = [];
3683 + foreach ($rooms_pool as $rind => $nowroom) {
3684 + for ($r = 1; $r <= $or_forend; $r++) {
3685 + // determine room-level stay dates
3686 + $room_stay_checkin = $nowroom['checkin_ts'] ?? $roomsData[$rind]['checkin'] ?? $checkin_ts;
3687 + $room_stay_checkout = $nowroom['checkout_ts'] ?? $roomsData[$rind]['checkout'] ?? $checkout_ts;
3688 + $room_stay_realback = $roomsData[$rind]['realback'] ?? $roomsData[$rind]['checkout'] ?? $realback_ts;
3689 +
3690 + // check booking status for sub-units
3691 + if ($status == 'confirmed') {
3692 + // assign room specific unit
3693 + $info_room_avail = [
3694 + 'id' => $newoid,
3695 + 'checkin' => $nowroom['checkin_ts'] ?? $roomsData[$rind]['checkin'] ?? $checkin_ts,
3696 + 'checkout' => $nowroom['checkout_ts'] ?? $roomsData[$rind]['checkout'] ?? $checkout_ts,
3697 + ];
3698 + $room_indexes = $set_room_indexes === true ? VikBooking::getRoomUnitNumsAvailable($info_room_avail, $nowroom['id']) : [];
3699 + $use_ind_key = 0;
3700 + if ($room_indexes) {
3701 + if (!array_key_exists($nowroom['id'], $room_indexes_usemap)) {
3702 + $room_indexes_usemap[$nowroom['id']] = $use_ind_key;
3703 + } else {
3704 + $use_ind_key = $room_indexes_usemap[$nowroom['id']];
3705 + }
3706 + }
3707 + }
3708 +
3709 + // gather room-level information
3710 + $cust_cost = (float) ($roomsData[$rind]['cust_cost'] ?? 0);
3711 + $room_cost = (float) ($roomsData[$rind]['room_cost'] ?? 0);
3712 + $id_price = (int) ($roomsData[$rind]['id_price'] ?? 0);
3713 + $id_tax = (int) ($roomsData[$rind]['id_tax'] ?? 0);
3714 + $id_tariff = $this->loadTariffID($roomsData[$rind]);
3715 + $cust_indexes = (array) ($roomsData[$rind]['cust_indexes'] ?? []);
3716 + $totalpnight = ($roomsData[$rind]['total_or_pnight'] ?? null) ?: 'total';
3717 + if ($cust_cost > 0.00 && !$set_closed && $totalpnight == 'pnight') {
3718 + // custom rate modifier per night
3719 + $cust_cost = $cust_cost * $daysdiff;
3720 + }
3721 + $cust_cpolicy_id = (int) ($roomsData[$rind]['cust_cpolicy_id'] ?? 0);
3722 +
3723 + // room custom cost
3724 + $or_cust_cost = $cust_cost > 0.00 ? $cust_cost : 0;
3725 + $or_cust_cost = $or_forend > 1 && $or_cust_cost > 0 ? round(($or_cust_cost / $or_forend), 2) : $or_cust_cost;
3726 + // room cost from website rate plan is always based on one room
3727 + $or_room_cost = $room_cost > 0.00 && !$or_cust_cost ? $room_cost : 0;
3728 + if (!empty($split_stay_data) && $cust_cost > 0) {
3729 + // set the average cost per room in case of split stay
3730 + $cost_per_room = ($cust_cost / count($split_stay_data));
3731 + $or_cust_cost = round($cost_per_room, 2);
3732 + if (isset($split_stay_data[$rind]['nights'])) {
3733 + // count the average cost per room depending on the number of nights of stay
3734 + $cost_per_room = $cust_cost / $daysdiff * $split_stay_data[$rind]['nights'];
3735 + $or_cust_cost = round($cost_per_room, 2);
3736 + }
3737 + }
3738 +
3739 + // room guests
3740 + $room_adults = (int) ($roomsData[$rind]['adults'] ?? (isset($adults_map[$r]) && empty($split_stay_data) ? $adults_map[$r] : $adults));
3741 + $room_children = (int) ($roomsData[$rind]['children'] ?? (isset($children_map[$r]) && empty($split_stay_data) ? $children_map[$r] : $children));
3742 + $use_children_age = (array) ($roomsData[$rind]['children_age'] ?? $children_age);
3743 +
3744 + // number of pets per room
3745 + $room_pets = (int) ($roomsData[$rind]['pets'] ?? 0);
3746 +
3747 + // attempt to gather the children age for this room
3748 + $room_children_age = null;
3749 + if ($room_children && $use_children_age) {
3750 + $children_age_pool = [];
3751 + for ($ic = 0; $ic < $room_children; $ic++) {
3752 + if (!$use_children_age) {
3753 + $children_age_pool[] = 0;
3754 + continue;
3755 + }
3756 + // shorten the list and push the current child age
3757 + $current_child_age = array_shift($use_children_age);
3758 + $children_age_pool[] = (int) $current_child_age;
3759 + }
3760 + $room_children_age = json_encode(['age' => $children_age_pool]);
3761 + }
3762 +
3763 + // determine room index to use, if any
3764 + $roomindex = null;
3765 + if ($status == 'confirmed') {
3766 + if ($cust_indexes[$use_ind_key] ?? null) {
3767 + // use a custom room index
3768 + $roomindex = (int) $cust_indexes[$use_ind_key];
3769 + } elseif ($room_indexes) {
3770 + $roomindex = (int) $room_indexes[$use_ind_key];
3771 + }
3772 + }
3773 +
3774 + // handle room record options/extras
3775 + $roomOptStr = null;
3776 + if ($roomsData[$rind]['options'] ?? null) {
3777 + $roomOptData = array_values(array_filter((array) $roomsData[$rind]['options'], function($roomOpt) {
3778 + return is_array($roomOpt) && !empty($roomOpt['id']) && (!empty($roomOpt['quantity']) || !empty($roomOpt['age_intervals']));
3779 + }));
3780 + // normalize values
3781 + $roomOptData = array_map(function($roomOpt) {
3782 + // base option string with ID and quantity
3783 + $baseOptStr = sprintf('%d:%d', (int) $roomOpt['id'], (int) (($roomOpt['quantity'] ?? 0) ?: 1));
3784 + if (!empty($roomOpt['age_intervals'])) {
3785 + $ageData = [];
3786 + foreach ((array) $roomOpt['age_intervals'] as $ageIntervalIndex) {
3787 + $ageData[] = $baseOptStr . '-' . $ageIntervalIndex;
3788 + }
3789 + $finalOptStr = implode(';', $ageData);
3790 + } else {
3791 + $finalOptStr = $baseOptStr;
3792 + }
3793 + return $finalOptStr;
3794 + }, $roomOptData);
3795 + if ($roomOptData) {
3796 + // obtain the value to save
3797 + $roomOptStr = implode(';', $roomOptData) . ';';
3798 + }
3799 + }
3800 +
3801 + // handle room record extra costs
3802 + $roomExtraCosts = array_values(array_filter((array) ($roomsData[$rind]['extras'] ?? []), function($extraCost) {
3803 + return is_array($extraCost) && !empty($extraCost['name']);
3804 + }));
3805 + if ($roomExtraCosts) {
3806 + // normalize values
3807 + $roomExtraCosts = array_map(function($extraCost) {
3808 + return [
3809 + 'name' => $extraCost['name'],
3810 + 'cost' => (float) ($extraCost['cost'] ?? 0),
3811 + 'idtax' => (int) ($extraCost['idtax'] ?? $extraCost['vat'] ?? 0),
3812 + ];
3813 + }, $roomExtraCosts);
3814 + }
3815 +
3816 + // store room record
3817 + $room_record = new stdClass;
3818 + $room_record->idorder = (int) $newoid;
3819 + $room_record->idroom = (int) $nowroom['id'];
3820 + $room_record->adults = $room_adults;
3821 + $room_record->children = $room_children;
3822 + $room_record->pets = $room_pets ?: 0;
3823 + $room_record->idtar = $id_tariff ?: null;
3824 + $room_record->optionals = $roomOptStr ?: null;
3825 + $room_record->childrenage = $room_children_age;
3826 + $room_record->t_first_name = $t_first_name;
3827 + $room_record->t_last_name = $t_last_name;
3828 + $room_record->roomindex = $roomindex;
3829 + $room_record->cust_cost = $cust_cost > 0 ? $or_cust_cost : null;
3830 + $room_record->cust_idiva = $cust_cost > 0 && !empty($id_tax) ? $id_tax : null;
3831 + $room_record->cust_cpolicy_id = $cust_cpolicy_id ?: null;
3832 + $room_record->extracosts = $roomExtraCosts ? json_encode($roomExtraCosts) : null;
3833 + $room_record->room_cost = $or_room_cost > 0 ? $or_room_cost : null;
3834 +
3835 + $dbo->insertObject('#__vikbooking_ordersrooms', $room_record, 'id');
3836 +
3837 + if (empty($room_record->id)) {
3838 + $this->setError('Could not store room reservation record for booking ID ' . $room_record->idorder);
3839 + continue;
3840 + }
3841 +
3842 + if ($status == 'confirmed') {
3843 + // assign room specific unit
3844 + if ($room_indexes) {
3845 + $room_indexes_usemap[$nowroom['id']]++;
3846 + }
3847 + } elseif ($status == 'standby' && empty($split_stay_data)) {
3848 + // lock room for pending status when NO split-stay data by supporting room-level dates
3849 + $lockUntil = VikBooking::getMinutesLock(true);
3850 +
3851 + if ($this->get('lock_until')) {
3852 + // convert expected date-time string into a timestamp for custom lock until date
3853 + $lockUntil = strtotime((string) $this->get('lock_until')) ?: $lockUntil;
3854 + }
3855 +
3856 + if ($lockUntil && $this->get('dont_lock') !== true) {
3857 + // store room lock record
3858 + $tmplock_record = new stdClass;
3859 + $tmplock_record->idroom = (int) $nowroom['id'];
3860 + $tmplock_record->checkin = $room_stay_checkin;
3861 + $tmplock_record->checkout = $room_stay_checkout;
3862 + $tmplock_record->until = $lockUntil;
3863 + $tmplock_record->realback = $room_stay_realback;
3864 + $tmplock_record->idorder = (int) $newoid;
3865 +
3866 + $dbo->insertObject('#__vikbooking_tmplock', $tmplock_record, 'id');
3867 + }
3868 + }
3869 + }
3870 + }
3871 +
3872 + if ($status == 'standby' && !empty($split_stay_data)) {
3873 + // lock split-stay rooms for pending status on proper stay dates
3874 + foreach ($split_stay_data as $split_stay) {
3875 + $tmplock_record = new stdClass;
3876 + $tmplock_record->idroom = (int) $split_stay['idroom'];
3877 + $tmplock_record->checkin = (int) $split_stay['checkin_ts'];
3878 + $tmplock_record->checkout = (int) $split_stay['checkout_ts'];
3879 + $tmplock_record->until = VikBooking::getMinutesLock(true);
3880 + $tmplock_record->realback = (int) $split_stay['realback_ts'];
3881 + $tmplock_record->idorder = (int) $newoid;
3882 +
3883 + $dbo->insertObject('#__vikbooking_tmplock', $tmplock_record, 'id');
3884 + }
3885 + }
3886 +
3887 + // assign booking to customer
3888 + if (!$cpin->getNewCustomerId() && !empty($customer_id)) {
3889 + $cpin->setNewPin($customer_pin);
3890 + $cpin->setNewCustomerId($customer_id);
3891 + }
3892 + $cpin->saveCustomerBooking($newoid);
3893 +
3894 + // handle booking history
3895 + $history_obj = VikBooking::getBookingHistoryInstance($newoid);
3896 +
3897 + $forced_reason = !empty($forced_reason) ? " {$forced_reason}" : $forced_reason;
3898 + $caller_id = $now_user->name ? "({$now_user->name})" : '';
3899 + if ($this->getCaller()) {
3900 + $caller_id = '(' . $this->getCaller() . ')';
3901 + if ($this->getHistoryData()) {
3902 + $history_obj->setExtraData($this->getHistoryData());
3903 + }
3904 + }
3905 + if ($this->get('idquote')) {
3906 + // mention the quote ID in the history description
3907 + $forced_reason .= sprintf(' %s #%d', JText::translate('VBO_BTYPE_QUOTE'), (int) $this->get('idquote'));
3908 + }
3909 +
3910 + // tell whether alterations were detected, in case of booking update
3911 + $alterationsDetected = false;
3912 +
3913 + if ($bookingId) {
3914 + // booking updated event
3915 + $historyPrevBooking = $this->prevBookingRegistry->getData();
3916 + $historyPrevBooking['rooms_info'] = $this->prevBookingRegistry->getRooms();
3917 + $history_obj
3918 + ->setPrevBooking($historyPrevBooking)
3919 + ->store('MB', trim($caller_id . $forced_reason) . ' ' . VikBooking::getLogBookingModification($historyPrevBooking));
3920 +
3921 + // construct a new booking registry, and inject the booking snapshot prior updating
3922 + $currentRegistry = VBOBookingRegistry::getInstance(['id' => $bookingId], [], $historyPrevBooking);
3923 + // detect alterations (include room-level)
3924 + $alterationsDetected = $currentRegistry->detectAlterations(true);
3925 + } else {
3926 + // new booking event
3927 + $history_obj->store('NB', trim($caller_id . $forced_reason));
3928 + }
3929 +
3930 + if ($status == 'confirmed' || ($status == 'standby' && class_exists('VCMRequestAvailability'))) {
3931 + // Invoke Channel Manager
3932 + $vcm_autosync = VikBooking::vcmAutoUpdate();
3933 + if ($vcm_autosync > 0) {
3934 + $vcm_obj = VikBooking::getVcmInvoker();
3935 +
3936 + // tell if sync is actually needed
3937 + $sync_needed = true;
3938 + if ($bookingId && $alterationsDetected === false) {
3939 + // do not waste connections when nothing sensitive was updated
3940 + $sync_needed = false;
3941 + }
3942 +
3943 + if ($sync_needed) {
3944 + if ($bookingId) {
3945 + // sync for an updated booking, after something was truly modified
3946 + $vcm_obj->setOids([$bookingId])->setSyncType('modify')->setOriginalBooking($historyPrevBooking ?? []);
3947 + } else {
3948 + // always sync for a new booking
3949 + $vcm_obj->setOids([$newoid])->setSyncType('new');
3950 + }
3951 +
3952 + // run operation
3953 + $sync_result = $vcm_obj->doSync();
3954 +
3955 + if ($sync_result === false) {
3956 + // set error message, but do NOT return false at this point
3957 + $vcm_err = $vcm_obj->getError();
3958 + $this->setError(JText::translate('VBCHANNELMANAGERRESULTKO') . (!empty($vcm_err) ? ' - ' . $vcm_err : ''));
3959 + }
3960 + }
3961 + } elseif (!$bookingId && is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'synch.vikbooking.php')) {
3962 + // set the necessary action to invoke VCM (only when creating a new booking)
3963 + $vcm_sync_url = 'index.php?option=com_vikbooking&task=invoke_vcm&stype=new&cid[]=' . $newoid . '&returl=' . urlencode('index.php?option=com_vikbooking&task=calendar&cid[]=' . $room['id']);
3964 +
3965 + $this->setChannelManagerAction(JText::translate('VBCHANNELMANAGERINVOKEASK') . ' <button type="button" class="btn btn-primary" onclick="document.location.href=\'' . $vcm_sync_url . '\';">' . JText::translate('VBCHANNELMANAGERSENDRQ') . '</button>');
3966 + }
3967 + }
3968 +
3969 + if (VikBooking::isAdmin()) {
3970 + /**
3971 + * Trigger event to allow third party plugins to intercept the admin new/updated booking event.
3972 + *
3973 + * @since 1.16.8 (J) - 1.6.8 (WP)
3974 + */
3975 + VBOFactory::getPlatform()->getDispatcher()->trigger($bookingId ? 'onAfterUpdateBookingAdmin' : 'onAfterCreateNewBookingAdmin', [$newoid]);
3976 + }
3977 +
3978 + return true;
3979 + }
3980 +
3981 + /**
3982 + * Attempts to find the default payment method to be assigned to a booking.
3983 + *
3984 + * @param bool $auto If true, it was requested to automatically find the best payment method.
3985 + *
3986 + * @return string The payment method string for the reservation "ID=Name", or an empty string.
3987 + *
3988 + * @since 1.17.3 (J) - 1.7.3 (WP)
3989 + */
3990 + protected function getDefaultPaymentMethod($auto = false)
3991 + {
3992 + $dbo = JFactory::getDbo();
3993 +
3994 + $dbo->setQuery(
3995 + $dbo->getQuery(true)
3996 + ->select('*')
3997 + ->from($dbo->qn('#__vikbooking_gpayments'))
3998 + ->order($dbo->qn('published') . ' DESC')
3999 + ->order($dbo->qn('ordering') . ' ASC')
4000 + ->order($dbo->qn('setconfirmed') . ' ASC')
4001 + ->order($dbo->qn('name') . ' ASC')
4002 + );
4003 +
4004 + $methods = $dbo->loadAssocList();
4005 +
4006 + if ($auto) {
4007 + // exclude all the offline or unpublished payment methods
4008 + $methods = array_filter($methods, function($method) {
4009 + return !((bool) intval($method['setconfirmed'])) && (bool) intval($method['published']);
4010 + });
4011 +
4012 + // reset array keys
4013 + $methods = array_values($methods);
4014 + }
4015 +
4016 + if ($methods) {
4017 + // default payment method found
4018 + return sprintf('%s=%s', $methods[0]['id'], $methods[0]['name']);
4019 + }
4020 +
4021 + // nothing was found
4022 + return '';
4023 + }
4024 +
4025 + /**
4026 + * Given a payment method record ID, returns the string
4027 + * in the format for saving it along the reservation.
4028 + *
4029 + * @param int $id The record ID.
4030 + *
4031 + * @return ?string ID=Name calculated value, or null.
4032 + *
4033 + * @since 1.18.15 (J) - 1.8.15 (WP)
4034 + */
4035 + protected function getPaymentMethodValue(int $id)
4036 + {
4037 + $record = VBOMvcModel::getInstance('payment')->getItem($id);
4038 +
4039 + if ($record) {
4040 + return sprintf('%s=%s', $record->id, $record->name);
4041 + }
4042 +
4043 + return null;
4044 + }
1174 4045 }