PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.9
VikBooking Hotel Booking Engine & PMS v1.8.9
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
vikbooking / admin / helpers / src / model / reservation.php

reservation.php in VikBooking Hotel Booking Engine & PMS 1.8.9, at admin/helpers/src/model/reservation.php

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