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

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