PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.8
VikBooking Hotel Booking Engine & PMS v1.8.8
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.8, at admin/helpers/src/model/reservation.php

3,629 lines 148.5 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
2601 // determine availability
2602 $rooms_available = true;
2603
2604 // check if we are dealing with split-stay data or regular booking values
2605 if (empty($split_stay_data)) {
2606 // make sure the rooms are available
2607 if ($this->isMultiRoom() && !$set_closed) {
2608 // multi-room booking context with support for stay dates at room-level
2609 $roomBookings = $this->getRooms();
2610 $roomStayData = [];
2611 $roomUnitsUse = [];
2612 $roomNamesMap = [];
2613 foreach ($rooms_pool as $index => $room) {
2614 $roomUnitsUse[$room['id']] = ($roomUnitsUse[$room['id']] ?? 0) + 1;
2615 $roomNamesMap[$room['id']] = $room['name'] ?? $room['id'];
2616 $roomStayData[] = [
2617 'idroom' => $room['id'],
2618 'units' => ($room['units'] ?? 1),
2619 'checkin' => $roomBookings[$index]['checkin'] ?? $default_checkin,
2620 'checkout' => $roomBookings[$index]['checkout'] ?? $default_checkout,
2621 ];
2622 }
2623 foreach ($roomStayData as $roomStay) {
2624 // check the remaining availability for the number of room units booked
2625 $unitsBooked = $roomUnitsUse[$roomStay['idroom']];
2626 $check_units = $roomStay['units'] - $unitsBooked + 1;
2627 // check if the room is available
2628 $rooms_available = VikBooking::roomBookable($roomStay['idroom'], $check_units, $roomStay['checkin'], $roomStay['checkout']);
2629 if (!$rooms_available) {
2630 // set an error and abort
2631 $this->setError(sprintf(
2632 'Room "%s" is not available from %s to %s. Looking for %d free unit(s) over a total of %d.',
2633 $roomNamesMap[$roomStay['idroom']],
2634 date('Y-m-d H:i', $roomStay['checkin']),
2635 date('Y-m-d H:i', $roomStay['checkout']),
2636 $unitsBooked,
2637 $roomStay['units']
2638 ));
2639 break;
2640 }
2641 }
2642 } else {
2643 // single-room booking context
2644 $check_units = $rooms_pool[0]['units'] ?? 1;
2645 if ($num_rooms > 1 && $num_rooms <= $check_units && !$set_closed) {
2646 // only when non closing the room we check the availability for the units requested for booking
2647 $check_units = $check_units - $num_rooms + 1;
2648 }
2649 // check if the room is available
2650 $rooms_available = VikBooking::roomBookable($rooms_pool[0]['id'], $check_units, $default_checkin, $default_checkout);
2651 }
2652 } else {
2653 $all_rooms = $this->loadAllRoomsData();
2654 // make sure the rooms for the split stay are available
2655 foreach ($split_stay_data as $split_stay) {
2656 if (!isset($all_rooms[$split_stay['idroom']])) {
2657 $rooms_available = false;
2658 break;
2659 }
2660 $rooms_available = $rooms_available && VikBooking::roomBookable($split_stay['idroom'], $all_rooms[$split_stay['idroom']]['units'], $split_stay['checkin_ts'], $split_stay['checkout_ts']);
2661 }
2662 }
2663
2664 return $rooms_available;
2665 }
2666
2667 /**
2668 * In case the reservation is forced or is a closure, we detect the
2669 * forced reason to eventually attach it to the booking history.
2670 *
2671 * @param bool $rooms_available Whether the rooms are available.
2672 *
2673 * @return void
2674 */
2675 protected function detectForcedReason($rooms_available = true)
2676 {
2677 $split_stay_data = $this->get('split_stay', []);
2678 $force_booking = $this->get('force_booking', 0);
2679 $set_closed = $this->get('set_closed', 0);
2680
2681 $forced_reason = $this->get('forced_reason', '');
2682
2683 if (empty($split_stay_data)) {
2684 // eventually build string for the description of the history event
2685 if (($force_booking || $set_closed) && !$rooms_available) {
2686 $forced_reason = JText::translate('VBO_FORCED_BOOKDATES');
2687 }
2688 } else {
2689 $all_rooms = $this->loadAllRoomsData();
2690 // set "split stay" as the description of the history event
2691 $forced_reason = JText::translate('VBO_SPLIT_STAY') . "\n";
2692 foreach ($split_stay_data as $sps_k => $split_stay) {
2693 // describe the split stay for each room
2694 if (!isset($all_rooms[$split_stay['idroom']])) {
2695 continue;
2696 }
2697 $room_stay_nights = $split_stay['nights'];
2698 $forced_reason .= $all_rooms[$split_stay['idroom']]['name'] . ': ' . $room_stay_nights . ' ' . ($room_stay_nights > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY')) . ', ';
2699 $forced_reason .= $split_stay['checkin'] . ' - ' . $split_stay['checkout'] . "\n";
2700 }
2701 $forced_reason = rtrim($forced_reason, "\n");
2702 }
2703
2704 $this->set('forced_reason', $forced_reason);
2705 }
2706
2707 /**
2708 * Stores the customer information to a new or existing record.
2709 * In case of success, the customer ID property is updated.
2710 * The customer shall be stored before the reservation records.
2711 *
2712 * @return bool
2713 */
2714 protected function storeCustomer()
2715 {
2716 $dbo = JFactory::getDbo();
2717
2718 $inj_customer = $this->getCustomer();
2719 $first_name = !empty($inj_customer['first_name']) ? $inj_customer['first_name'] : '';
2720 $last_name = !empty($inj_customer['last_name']) ? $inj_customer['last_name'] : '';
2721 $custdata = !empty($inj_customer['data']) ? $inj_customer['data'] : '';
2722 $email = !empty($inj_customer['email']) ? $inj_customer['email'] : '';
2723 $country = !empty($inj_customer['country']) ? $inj_customer['country'] : '';
2724 $phone = !empty($inj_customer['phone']) ? $inj_customer['phone'] : '';
2725 $gender = !empty($inj_customer['gender']) ? $inj_customer['gender'] : '';
2726
2727 // custom fields
2728 $q = "SELECT * FROM `#__vikbooking_custfields` ORDER BY `ordering` ASC;";
2729 $dbo->setQuery($q);
2730 $all_cfields = $dbo->loadAssocList();
2731
2732 $customer_cfields = [];
2733 $customer_extrainfo = [];
2734 $custdata_parts = explode("\n", $custdata);
2735 foreach ($custdata_parts as $cdataline) {
2736 if (!strlen(trim($cdataline))) {
2737 continue;
2738 }
2739 $cdata_parts = explode(':', $cdataline);
2740 if (count($cdata_parts) < 2 || !strlen(trim($cdata_parts[0])) || !strlen(trim($cdata_parts[1]))) {
2741 continue;
2742 }
2743 foreach ($all_cfields as $cf) {
2744 $needle = JText::translate($cf['name']);
2745 if (!empty($needle) && strpos($cdata_parts[0], $needle) !== false && !array_key_exists($cf['id'], $customer_cfields) && $cf['type'] != 'country') {
2746 $user_input_val = trim($cdata_parts[1]);
2747 $customer_cfields[$cf['id']] = $user_input_val;
2748 if (!empty($cf['flag'])) {
2749 $customer_extrainfo[$cf['flag']] = $user_input_val;
2750 } elseif ($cf['type'] == 'state') {
2751 $customer_extrainfo['state'] = $user_input_val;
2752 }
2753 break;
2754 }
2755 }
2756 }
2757
2758 if (!empty($gender) && in_array(strtoupper((string) $gender), ['M', 'F'])) {
2759 // inject the customer gender value
2760 $customer_extrainfo['gender'] = strtoupper($gender);
2761 }
2762
2763 $cpin = VikBooking::getCPinInstance();
2764 $cpin->is_admin = true;
2765 $cpin->setCustomerExtraInfo($customer_extrainfo);
2766 $cpin->saveCustomerDetails($first_name, $last_name, $email, $phone, $country, $customer_cfields);
2767
2768 $customer_id = $cpin->getNewCustomerId();
2769 if (!$customer_id) {
2770 return false;
2771 }
2772
2773 $inj_customer['id'] = $customer_id;
2774 $this->setCustomer($inj_customer);
2775
2776 return true;
2777 }
2778
2779 /**
2780 * Returns the calculated total booking amount and total taxes.
2781 * Sets the necessary properties with the calculated amounts.
2782 *
2783 * @return array list of booking total amount, taxes and fees.
2784 */
2785 protected function calculateTotal()
2786 {
2787 $dbo = JFactory::getDbo();
2788
2789 // the values to calculate
2790 $set_total = 0;
2791 $set_taxes = 0;
2792 $set_city_tax = 0;
2793 $set_fees = 0;
2794
2795 // access all rooms booking data
2796 $roomsData = $this->getRooms();
2797
2798 if (!$roomsData) {
2799 return [$set_total, $set_taxes];
2800 }
2801
2802 // get booking values
2803 $set_closed = $this->get('set_closed', 0);
2804 $daysdiff = (int) $this->get('nights', 1);
2805 $num_rooms = (int) $this->get('num_rooms', 1);
2806 $split_stay_data = $this->get('split_stay', []);
2807
2808 // iterate all booking rooms data
2809 foreach ($roomsData as $index => $roomData) {
2810 // gather booking room values
2811 $totalpnight = ($roomData['total_or_pnight'] ?? '') ?: 'total';
2812 $cust_cost = (float) ($roomData['cust_cost'] ?? 0);
2813 $room_cost = (float) ($roomData['room_cost'] ?? 0);
2814 $id_price = (int) ($roomData['id_price'] ?? 0);
2815 $id_tax = (int) ($roomData['id_tax'] ?? 0);
2816 $guess_tax = $roomData['guess_tax'] ?? false;
2817
2818 // access room details
2819 $roomDetails = $this->getRoomDetails($roomData['id'] ?? 0);
2820
2821 // check for stay dates at room level
2822 $roomNightsStay = ($roomData['nights'] ?? 0) ?: $daysdiff;
2823
2824 // calculate room totals
2825 $roomTotalValue = 0;
2826 $roomTotalTax = 0;
2827 $roomTotalCityTax = 0;
2828 $roomTotalFees = 0;
2829
2830 if ($cust_cost > 0 && !$set_closed) {
2831 // custom cost can be per night
2832 if ($totalpnight == 'pnight') {
2833 $cust_cost = $cust_cost * $roomNightsStay;
2834 }
2835 $roomTotalValue = $cust_cost;
2836
2837 if (!$id_tax && $guess_tax) {
2838 // try to guess the tax rate
2839 $dbo->setQuery(
2840 $dbo->getQuery(true)
2841 ->select($dbo->qn('id'))
2842 ->from($dbo->qn('#__vikbooking_iva'))
2843 ->order($dbo->qn('aliq') . ' ASC'),
2844 0, 1);
2845 $guessed_id_tax = $dbo->loadResult();
2846 if ($guessed_id_tax) {
2847 // update the id_tax values
2848 $id_tax = $guessed_id_tax;
2849 $roomData['id_tax'] = $guessed_id_tax;
2850 $this->setRoom($roomData, $index);
2851 }
2852 }
2853
2854 // apply taxes, if necessary
2855 if ($id_tax) {
2856 $taxdata = VBOTaxonomySummary::getTaxRateRecord((int) $id_tax);
2857 if (!empty($taxdata['aliq'])) {
2858 $aliq = $taxdata['aliq'];
2859 if (!VikBooking::ivaInclusa()) {
2860 // add tax to the total amount
2861 $subt = 100 + (float)$aliq;
2862 $roomTotalValue = ($roomTotalValue * $subt / 100);
2863 /**
2864 * Tax Cap implementation for prices tax excluded (most common).
2865 *
2866 * @since 1.12 (J) - 1.2 (WP)
2867 */
2868 if ($taxdata['taxcap'] > 0 && ($roomTotalValue - $cust_cost) > $taxdata['taxcap']) {
2869 $roomTotalValue = ($cust_cost + $taxdata['taxcap']);
2870 }
2871 // calculate tax
2872 $roomTotalTax = $roomTotalValue - $cust_cost;
2873 } else {
2874 // calculate tax
2875 $cost_minus_tax = VikBooking::sayPackageMinusIva($cust_cost, $id_tax);
2876 $roomTotalTax += ($cust_cost - $cost_minus_tax);
2877 }
2878 }
2879 }
2880 } elseif (!empty($id_price) && $room_cost > 0 && !$set_closed && empty($split_stay_data)) {
2881 // one website rate plan was selected, so we calculate total and taxes
2882 $roomTotalValue = $room_cost;
2883
2884 // find tax rate assigned to this rate plan
2885 $taxdata = VBOTaxonomySummary::getTaxRecordFromRatePlan($id_price);
2886 if (!empty($taxdata['aliq'])) {
2887 $aliq = $taxdata['aliq'];
2888 if (!VikBooking::ivaInclusa()) {
2889 // add tax to the total amount
2890 $subt = 100 + (float)$aliq;
2891 $roomTotalValue = ($roomTotalValue * $subt / 100);
2892 /**
2893 * Tax Cap implementation for prices tax excluded (most common).
2894 *
2895 * @since 1.12 (J) - 1.2 (WP)
2896 */
2897 if ($taxdata['taxcap'] > 0 && ($roomTotalValue - $room_cost) > $taxdata['taxcap']) {
2898 $roomTotalValue = ($room_cost + $taxdata['taxcap']);
2899 }
2900 // calculate tax
2901 $roomTotalTax = $roomTotalValue - $room_cost;
2902 } else {
2903 // calculate tax
2904 $cost_minus_tax = VikBooking::sayPackageMinusIva($room_cost, $taxdata['idiva']);
2905 $roomTotalTax += ($room_cost - $cost_minus_tax);
2906 }
2907 }
2908
2909 // total and taxes should be multiplied by the number of rooms booked when using a website rate plan
2910 if ($set_closed) {
2911 $roomTotalValue *= $roomDetails['units'];
2912 $roomTotalTax *= $roomDetails['units'];
2913 } elseif ($num_rooms > 1 && $num_rooms <= $roomDetails['units']) {
2914 $roomTotalValue *= $num_rooms;
2915 $roomTotalTax *= $num_rooms;
2916 }
2917 }
2918
2919 // check for options/extras at room level
2920 if ($roomData['options'] ?? null) {
2921 // load all the eligible options for this room party
2922 $eligibleOptions = VBORoomHelper::getInstance()->getEligibleOptions($roomData['id'], [
2923 'checkin' => date('Y-m-d', ($roomData['checkin'] ?? null) ?: $this->get('checkin', 0)),
2924 'checkout' => date('Y-m-d', ($roomData['checkout'] ?? null) ?: $this->get('checkout', 0)),
2925 'adults' => $roomData['adults'] ?? $this->get('adults', 1),
2926 'children' => $roomData['children'] ?? $this->get('children', 0),
2927 'rate_id' => $roomData['id_price'] ?? null,
2928 ]);
2929 // obtain a list of eligible option IDs
2930 $eligibleOptIds = array_column($eligibleOptions, 'id');
2931 // get all eligible room options/extras that were set for booking
2932 $roomOptData = array_values(array_filter((array) $roomData['options'], function($roomOpt) use ($eligibleOptIds) {
2933 if (!is_array($roomOpt) || empty($roomOpt['id'])) {
2934 return false;
2935 }
2936 return in_array($roomOpt['id'], $eligibleOptIds) && (!empty($roomOpt['quantity']) || !empty($roomOpt['age_intervals']));
2937 }));
2938 if ($roomOptData) {
2939 // turn all eligible options into an associative list
2940 $eligibleOptions = array_combine(array_column($eligibleOptions, 'id'), array_values($eligibleOptions));
2941 // scan all room options to increase totals
2942 foreach ($roomOptData as $optData) {
2943 // obtain room option computed data
2944 $computedData = $eligibleOptions[$optData['id']] ?? [];
2945 // calculate option cost
2946 $roomOptCost = 0;
2947 if (!empty($optData['age_intervals'])) {
2948 // children age interval option
2949 foreach ((array) $optData['age_intervals'] as $childIndex => $ageIntervalIndex) {
2950 // child number is 0-based
2951 $childNumber = $childIndex + 1;
2952 // age interval index is 1-based
2953 $ageIntervalNumber = $ageIntervalIndex - 1;
2954 // check if a room cost is known
2955 $roomOptCost += (float) ($computedData['_computed_children_costs'][$childNumber][$ageIntervalNumber] ?? 0);
2956 }
2957 } else {
2958 // regular option
2959 $roomOptCost = floatval($computedData['_computed_cost'] ?? 0) * floatval($optData['quantity'] ?? 1);
2960 }
2961 // calculate gross and net costs
2962 $roomOptGross = VikBooking::sayOptionalsPlusIva($roomOptCost, $computedData['idiva'] ?? 0);
2963 $roomOptNet = VikBooking::sayOptionalsMinusIva($roomOptCost, $computedData['idiva'] ?? 0);
2964 // increase total value
2965 $roomTotalValue += $roomOptGross;
2966 // increase total tax
2967 $roomTotalTax += ($roomOptGross - $roomOptNet);
2968 if ($computedData['is_citytax'] ?? null) {
2969 // increase total city tax
2970 $roomTotalCityTax += $roomOptNet;
2971 } elseif ($computedData['is_fee'] ?? null) {
2972 // increase total fees
2973 $roomTotalFees += $roomOptNet;
2974 }
2975 }
2976 }
2977 }
2978
2979 // check for custom extra services at room level
2980 if ($roomData['extras'] ?? null) {
2981 $roomExtraCosts = array_values(array_filter((array) ($roomData['extras'] ?? []), function($extraCost) {
2982 return is_array($extraCost) && !empty($extraCost['name']);
2983 }));
2984 if ($roomExtraCosts) {
2985 // normalize values
2986 $roomExtraCosts = array_map(function($extraCost) {
2987 return [
2988 'name' => $extraCost['name'],
2989 'cost' => (float) ($extraCost['cost'] ?? 0),
2990 'idtax' => (int) ($extraCost['idtax'] ?? $extraCost['vat'] ?? 0),
2991 ];
2992 }, $roomExtraCosts);
2993 // scan all room extra services to increase totals
2994 foreach ($roomExtraCosts as $roomExtraCost) {
2995 $ecplustax = !empty($roomExtraCost['idtax']) ? VikBooking::sayOptionalsPlusIva($roomExtraCost['cost'], $roomExtraCost['idtax']) : $roomExtraCost['cost'];
2996 $ecminustax = !empty($roomExtraCost['idtax']) ? VikBooking::sayOptionalsMinusIva($roomExtraCost['cost'], $roomExtraCost['idtax']) : $roomExtraCost['cost'];
2997 // increase total value
2998 $roomTotalValue += $ecplustax;
2999 // increase total tax
3000 $roomTotalTax += ($ecplustax - $ecminustax);
3001 }
3002 }
3003 }
3004
3005 // increase global totals
3006 $set_total += $roomTotalValue;
3007 $set_taxes += $roomTotalTax;
3008 $set_city_tax += $roomTotalCityTax;
3009 $set_fees += $roomTotalFees;
3010 }
3011
3012 // set values
3013 $this->set('_total', $set_total);
3014 $this->set('_total_tax', $set_taxes);
3015 $this->set('_total_city_tax', $set_city_tax);
3016 $this->set('_total_fees', $set_fees);
3017
3018 // return the list of calculated values
3019 return [
3020 $set_total,
3021 $set_taxes,
3022 $set_city_tax,
3023 $set_fees,
3024 ];
3025 }
3026
3027 /**
3028 * Stores the booking and room-booking records.
3029 * If no errors, the newly generated booking id is set.
3030 *
3031 * @param array $rooms_pool list of rooms involved.
3032 *
3033 * @return bool
3034 */
3035 protected function storeReservationRecords(array $rooms_pool)
3036 {
3037 $dbo = JFactory::getDbo();
3038
3039 // access properties
3040 $set_closed = $this->get('set_closed', 0);
3041 $units_closed = $this->get('units_closed', 0);
3042 $daysdiff = (int) $this->get('nights', 1);
3043 $num_rooms = (int) $this->get('num_rooms', 1);
3044 $adults = (int) $this->get('adults', 1);
3045 $children = (int) $this->get('children', 0);
3046 $children_age = (array) $this->get('children_age', []);
3047 $status = $this->get('status', 'confirmed');
3048
3049 $split_stay_data = $this->get('split_stay', []);
3050 $room = $this->getRoomDetails();
3051 if (!$room || !$rooms_pool) {
3052 return false;
3053 }
3054
3055 // access all rooms data
3056 $roomsData = $this->getRooms();
3057
3058 // get current Joomla/WordPress User ID
3059 $now_user = JFactory::getUser();
3060 $store_ujid = property_exists($now_user, 'id') ? (int)$now_user->id : 0;
3061
3062 // forced booking reason, status validation and additional data
3063 $forced_reason = $this->get('forced_reason', '');
3064 $valid_statuses = ['confirmed', 'standby'];
3065 $status = in_array($status, $valid_statuses) ? $status : 'confirmed';
3066 $paymentmeth = $this->get('id_payment', '');
3067 $auto_paymeth = (bool) $this->get('auto_payment_method', false);
3068 $set_total = (float) $this->get('_total', 0);
3069 $set_taxes = (float) $this->get('_total_tax', 0);
3070 $set_city_tax = (float) $this->get('_total_city_tax', 0);
3071 $set_fees = (float) $this->get('_total_fees', 0);
3072
3073 // booking creation date
3074 $now_ts = time();
3075 if ($created_on = $this->get('created_on')) {
3076 if (is_int($created_on)) {
3077 // timestamp expected
3078 $now_ts = $created_on;
3079 } elseif (preg_match('/^[0-9]{4}\-[0-9]{2}\-[0-9]{2}/', (string) $created_on)) {
3080 // date in military format expected, with or without the time
3081 $now_ts = strtotime($created_on);
3082 }
3083 }
3084
3085 // stay dates
3086 $checkin_ts = $this->get('checkin');
3087 $checkout_ts = $this->get('checkout');
3088 $realback_ts = $this->get('checkout_real', $checkout_ts);
3089
3090 // customer
3091 $cpin = VikBooking::getCPinInstance();
3092 $inj_customer = $this->getCustomer();
3093 $customer_id = ($inj_customer['id'] ?? null) ?: 0;
3094 $customer_pin = ($inj_customer['pin'] ?? null) ?: '';
3095 $t_first_name = ($inj_customer['first_name'] ?? null) ?: '';
3096 $t_last_name = ($inj_customer['last_name'] ?? null) ?: '';
3097 $customer_data = ($inj_customer['data'] ?? null) ?: '';
3098 $customer_email = ($inj_customer['email'] ?? null) ?: '';
3099 $country_code = ($inj_customer['country'] ?? null) ?: '';
3100 $phone_number = ($inj_customer['phone'] ?? null) ?: '';
3101
3102 if ($set_closed) {
3103 // get the possibly custom "customer notes"
3104 $customer_notes = $customer_data;
3105 // overwrite "customer notes" to "Room Closed"
3106 $customer_data = JText::translate('VBDBTEXTROOMCLOSED');
3107 if ($customer_notes != $customer_data) {
3108 // append the custom "custom notes" to the administrator notes instead
3109 $custom_admin_notes = trim($this->get('admin_notes', '') . "\n" . $customer_notes);
3110 $this->set('admin_notes', $custom_admin_notes);
3111 }
3112 }
3113
3114 // check for default customer raw data
3115 if (!$customer_data && $t_first_name) {
3116 // build a default raw data string
3117 $customer_data = "Name: {$t_first_name}\n";
3118 if ($t_last_name) {
3119 $customer_data .= "Last Name: {$t_last_name}\n";
3120 }
3121 if ($customer_email) {
3122 $customer_data .= "eMail: {$customer_email}\n";
3123 }
3124 if ($country_code) {
3125 $customer_data .= "Country: {$country_code}\n";
3126 }
3127 if ($phone_number) {
3128 $customer_data .= "Phone: {$phone_number}\n";
3129 }
3130 $customer_data = rtrim($customer_data, "\n");
3131 }
3132
3133 // ensure the country code is in the right format
3134 if (!empty($country_code)) {
3135 $country_code = $cpin->get3CharCountry($country_code);
3136 }
3137
3138 // ensure we have a valid country ISO code
3139 if (strlen((string) $country_code) > 3) {
3140 // this looks like an unknown country name
3141 $country_code = '';
3142 }
3143
3144 // generate booking SID
3145 $sid = VikBooking::getSecretLink();
3146
3147 // assign room specific unit
3148 $set_room_indexes = !$set_closed ? VikBooking::autoRoomUnit() : false;
3149 $num_rooms = $num_rooms > 0 ? $num_rooms : 1;
3150
3151 // occupancy and loop limits
3152 $forend = 1;
3153 $or_forend = 1;
3154 $adults_map = [];
3155 $children_map = [];
3156 if ($set_closed && empty($split_stay_data)) {
3157 $forend = $room['units'];
3158 } elseif ($num_rooms > 1 && $num_rooms <= $room['units'] && empty($split_stay_data)) {
3159 $forend = $num_rooms;
3160 $or_forend = $num_rooms;
3161 // assign adults/children proportionally
3162 if (($adults + $children) < $num_rooms) {
3163 // the number of guests does not make much sense but we build the maps anyway
3164 for ($r = 1; $r <= $or_forend; $r++) {
3165 $adults_map[$r] = $adults;
3166 $children_map[$r] = $children;
3167 }
3168 } else {
3169 $adults_per_room = floor(($adults / $num_rooms));
3170 $adults_left = ($adults % $num_rooms);
3171 $children_per_room = floor(($children / $num_rooms));
3172 $children_left = ($children % $num_rooms);
3173 for ($r = 1; $r <= $or_forend; $r++) {
3174 $adults_map[$r] = $adults_per_room;
3175 $children_map[$r] = $children_per_room;
3176 if ($r == $or_forend) {
3177 $adults_map[$r] += $adults_left;
3178 $children_map[$r] += $children_left;
3179 }
3180 }
3181 }
3182 }
3183
3184 if ($this->isMultiRoom()) {
3185 // adjust values
3186 $forend = 1;
3187 $or_forend = 1;
3188 }
3189
3190 // count total rooms booked
3191 $totalrooms = (($set_closed && $status == 'confirmed') || $this->isMultiRoom() ? count($rooms_pool) : ($num_rooms > 1 && $num_rooms <= $room['units'] ? $num_rooms : 1));
3192 $totalrooms = !empty($split_stay_data) ? count($split_stay_data) : $totalrooms;
3193
3194 // attempt to get the default payment method
3195 if (!$paymentmeth && ($auto_paymeth || $status == 'standby')) {
3196 // get the default payment method, if any
3197 $paymentmeth = $this->getDefaultPaymentMethod($auto_paymeth);
3198 }
3199
3200 // prepare booking record
3201 $booking = new stdClass;
3202 $booking->custdata = $customer_data;
3203 $booking->ts = $now_ts;
3204 $booking->status = $status;
3205 $booking->days = $daysdiff;
3206 $booking->checkin = $checkin_ts;
3207 $booking->checkout = $checkout_ts;
3208 $booking->custmail = $customer_email;
3209 $booking->sid = $sid;
3210 $booking->idpayment = $paymentmeth;
3211 $booking->ujid = (int)$store_ujid;
3212 $booking->roomsnum = $totalrooms;
3213 $booking->total = $set_total ?: null;
3214 if ($this->get('admin_notes')) {
3215 $booking->adminnotes = $this->get('admin_notes', '');
3216 }
3217 $booking->lang = VikBooking::guessBookingLangFromCountry($country_code);
3218 $booking->country = $country_code;
3219 $booking->tot_taxes = $set_taxes ?: null;
3220 $booking->tot_city_taxes = $set_city_tax ?: ($this->get('tot_city_taxes') ? ((float) $this->get('tot_city_taxes')) : null);
3221 $booking->tot_fees = $set_fees ?: ($this->get('tot_fees') ? ((float) $this->get('tot_fees')) : null);
3222 $booking->tot_damage_dep = $this->get('tot_damage_dep') ? ((float) $this->get('tot_damage_dep')) : null;
3223 $booking->phone = $phone_number;
3224 $booking->cmms = $this->get('cmms') ? ((float) $this->get('cmms')) : null;
3225 $booking->closure = ($status == 'standby' ? 0 : ($set_closed || $units_closed ? 1 : 0));
3226 $booking->payable = $this->get('payable') ? ((float) $this->get('payable')) : null;
3227 $booking->refund = $this->get('refund') ? ((float) $this->get('refund')) : null;
3228 $booking->type = $this->get('type') ? ((string) $this->get('type')) : null;
3229 $booking->ota_type_data = $this->get('ota_type_data') && !is_scalar($this->get('ota_type_data')) ? json_encode($this->get('ota_type_data')) : null;
3230 $booking->split_stay = !empty($split_stay_data) ? 1 : 0;
3231 $booking->canc_fee = $this->get('canc_fee') ? ((float) $this->get('canc_fee')) : null;
3232 $booking->idquote = $this->get('idquote') ? ((int) $this->get('idquote')) : null;
3233
3234 // process reservation
3235 if ($status == 'confirmed') {
3236 // occupy room records first, when status is confirmed
3237 $insertedbusy = [];
3238 if (empty($split_stay_data)) {
3239 // only when closing other rooms we have an array containing multiple rooms info
3240 foreach ($rooms_pool as $index => $nowroom) {
3241 // determine the number of records to occupy
3242 $nowforend = $set_closed ? $nowroom['units'] : $forend;
3243 // determine the timestamps to occupy by supporting room-level stay dates
3244 $room_checkin_ts = $roomsData[$index]['checkin'] ?? $checkin_ts;
3245 $room_checkout_ts = $roomsData[$index]['checkout'] ?? $checkout_ts;
3246 $room_realback_ts = $roomsData[$index]['realback'] ?? $roomsData[$index]['checkout'] ?? $realback_ts;
3247 // occupy records
3248 for ($b = 1; $b <= $nowforend; $b++) {
3249 $busy_record = new stdClass;
3250 $busy_record->idroom = (int) $nowroom['id'];
3251 $busy_record->checkin = (int) $room_checkin_ts;
3252 $busy_record->checkout = (int) $room_checkout_ts;
3253 $busy_record->realback = (int) $room_realback_ts;
3254
3255 // store busy record
3256 $dbo->insertObject('#__vikbooking_busy', $busy_record, 'id');
3257
3258 if (isset($busy_record->id)) {
3259 $insertedbusy[] = $busy_record->id;
3260 }
3261 }
3262 }
3263 } else {
3264 // for split stay bookings we occupy the rooms on the individual stay dates
3265 foreach ($split_stay_data as $split_stay) {
3266 $busy_record = new stdClass;
3267 $busy_record->idroom = (int) $split_stay['idroom'];
3268 $busy_record->checkin = (int) $split_stay['checkin_ts'];
3269 $busy_record->checkout = (int) $split_stay['checkout_ts'];
3270 $busy_record->realback = (int) $split_stay['realback_ts'];
3271
3272 // store busy record
3273 $dbo->insertObject('#__vikbooking_busy', $busy_record, 'id');
3274
3275 if (isset($busy_record->id)) {
3276 $insertedbusy[] = $busy_record->id;
3277 }
3278 }
3279 }
3280
3281 if (!$insertedbusy) {
3282 $this->setError('No records were occupied');
3283 return false;
3284 }
3285 }
3286
3287 // store booking record
3288 $dbo->insertObject('#__vikbooking_orders', $booking, 'id');
3289
3290 if (empty($booking->id)) {
3291 $this->setError('Could not store the reservation record');
3292 return false;
3293 }
3294
3295 // get the newly generated booking ID
3296 $newoid = $booking->id;
3297
3298 // set the new booking ID
3299 $this->setNewBookingID($newoid);
3300
3301 if (!empty($split_stay_data)) {
3302 // save transient on db for split stay information
3303 VBOFactory::getConfig()->set('split_stay_' . $newoid, json_encode($split_stay_data));
3304 }
3305
3306 if ($status == 'confirmed') {
3307 // check if some of the rooms booked have shared calendars
3308 VikBooking::updateSharedCalendars($newoid, [$room['id']], $checkin_ts, $checkout_ts);
3309
3310 // generate and set confirmation number
3311 $confirmnumber = VikBooking::generateConfirmNumber($newoid, true);
3312
3313 // store busy record-booking relations
3314 foreach ($insertedbusy as $lid) {
3315 $obusy_record = new stdClass;
3316 $obusy_record->idorder = (int) $newoid;
3317 $obusy_record->idbusy = (int) $lid;
3318
3319 // store busy relation record
3320 $dbo->insertObject('#__vikbooking_ordersbusy', $obusy_record, 'id');
3321 }
3322 }
3323
3324 // store room booking records
3325 foreach ($rooms_pool as $rind => $nowroom) {
3326 $room_indexes_usemap = [];
3327 for ($r = 1; $r <= $or_forend; $r++) {
3328 // determine room-level stay dates
3329 $room_stay_checkin = $nowroom['checkin_ts'] ?? $roomsData[$rind]['checkin'] ?? $checkin_ts;
3330 $room_stay_checkout = $nowroom['checkout_ts'] ?? $roomsData[$rind]['checkout'] ?? $checkout_ts;
3331 $room_stay_realback = $roomsData[$rind]['realback'] ?? $roomsData[$rind]['checkout'] ?? $realback_ts;
3332
3333 // check booking status for sub-units
3334 if ($status == 'confirmed') {
3335 // assign room specific unit
3336 $info_room_avail = [
3337 'id' => $newoid,
3338 'checkin' => $nowroom['checkin_ts'] ?? $roomsData[$rind]['checkin'] ?? $checkin_ts,
3339 'checkout' => $nowroom['checkout_ts'] ?? $roomsData[$rind]['checkout'] ?? $checkout_ts,
3340 ];
3341 $room_indexes = $set_room_indexes === true ? VikBooking::getRoomUnitNumsAvailable($info_room_avail, $nowroom['id']) : [];
3342 $use_ind_key = 0;
3343 if ($room_indexes) {
3344 if (!array_key_exists($nowroom['id'], $room_indexes_usemap)) {
3345 $room_indexes_usemap[$nowroom['id']] = $use_ind_key;
3346 } else {
3347 $use_ind_key = $room_indexes_usemap[$nowroom['id']];
3348 }
3349 }
3350 }
3351
3352 // gather room-level information
3353 $cust_cost = (float) ($roomsData[$rind]['cust_cost'] ?? 0);
3354 $room_cost = (float) ($roomsData[$rind]['room_cost'] ?? 0);
3355 $id_price = (int) ($roomsData[$rind]['id_price'] ?? 0);
3356 $id_tax = (int) ($roomsData[$rind]['id_tax'] ?? 0);
3357 $id_tariff = $this->loadTariffID($roomsData[$rind]);
3358 $cust_indexes = (array) ($roomsData[$rind]['cust_indexes'] ?? []);
3359 $totalpnight = ($roomsData[$rind]['total_or_pnight'] ?? null) ?: 'total';
3360 if ($cust_cost > 0.00 && !$set_closed && $totalpnight == 'pnight') {
3361 // custom rate modifier per night
3362 $cust_cost = $cust_cost * $daysdiff;
3363 }
3364 $cust_cpolicy_id = (int) ($roomsData[$rind]['cust_cpolicy_id'] ?? 0);
3365
3366 // room custom cost
3367 $or_cust_cost = $cust_cost > 0.00 ? $cust_cost : 0;
3368 $or_cust_cost = $or_forend > 1 && $or_cust_cost > 0 ? round(($or_cust_cost / $or_forend), 2) : $or_cust_cost;
3369 // room cost from website rate plan is always based on one room
3370 $or_room_cost = $room_cost > 0.00 && !$or_cust_cost ? $room_cost : 0;
3371 if (!empty($split_stay_data) && $cust_cost > 0) {
3372 // set the average cost per room in case of split stay
3373 $cost_per_room = ($cust_cost / count($split_stay_data));
3374 $or_cust_cost = round($cost_per_room, 2);
3375 if (isset($split_stay_data[$rind]['nights'])) {
3376 // count the average cost per room depending on the number of nights of stay
3377 $cost_per_room = $cust_cost / $daysdiff * $split_stay_data[$rind]['nights'];
3378 $or_cust_cost = round($cost_per_room, 2);
3379 }
3380 }
3381
3382 // room guests
3383 $room_adults = (int) (isset($adults_map[$r]) && empty($split_stay_data) ? $adults_map[$r] : ($roomsData[$rind]['adults'] ?? $adults));
3384 $room_children = (int) (isset($children_map[$r]) && empty($split_stay_data) ? $children_map[$r] : ($roomsData[$rind]['children'] ?? $children));
3385 $use_children_age = (array) ($roomsData[$rind]['children_age'] ?? $children_age);
3386
3387 // number of pets per room
3388 $room_pets = (int) ($roomsData[$rind]['pets'] ?? 0);
3389
3390 // attempt to gather the children age for this room
3391 $room_children_age = null;
3392 if ($room_children && $use_children_age) {
3393 $children_age_pool = [];
3394 for ($ic = 0; $ic < $room_children; $ic++) {
3395 if (!$use_children_age) {
3396 $children_age_pool[] = 0;
3397 continue;
3398 }
3399 // shorten the list and push the current child age
3400 $current_child_age = array_shift($use_children_age);
3401 $children_age_pool[] = (int) $current_child_age;
3402 }
3403 $room_children_age = json_encode(['age' => $children_age_pool]);
3404 }
3405
3406 // determine room index to use, if any
3407 $roomindex = null;
3408 if ($status == 'confirmed') {
3409 if ($cust_indexes[$use_ind_key] ?? null) {
3410 // use a custom room index
3411 $roomindex = (int) $cust_indexes[$use_ind_key];
3412 } elseif ($room_indexes) {
3413 $roomindex = (int) $room_indexes[$use_ind_key];
3414 }
3415 }
3416
3417 // handle room record options/extras
3418 $roomOptStr = null;
3419 if ($roomsData[$rind]['options'] ?? null) {
3420 $roomOptData = array_values(array_filter((array) $roomsData[$rind]['options'], function($roomOpt) {
3421 return is_array($roomOpt) && !empty($roomOpt['id']) && (!empty($roomOpt['quantity']) || !empty($roomOpt['age_intervals']));
3422 }));
3423 // normalize values
3424 $roomOptData = array_map(function($roomOpt) {
3425 // base option string with ID and quantity
3426 $baseOptStr = sprintf('%d:%d', (int) $roomOpt['id'], (int) (($roomOpt['quantity'] ?? 0) ?: 1));
3427 if (!empty($roomOpt['age_intervals'])) {
3428 $ageData = [];
3429 foreach ((array) $roomOpt['age_intervals'] as $ageIntervalIndex) {
3430 $ageData[] = $baseOptStr . '-' . $ageIntervalIndex;
3431 }
3432 $finalOptStr = implode(';', $ageData);
3433 } else {
3434 $finalOptStr = $baseOptStr;
3435 }
3436 return $finalOptStr;
3437 }, $roomOptData);
3438 if ($roomOptData) {
3439 // obtain the value to save
3440 $roomOptStr = implode(';', $roomOptData) . ';';
3441 }
3442 }
3443
3444 // handle room record extra costs
3445 $roomExtraCosts = array_values(array_filter((array) ($roomsData[$rind]['extras'] ?? []), function($extraCost) {
3446 return is_array($extraCost) && !empty($extraCost['name']);
3447 }));
3448 if ($roomExtraCosts) {
3449 // normalize values
3450 $roomExtraCosts = array_map(function($extraCost) {
3451 return [
3452 'name' => $extraCost['name'],
3453 'cost' => (float) ($extraCost['cost'] ?? 0),
3454 'idtax' => (int) ($extraCost['idtax'] ?? $extraCost['vat'] ?? 0),
3455 ];
3456 }, $roomExtraCosts);
3457 }
3458
3459 // store room record
3460 $room_record = new stdClass;
3461 $room_record->idorder = (int) $newoid;
3462 $room_record->idroom = (int) $nowroom['id'];
3463 $room_record->adults = $room_adults;
3464 $room_record->children = $room_children;
3465 $room_record->pets = $room_pets ?: 0;
3466 $room_record->idtar = $id_tariff ?: null;
3467 $room_record->optionals = $roomOptStr ?: null;
3468 $room_record->childrenage = $room_children_age;
3469 $room_record->t_first_name = $t_first_name;
3470 $room_record->t_last_name = $t_last_name;
3471 $room_record->roomindex = $roomindex;
3472 $room_record->cust_cost = $cust_cost > 0 ? $or_cust_cost : null;
3473 $room_record->cust_idiva = $cust_cost > 0 && !empty($id_tax) ? $id_tax : null;
3474 $room_record->cust_cpolicy_id = $cust_cpolicy_id ?: null;
3475 $room_record->extracosts = $roomExtraCosts ? json_encode($roomExtraCosts) : null;
3476 $room_record->room_cost = $or_room_cost > 0 ? $or_room_cost : null;
3477
3478 $dbo->insertObject('#__vikbooking_ordersrooms', $room_record, 'id');
3479
3480 if (empty($room_record->id)) {
3481 $this->setError('Could not store room reservation record for booking ID ' . $room_record->idorder);
3482 continue;
3483 }
3484
3485 if ($status == 'confirmed') {
3486 // assign room specific unit
3487 if ($room_indexes) {
3488 $room_indexes_usemap[$nowroom['id']]++;
3489 }
3490 } elseif ($status == 'standby' && empty($split_stay_data)) {
3491 // lock room for pending status when NO split-stay data by supporting room-level dates
3492 $lockUntil = VikBooking::getMinutesLock(true);
3493 if ($this->get('lock_until')) {
3494 // convert expected date-time string into a timestamp for custom lock until date
3495 $lockUntil = strtotime((string) $this->get('lock_until')) ?: $lockUntil;
3496 }
3497
3498 // store room lock record
3499 $tmplock_record = new stdClass;
3500 $tmplock_record->idroom = (int) $nowroom['id'];
3501 $tmplock_record->checkin = $room_stay_checkin;
3502 $tmplock_record->checkout = $room_stay_checkout;
3503 $tmplock_record->until = $lockUntil;
3504 $tmplock_record->realback = $room_stay_realback;
3505 $tmplock_record->idorder = (int) $newoid;
3506
3507 $dbo->insertObject('#__vikbooking_tmplock', $tmplock_record, 'id');
3508 }
3509 }
3510 }
3511
3512 if ($status == 'standby' && !empty($split_stay_data)) {
3513 // lock split-stay rooms for pending status on proper stay dates
3514 foreach ($split_stay_data as $split_stay) {
3515 $tmplock_record = new stdClass;
3516 $tmplock_record->idroom = (int) $split_stay['idroom'];
3517 $tmplock_record->checkin = (int) $split_stay['checkin_ts'];
3518 $tmplock_record->checkout = (int) $split_stay['checkout_ts'];
3519 $tmplock_record->until = VikBooking::getMinutesLock(true);
3520 $tmplock_record->realback = (int) $split_stay['realback_ts'];
3521 $tmplock_record->idorder = (int) $newoid;
3522
3523 $dbo->insertObject('#__vikbooking_tmplock', $tmplock_record, 'id');
3524 }
3525 }
3526
3527 // assign booking to customer
3528 if (!$cpin->getNewCustomerId() && !empty($customer_id)) {
3529 $cpin->setNewPin($customer_pin);
3530 $cpin->setNewCustomerId($customer_id);
3531 }
3532 $cpin->saveCustomerBooking($newoid);
3533
3534 // Booking History
3535 $history_obj = VikBooking::getBookingHistoryInstance($newoid);
3536 $forced_reason = !empty($forced_reason) ? " {$forced_reason}" : $forced_reason;
3537 $caller_id = $now_user->name ? "({$now_user->name})" : '';
3538 if ($this->getCaller()) {
3539 $caller_id = '(' . $this->getCaller() . ')';
3540 if ($this->getHistoryData()) {
3541 $history_obj->setExtraData($this->getHistoryData());
3542 }
3543 }
3544 if ($this->get('idquote')) {
3545 // mention the quote ID in the history description
3546 $forced_reason .= sprintf(' %s #%d', JText::translate('VBO_BTYPE_QUOTE'), (int) $this->get('idquote'));
3547 }
3548 $history_obj->store('NB', trim($caller_id . $forced_reason));
3549
3550 if ($status == 'confirmed' || ($status == 'standby' && class_exists('VCMRequestAvailability'))) {
3551 // Invoke Channel Manager
3552 $vcm_autosync = VikBooking::vcmAutoUpdate();
3553 if ($vcm_autosync > 0) {
3554 $vcm_obj = VikBooking::getVcmInvoker();
3555 $vcm_obj->setOids([$newoid])->setSyncType('new');
3556 $sync_result = $vcm_obj->doSync();
3557 if ($sync_result === false) {
3558 // set error message
3559 $vcm_err = $vcm_obj->getError();
3560 $this->setError(JText::translate('VBCHANNELMANAGERRESULTKO') . (!empty($vcm_err) ? ' - ' . $vcm_err : ''));
3561
3562 // return true because the booking was actually stored
3563 return true;
3564 }
3565 } elseif (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'synch.vikbooking.php')) {
3566 // set the necessary action to invoke VCM
3567 $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']);
3568
3569 $this->setChannelManagerAction(JText::translate('VBCHANNELMANAGERINVOKEASK') . ' <button type="button" class="btn btn-primary" onclick="document.location.href=\'' . $vcm_sync_url . '\';">' . JText::translate('VBCHANNELMANAGERSENDRQ') . '</button>');
3570 }
3571 }
3572
3573 if (VikBooking::isAdmin()) {
3574 /**
3575 * Trigger event to allow third party plugins to intercept the admin new booking event.
3576 *
3577 * @since 1.16.8 (J) - 1.6.8 (WP)
3578 */
3579 VBOFactory::getPlatform()->getDispatcher()->trigger('onAfterCreateNewBookingAdmin', [$newoid]);
3580 }
3581
3582 return true;
3583 }
3584
3585 /**
3586 * Attempts to find the default payment method to be assigned to a booking.
3587 *
3588 * @param bool $auto If true, it was requested to automatically find the best payment method.
3589 *
3590 * @return string The payment method string for the reservation "ID=Name", or an empty string.
3591 *
3592 * @since 1.17.3 (J) - 1.7.3 (WP)
3593 */
3594 protected function getDefaultPaymentMethod($auto = false)
3595 {
3596 $dbo = JFactory::getDbo();
3597
3598 $dbo->setQuery(
3599 $dbo->getQuery(true)
3600 ->select('*')
3601 ->from($dbo->qn('#__vikbooking_gpayments'))
3602 ->order($dbo->qn('published') . ' DESC')
3603 ->order($dbo->qn('ordering') . ' ASC')
3604 ->order($dbo->qn('setconfirmed') . ' ASC')
3605 ->order($dbo->qn('name') . ' ASC')
3606 );
3607
3608 $methods = $dbo->loadAssocList();
3609
3610 if ($auto) {
3611 // exclude all the offline or unpublished payment methods
3612 $methods = array_filter($methods, function($method) {
3613 return !((bool) intval($method['setconfirmed'])) && (bool) intval($method['published']);
3614 });
3615
3616 // reset array keys
3617 $methods = array_values($methods);
3618 }
3619
3620 if ($methods) {
3621 // default payment method found
3622 return sprintf('%s=%s', $methods[0]['id'], $methods[0]['name']);
3623 }
3624
3625 // nothing was found
3626 return '';
3627 }
3628 }
3629