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

2,605 lines 83.6 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 * Proxy for immediately getting the object and bind data.
37 *
38 * @param array|object $data optional data to bind.
39 * @param boolean $anew true for forcing a new instance.
40 *
41 * @return self
42 */
43 public static function getInstance($data = [], $anew = false)
44 {
45 if (is_null(static::$instance) || $anew) {
46 static::$instance = new static($data);
47 }
48
49 return static::$instance;
50 }
51
52 /**
53 * Sets the caller information used to save history records.
54 *
55 * @param string $caller The caller identifier.
56 *
57 * @return self
58 *
59 * @since 1.16.10 (J) - 1.6.10 (WP)
60 */
61 public function setCaller($caller = '')
62 {
63 $this->set('_caller', (string) $caller);
64
65 return $this;
66 }
67
68 /**
69 * Returns the caller information.
70 *
71 * @return string
72 *
73 * @since 1.16.10 (J) - 1.6.10 (WP)
74 */
75 public function getCaller()
76 {
77 return (string) $this->get('_caller', '');
78 }
79
80 /**
81 * Sets the history extra data value.
82 *
83 * @param array $data The history extra data array.
84 *
85 * @return self
86 *
87 * @since 1.16.10 (J) - 1.6.10 (WP)
88 */
89 public function setHistoryData(array $data = [])
90 {
91 $this->set('_historyData', $data);
92
93 return $this;
94 }
95
96 /**
97 * Returns the customer information.
98 *
99 * @return array
100 *
101 * @since 1.16.10 (J) - 1.6.10 (WP)
102 */
103 public function getHistoryData()
104 {
105 return (array) $this->get('_historyData', []);
106 }
107
108 /**
109 * Sets the search filters.
110 *
111 * @param array $data The search filters associative array.
112 *
113 * @return self
114 *
115 * @since 1.16.10 (J) - 1.6.10 (WP)
116 */
117 public function setFilters(array $data = [])
118 {
119 $this->set('_filters', $data);
120
121 return $this;
122 }
123
124 /**
125 * Returns the search filters.
126 *
127 * @return array
128 *
129 * @since 1.16.10 (J) - 1.6.10 (WP)
130 */
131 public function getFilters()
132 {
133 return (array) $this->get('_filters', []);
134 }
135
136 /**
137 * Sets the booking information record.
138 *
139 * @param array $booking The booking record.
140 *
141 * @return self
142 *
143 * @since 1.16.10 (J) - 1.6.10 (WP)
144 */
145 public function setBooking(array $booking = [])
146 {
147 $this->set('_booking', $booking);
148
149 return $this;
150 }
151
152 /**
153 * Returns the booking information record.
154 *
155 * @return array
156 *
157 * @since 1.16.10 (J) - 1.6.10 (WP)
158 */
159 public function getBooking()
160 {
161 return (array) $this->get('_booking', []);
162 }
163
164 /**
165 * Sets the room booking records.
166 *
167 * @param array $room_booking The room booking records.
168 *
169 * @return self
170 *
171 * @since 1.16.10 (J) - 1.6.10 (WP)
172 */
173 public function setRoomBooking(array $room_booking = [])
174 {
175 $this->set('_roomBooking', $room_booking);
176
177 return $this;
178 }
179
180 /**
181 * Returns the room booking records.
182 *
183 * @return array
184 *
185 * @since 1.16.10 (J) - 1.6.10 (WP)
186 */
187 public function getRoomBooking()
188 {
189 return (array) $this->get('_roomBooking', []);
190 }
191
192 /**
193 * Sets the customer information.
194 *
195 * @param array $customer the customer array.
196 *
197 * @return self
198 */
199 public function setCustomer(array $customer = [])
200 {
201 $this->set('_customer', $customer);
202
203 return $this;
204 }
205
206 /**
207 * Returns the customer information.
208 *
209 * @return array
210 */
211 public function getCustomer()
212 {
213 return (array) $this->get('_customer', []);
214 }
215
216 /**
217 * Sets the room information.
218 *
219 * @param array $room the room array.
220 *
221 * @return self
222 */
223 public function setRoom(array $room = [])
224 {
225 $this->set('_room', $room);
226
227 return $this;
228 }
229
230 /**
231 * Returns the room information.
232 *
233 * @return array
234 */
235 public function getRoom()
236 {
237 return (array) $this->get('_room', []);
238 }
239
240 /**
241 * Sets the new booking ID created.
242 *
243 * @param int $bid the newly added record ID.
244 *
245 * @return self
246 */
247 protected function setNewBookingID($bid = 0)
248 {
249 $this->set('_newBookingID', $bid);
250
251 return $this;
252 }
253
254 /**
255 * Returns the new booking ID created, or 0.
256 *
257 * @return int
258 */
259 public function getNewBookingID()
260 {
261 return (int) $this->get('_newBookingID', 0);
262 }
263
264 /**
265 * Sets the VCM action to be performed in order to sync the availability.
266 *
267 * @param string $action the VCM action, usually an HTML link.
268 *
269 * @return self
270 */
271 protected function setChannelManagerAction($action = '')
272 {
273 $this->set('_vcmAction', $action);
274
275 return $this;
276 }
277
278 /**
279 * Returns the VCM action (if any) to sync the availability.
280 *
281 * @return string
282 */
283 public function getChannelManagerAction()
284 {
285 return $this->get('_vcmAction', '');
286 }
287
288 /**
289 * Sets the check-in and check-out times with hours and minutes.
290 *
291 * @return array list of check-in and check-out hours and minutes.
292 */
293 public function loadCheckinOutTimes()
294 {
295 static $times_loaded = null;
296
297 if ($times_loaded) {
298 return [
299 $this->get('checkin_h'),
300 $this->get('checkin_m'),
301 $this->get('checkout_h'),
302 $this->get('checkout_m'),
303 ];
304 }
305
306 $timeopst = VikBooking::getTimeOpenStore();
307 if (is_array($timeopst) && $timeopst) {
308 $opent = VikBooking::getHoursMinutes($timeopst[0]);
309 $closet = VikBooking::getHoursMinutes($timeopst[1]);
310 $hcheckin = $opent[0];
311 $mcheckin = $opent[1];
312 $hcheckout = $closet[0];
313 $mcheckout = $closet[1];
314 } else {
315 $hcheckin = 0;
316 $mcheckin = 0;
317 $hcheckout = 0;
318 $mcheckout = 0;
319 }
320
321 $this->set('checkin_h', $hcheckin);
322 $this->set('checkin_m', $mcheckin);
323 $this->set('checkout_h', $hcheckout);
324 $this->set('checkout_m', $mcheckout);
325
326 $times_loaded = 1;
327
328 return [
329 $hcheckin,
330 $mcheckin,
331 $hcheckout,
332 $mcheckout,
333 ];
334 }
335
336 /**
337 * Attempts to extract the Special Requests from the customer raw data.
338 *
339 * @return string
340 *
341 * @since 1.16.5 (J) - 1.6.5 (WP)
342 */
343 public function extractSpecialRequests()
344 {
345 $raw_cust_data = $this->get('custdata', '');
346 if (empty($raw_cust_data)) {
347 return '';
348 }
349
350 $special_requests = '';
351 if (preg_match("/(?:special_?requests:\s*)(.*?)$/is", $raw_cust_data, $match)) {
352 $special_requests = $match[1];
353 } elseif (preg_match("/(?:special_?request:\s*)(.*?)$/is", $raw_cust_data, $match)) {
354 $special_requests = $match[1];
355 } elseif (preg_match("/(?:special_?request\s*)(.*?)$/is", $raw_cust_data, $match)) {
356 $special_requests = $match[1];
357 } elseif (preg_match("/(?:" . JText::translate('ORDER_SPREQUESTS') . ":\s*)(.*?)$/is", $raw_cust_data, $match)) {
358 $special_requests = $match[1];
359 }
360
361 return $special_requests;
362 }
363
364 /**
365 * Creates a new reservation record after having constructed the
366 * object by properly injecting all the necessary booking information.
367 *
368 * @return bool
369 */
370 public function create()
371 {
372 if (!$this->canCreate()) {
373 $this->setError('Forbidden');
374 return false;
375 }
376
377 // availability helper
378 $av_helper = VikBooking::getAvailabilityInstance();
379
380 // validate mandatory fields
381 $room = $this->getRoom();
382 if (!$this->get('checkin') || !$this->get('checkout') || empty($room['id'])) {
383 $this->setError('Missing mandatory fields');
384 return false;
385 }
386
387 if ($this->get('checkin') >= $this->get('checkout')) {
388 $this->setError('Invalid dates');
389 return false;
390 }
391
392 // make sure we have the time for check-in and check-out
393 if (!$this->get('checkin_h') || !$this->get('checkout_h')) {
394 // make sure to set the times (hours/minutes) for check-in and check-out
395 $this->loadCheckinOutTimes();
396
397 // make sure check-in and check-out timestamps have been set to a proper time
398 $from_info = getdate($this->get('checkin'));
399 $to_info = getdate($this->get('checkout'));
400 if ((int) $from_info['hour'] != (int) $this->get('checkin_h')) {
401 $this->set('checkin', mktime((int) $this->get('checkin_h'), (int) $this->get('checkin_m'), 0, $from_info['mon'], $from_info['mday'], $from_info['year']));
402 }
403 if ((int) $to_info['hour'] != (int) $this->get('checkout_h')) {
404 $this->set('checkout', mktime((int) $this->get('checkout_h'), (int) $this->get('checkout_m'), 0, $to_info['mon'], $to_info['mday'], $to_info['year']));
405 }
406 }
407
408 // number of nights of stay
409 if (!$this->get('nights')) {
410 $this->set('nights', $av_helper->countNightsOfStay($this->get('checkin'), $this->get('checkout')));
411 }
412
413 // fetch and apply turnover time before doing anything else
414 $this->applyTurnover();
415
416 // if rate plan selected, get the tariff ID
417 $this->loadTariffID();
418
419 // get pool of rooms involved
420 $rooms_pool = $this->getRoomsPool();
421 if (!$rooms_pool) {
422 if ($this->getError() === false) {
423 // set generic error if not set already
424 $this->setError('No rooms involved in the reservation');
425 }
426 return false;
427 }
428
429 // check if the room is available
430 $room_available = $this->isRoomAvailable();
431 if (!$this->get('force_booking', 0) && !$this->get('set_closed', 0) && !$room_available) {
432 // no forcing, no closure and room fully booked results into an error message
433 $this->setError(JText::translate('VBBOOKNOTMADE'));
434 return false;
435 }
436
437 // detect if we are forcing the reservation
438 $this->detectForcedReason($room_available);
439
440 // store the customer information
441 $this->storeCustomer();
442
443 // calculate total amount and total tax
444 $this->calculateTotal();
445
446 // store booking and room-booking records
447 if (!$this->storeReservationRecords($rooms_pool)) {
448 if ($this->getError() === false) {
449 // set generic error if not set already
450 $this->setError('Could not create the reservation');
451 }
452 return false;
453 }
454
455 return true;
456 }
457
458 /**
459 * Searches for bookings according to specified filters.
460 *
461 * @return array
462 *
463 * @since 1.16.10 (J) - 1.6.10 (WP)
464 */
465 public function search()
466 {
467 $dbo = JFactory::getDbo();
468
469 $this->totalBookings = 0;
470
471 $filters = $this->getFilters();
472
473 if (!$filters) {
474 $this->setError('Missing filters to search for a booking.');
475 return [];
476 }
477
478 $q = $dbo->getQuery(true)
479 ->select($dbo->qn('o') . '.*')
480 ->select([
481 $dbo->qn('c.first_name', 'customer_first_name'),
482 $dbo->qn('c.last_name', 'customer_last_name'),
483 ])
484 ->from($dbo->qn('#__vikbooking_orders', 'o'))
485 ->leftJoin($dbo->qn('#__vikbooking_customers_orders', 'co') . ' ON ' . $dbo->qn('co.idorder') . ' = ' . $dbo->qn('o.id'))
486 ->leftJoin($dbo->qn('#__vikbooking_customers', 'c') . ' ON ' . $dbo->qn('c.id') . ' = ' . $dbo->qn('co.idcustomer'))
487 ->where(1);
488
489 if (($filters['booking_id'] ?? null)) {
490 $q->andWhere([
491 $dbo->qn('o.id') . ' = ' . $dbo->q($filters['booking_id']),
492 $dbo->qn('o.idorderota') . ' = ' . $dbo->q($filters['booking_id']),
493 ], $glue = 'OR');
494 }
495
496 if (($filters['status'] ?? null)) {
497 $q->where($dbo->qn('o.status') . ' = ' . $dbo->q($filters['status']));
498 }
499
500 if (($filters['exclude_closures'] ?? false)) {
501 $q->where($dbo->qn('o.closure') . ' = 0');
502 }
503
504 if (($filters['exclude_expired'] ?? false)) {
505 // take only active reservations with a check-out date in the future
506 $today_dt = JFactory::getDate('today', new DateTimeZone(date_default_timezone_get()));
507 $q->where($dbo->qn('o.checkout') . ' >= ' . $dbo->q($today_dt->format('U', true)));
508 }
509
510 if (($filters['email'] ?? null)) {
511 $q->where($dbo->qn('o.custmail') . ' = ' . $dbo->q($filters['email']));
512 }
513
514 if (($filters['phone'] ?? null)) {
515 $q->where(sprintf('REPLACE(%s, \' \', \'\') LIKE REPLACE(%s, \' \', \'\')',
516 $dbo->qn('o.phone'),
517 $dbo->q('%' . $filters['phone'])
518 ));
519 }
520
521 if (($filters['date_range']['type'] ?? null) && (($filters['date_range']['start'] ?? null) || ($filters['date_range']['end'] ?? null))) {
522 // search by date range
523 $from_dt = JFactory::getDate(($filters['date_range']['start'] ?? $filters['date_range']['end']));
524 $from_dt->modify('00:00:00');
525 $to_dt = JFactory::getDate(($filters['date_range']['end'] ?? $filters['date_range']['start']));
526 $to_dt->modify('23:59:59');
527
528 // check the type of date
529 if ($filters['date_range']['type'] == 'stay') {
530 // find intersections of stay dates
531 $q->andWhere([
532 '(' . $dbo->qn('o.checkin') . ' <= ' . $dbo->q($from_dt->format('U')) . ' AND ' . $dbo->qn('o.checkout') . ' >= ' . $dbo->q($to_dt->format('U')) . ')',
533 '(' . $dbo->qn('o.checkin') . ' >= ' . $dbo->q($from_dt->format('U')) . ' AND ' . $dbo->qn('o.checkout') . ' <= ' . $dbo->q($to_dt->format('U')) . ')',
534 '(' . $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')) . ')',
535 '(' . $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')) . ')',
536 ], $glue = 'OR');
537 } else {
538 $column = $dbo->qn('o.checkin');
539 if ($filters['date_range']['type'] == 'checkout') {
540 $column = $dbo->qn('o.checkout');
541 } elseif ($filters['date_range']['type'] == 'creation') {
542 $column = $dbo->qn('o.ts');
543 }
544 $q->where($column . ' >= ' . $dbo->q($from_dt->format('U')));
545 $q->where($column . ' <= ' . $dbo->q($to_dt->format('U')));
546 }
547 } else {
548 // check for single date filters
549 if (($filters['creation_date'] ?? null)) {
550 // dates are expected to be in military format
551 $creation = JFactory::getDate($filters['creation_date']);
552 $creation->modify('00:00:00');
553 $q->where($dbo->qn('o.ts') . ' >= ' . $dbo->q($creation->format('U')));
554 $creation->modify('23:59:59');
555 $q->where($dbo->qn('o.ts') . ' <= ' . $dbo->q($creation->format('U')));
556 }
557
558 if (($filters['checkin_date'] ?? null) && !($filters['checkout_date'] ?? null)) {
559 // dates are expected to be in military format
560 $checkin = JFactory::getDate($filters['checkin_date']);
561 $checkin->modify('00:00:00');
562 $q->where($dbo->qn('o.checkin') . ' >= ' . $dbo->q($checkin->format('U')));
563 $checkin->modify('23:59:59');
564 $q->where($dbo->qn('o.checkin') . ' <= ' . $dbo->q($checkin->format('U')));
565 }
566
567 if (($filters['checkout_date'] ?? null) && !($filters['checkin_date'] ?? null)) {
568 // dates are expected to be in military format
569 $checkout = JFactory::getDate($filters['checkout_date']);
570 $checkout->modify('00:00:00');
571 $q->where($dbo->qn('o.checkout') . ' >= ' . $dbo->q($checkout->format('U')));
572 $checkout->modify('23:59:59');
573 $q->where($dbo->qn('o.checkout') . ' <= ' . $dbo->q($checkout->format('U')));
574 }
575
576 if (($filters['checkin_date'] ?? null) && ($filters['checkout_date'] ?? null)) {
577 // range of dates (dates are expected to be in military format)
578 $checkin = JFactory::getDate($filters['checkin_date']);
579 $checkin->modify('00:00:00');
580 $checkout = JFactory::getDate($filters['checkout_date']);
581 $checkout->modify('23:59:59');
582 $q->andWhere([
583 $dbo->qn('o.checkin') . ' BETWEEN ' . $dbo->q($checkin->format('U')) . ' AND ' . $dbo->q($checkout->format('U')),
584 $dbo->qn('o.checkout') . ' BETWEEN ' . $dbo->q($checkin->format('U')) . ' AND ' . $dbo->q($checkout->format('U')),
585 ], $glue = 'OR');
586 }
587
588 if (($filters['stay_date'] ?? null)) {
589 // dates are expected to be in military format
590 $staydt = JFactory::getDate($filters['stay_date']);
591 $staydt->modify('23:59:59');
592 $q->where($dbo->qn('o.checkin') . ' < ' . $dbo->q($staydt->format('U')));
593 $q->where($dbo->qn('o.checkout') . ' > ' . $dbo->q($staydt->format('U')));
594 }
595 }
596
597 if (($filters['customer_name'] ?? null)) {
598 $q->where('CONCAT_WS(\' \', ' . $dbo->qn('c.first_name') . ', ' . $dbo->qn('c.last_name') . ') LIKE ' . $dbo->q('%' . $filters['customer_name'] . '%'));
599 }
600
601 if (($filters['confirmation_number'] ?? null)) {
602 $q->where($dbo->qn('o.confirmnumber') . ' = ' . $dbo->q($filters['confirmation_number']));
603 }
604
605 if (($filters['room_name'] ?? null)) {
606 // find the room involved from the given name
607 $room_record = VikBooking::getAvailabilityInstance()->getRoomByName($filters['room_name']);
608 if ($room_record) {
609 $q->leftJoin($dbo->qn('#__vikbooking_ordersrooms', 'or') . ' ON ' . $dbo->qn('or.idorder') . ' = ' . $dbo->qn('o.id'));
610 $q->where($dbo->qn('or.idroom') . ' = ' . (int) $room_record['id']);
611 }
612 }
613
614 /**
615 * It is now possible to use a custom ordering.
616 *
617 * @since 1.17.1 (J) - 1.7.1 (WP)
618 */
619 switch ($filters['ordering'] ?? 'id') {
620 case 'creation': $ordering = 'o.ts'; break;
621 case 'checkin': $ordering = 'o.checkin'; break;
622 case 'checkout': $ordering = 'o.checkout'; break;
623 default: $ordering = 'o.id';
624 }
625
626 $q->order($dbo->qn($ordering) . ' ' . (strcasecmp($filters['direction'] ?? 'desc', 'desc') ? 'ASC' : 'DESC'));
627
628 $dbo->setQuery($q, 0, ($filters['max_bookings'] ?? 0));
629 $rows = $dbo->loadAssocList();
630
631 $this->totalBookings = count($rows);
632
633 /**
634 * Calculate the total number of matching records.
635 *
636 * @since 1.17.1 (J) - 1.7.1 (WP)
637 */
638 if ($this->totalBookings && $this->totalBookings == ($filters['max_bookings'] ?? 0)) {
639 // set up the query used to count the matching records
640 $dbo->setQuery($q->clear('select')->clear('offset')->clear('limit')->select('COUNT(1)'));
641 $this->totalBookings = (int) $dbo->loadResult();
642 }
643
644 return $rows;
645 }
646
647 /**
648 * Returns the total number of bookings matching the last search query made.
649 *
650 * @return int
651 *
652 * @since 1.17.1 (J) - 1.7.1 (WP)
653 */
654 public function getTotBookingsFound()
655 {
656 return $this->totalBookings;
657 }
658
659 /**
660 * Modifies the requested booking ID according to the provided options.
661 * This method does not support all rate plan options like for the creation of a
662 * new booking. This is a method for making quick updates concerning a room switch,
663 * a change of stay dates, new booking total amount, guests, add extra services etc..
664 *
665 * @param array $options List of details to perform the modification.
666 *
667 * @return bool
668 *
669 * @since 1.16.10 (J) - 1.6.10 (WP)
670 */
671 public function modify(array $options)
672 {
673 $dbo = JFactory::getDbo();
674
675 // access the previous booking details
676 $prev_booking = $this->getBooking();
677
678 // access the current rooms booked
679 $roomBooking = $this->getRoomBooking();
680
681 // gather modification options
682 $booking_id = $options['booking_id'] ?? $prev_booking['id'] ?? 0;
683
684 if (!$booking_id) {
685 $this->setError('Missing booking ID.');
686 return false;
687 }
688
689 if (!$prev_booking) {
690 // load current booking record if not injected
691 $prev_booking = VikBooking::getBookingInfoFromID($booking_id);
692 if (!$prev_booking) {
693 $this->setError('Booking not found.');
694 return false;
695 }
696 }
697
698 if (!$roomBooking) {
699 // load current rooms booked
700 $roomBooking = VikBooking::loadOrdersRoomsData($booking_id);
701 }
702
703 // do not touch this array property because it's used by VCM
704 $prev_booking['rooms_info'] = $roomBooking;
705
706 // list of operations to trigger/perform
707 $trigger_operations = [];
708
709 // list of history description rows
710 $history_descr_rows = [];
711
712 // access availability helper
713 $av_helper = VikBooking::getAvailabilityInstance();
714
715 // calculate the new stay dates, if different
716 $diff_stay_dates = false;
717 $set_checkin = date('Y-m-d', $prev_booking['checkin']);
718 $set_checkout = date('Y-m-d', $prev_booking['checkout']);
719 if (($options['checkin'] ?? null)) {
720 // date is expected in military format
721 $diff_stay_dates = $diff_stay_dates || ($options['checkin'] != $set_checkin);
722 $set_checkin = $options['checkin'];
723 }
724 if (($options['checkout'] ?? null)) {
725 // date is expected in military format
726 $diff_stay_dates = $diff_stay_dates || ($options['checkout'] != $set_checkout);
727 $set_checkout = $options['checkout'];
728 }
729
730 // ensure the stay dates are valid
731 if (JFactory::getDate($set_checkin) >= JFactory::getDate($set_checkout)) {
732 $this->setError('Invalid stay dates provided.');
733 return false;
734 }
735
736 // ensure we are not changing dates for a split-stay reservation
737 if ($diff_stay_dates && !empty($prev_booking['split_stay'])) {
738 // we receive the stay dates at booking record, so we cannot proceed with the update
739 $this->setError('Cannot modify the stay dates for a split-stay reservation. Please do it manually.');
740 return false;
741 }
742
743 // set dates involved
744 $av_helper->setStayDates($set_checkin, $set_checkout);
745
746 // count new nights of stay
747 $set_nights = $av_helper->countNightsOfStay();
748
749 // gather stay timestamps
750 list($set_checkin_ts, $set_checkout_ts) = $av_helper->getStayDates(true);
751
752 // load the current busy record IDs before any modification, if any
753 $dbo->setQuery(
754 $dbo->getQuery(true)
755 ->select('*')
756 ->from($dbo->qn('#__vikbooking_ordersbusy'))
757 ->where($dbo->qn('idorder') . ' = ' . (int) $booking_id)
758 );
759 $busy_ids = array_column($dbo->loadAssocList(), 'idbusy');
760
761 // first off, check if any room switch was requested (recommended one switch at most)
762 $switching_details = [];
763 if ($options['switch_rooms'] ?? []) {
764 // get all room IDs for the switch that were not booked already
765 $booked_rooms = array_column($roomBooking, 'idroom');
766 $new_missing_rooms = array_values(array_diff((array) $options['switch_rooms'], $booked_rooms));
767
768 // scan all rooms requested for the switch that were not booked already
769 foreach ($new_missing_rooms as $index => $switch_room_id) {
770 if (!isset($roomBooking[$index])) {
771 // adding more rooms is not supported
772 break;
773 }
774 // ensure the room switch is allowed (room should be available on the new dates)
775 $switched_room_info = VikBooking::getRoomInfo($switch_room_id, ['id', 'name', 'units']);
776 if (!$switched_room_info) {
777 $this->setError('The requested room could not be found for the switch.');
778 return false;
779 }
780 if (!VikBooking::roomBookable($switch_room_id, 1, $set_checkin_ts, $set_checkout_ts, $busy_ids)) {
781 // abort by setting a descriptive error message
782 $this->setError(sprintf(
783 'The room %s is not available from %s to %s, and so the room switch cannot be made.',
784 $switched_room_info['name'] ?? '',
785 $set_checkin,
786 $set_checkout
787 ));
788 return false;
789 }
790 }
791
792 // scan again all rooms to be switched once we know they are available
793 foreach ($new_missing_rooms as $index => $switch_room_id) {
794 if (!isset($roomBooking[$index])) {
795 // adding more rooms is not supported
796 break;
797 }
798
799 // update room-booking record by switching room ID
800 $q = $dbo->getQuery(true)
801 ->update($dbo->qn('#__vikbooking_ordersrooms'))
802 ->set($dbo->qn('idroom') . ' = ' . (int) $switch_room_id)
803 ->where($dbo->qn('idorder') . ' = ' . (int) $booking_id)
804 ->where($dbo->qn('idroom') . ' = ' . (int) ($roomBooking[$index]['idroom'] ?? 0));
805 $dbo->setQuery($q, 0, 1);
806 $dbo->execute();
807
808 if ($busy_ids) {
809 // update busy records with new stay dates just for the switched room
810 $q = $dbo->getQuery(true)
811 ->update($dbo->qn('#__vikbooking_busy'))
812 ->set($dbo->qn('idroom') . ' = ' . (int) $switch_room_id)
813 ->set($dbo->qn('checkin') . ' = ' . $dbo->q($set_checkin_ts))
814 ->set($dbo->qn('checkout') . ' = ' . $dbo->q($set_checkout_ts))
815 ->set($dbo->qn('realback') . ' = ' . $dbo->q($set_checkout_ts + (VikBooking::getHoursRoomAvail() * 3600)))
816 ->where($dbo->qn('id') . ' IN (' . implode(', ', array_map('intval', $busy_ids)) . ')')
817 ->where($dbo->qn('idroom') . ' = ' . (int) ($roomBooking[$index]['idroom'] ?? 0));
818 $dbo->setQuery($q, 0, 1);
819 $dbo->execute();
820
821 // register room switching details
822 $switching_details[$index] = $switch_room_id;
823
824 // register CM sync operation
825 $trigger_operations[] = 'vcm_sync';
826 }
827
828 // register history description row
829 $switched_room_info = VikBooking::getRoomInfo($switch_room_id, ['id', 'name', 'units']);
830 $prev_room_info = VikBooking::getRoomInfo($roomBooking[$index]['idroom'] ?? 0, ['id', 'name', 'units']);
831 $history_descr_rows[] = sprintf('%s switched with %s.', $prev_room_info['name'] ?? '', $switched_room_info['name'] ?? '');
832 }
833 }
834
835 // start query builder for booking record
836 $bookingQ = $dbo->getQuery(true)
837 ->update($dbo->qn('#__vikbooking_orders'))
838 ->where($dbo->qn('id') . ' = ' . (int) $booking_id);
839
840 // modify stay dates, if requested
841 if ($diff_stay_dates) {
842 // ensure all rooms are bookable on the new stay dates
843 if ($prev_booking['status'] == 'confirmed') {
844 foreach ($roomBooking as $kor => $or) {
845 if ($switching_details[$kor] ?? null) {
846 // this room index was switched with another room, hence we know it was available
847 continue;
848 }
849 if (!VikBooking::roomBookable($or['idroom'], 1, $set_checkin_ts, $set_checkout_ts, $busy_ids)) {
850 // abort
851 $abort_room_info = VikBooking::getRoomInfo($or['idroom'], ['id', 'name', 'units']);
852 $this->setError(sprintf(
853 'The room %s is not available from %s to %s, and so the stay dates cannot be modified.',
854 $abort_room_info['name'] ?? '',
855 $set_checkin,
856 $set_checkout
857 ));
858 return false;
859 }
860 }
861 }
862
863 // set booking values to update
864 $bookingQ->set($dbo->qn('checkin') . ' = ' . $dbo->q($set_checkin_ts));
865 $bookingQ->set($dbo->qn('checkout') . ' = ' . $dbo->q($set_checkout_ts));
866 $bookingQ->set($dbo->qn('days') . ' = ' . $dbo->q($set_nights));
867
868 // update busy records, if any (reservation status could be confirmed)
869 if ($busy_ids) {
870 // update busy records with new stay dates for all rooms
871 $dbo->setQuery(
872 $dbo->getQuery(true)
873 ->update($dbo->qn('#__vikbooking_busy'))
874 ->set($dbo->qn('checkin') . ' = ' . $dbo->q($set_checkin_ts))
875 ->set($dbo->qn('checkout') . ' = ' . $dbo->q($set_checkout_ts))
876 ->set($dbo->qn('realback') . ' = ' . $dbo->q($set_checkout_ts + (VikBooking::getHoursRoomAvail() * 3600)))
877 ->where($dbo->qn('id') . ' IN (' . implode(', ', array_map('intval', $busy_ids)) . ')')
878 );
879 $dbo->execute();
880
881 // register CM sync operation
882 $trigger_operations[] = 'vcm_sync';
883 }
884
885 // register operation to trigger the shared calendars
886 $trigger_operations[] = 'shared_calendars';
887 }
888
889 // update number of guests, if requested
890 if (($options['guests']['adults'] ?? null) || ($options['guests']['children'] ?? null)) {
891 // update the requested number of guests ONLY on the first room booked
892 $q = $dbo->getQuery(true)
893 ->update($dbo->qn('#__vikbooking_ordersrooms'))
894 ->set($dbo->qn('adults') . ' = ' . (int) ($options['guests']['adults'] ?? $roomBooking[0]['adults']))
895 ->set($dbo->qn('children') . ' = ' . (int) ($options['guests']['children'] ?? $roomBooking[0]['children']))
896 ->where($dbo->qn('idorder') . ' = ' . (int) $booking_id);
897 $dbo->setQuery($q, 0, 1);
898 $dbo->execute();
899
900 // register history description row
901 $history_descr_rows[] = sprintf(
902 'New adults %d, new children %d.',
903 (int) ($options['guests']['adults'] ?? $roomBooking[0]['adults']),
904 (int) ($options['guests']['children'] ?? $roomBooking[0]['children'])
905 );
906 }
907
908 // check if extra services should be added and calculate the booking cost difference
909 $new_extras_cost = 0;
910 if (is_array(($options['add_extra_services'] ?? null))) {
911 $current_extras = !empty($roomBooking[0]['extracosts']) ? json_decode($roomBooking[0]['extracosts'], true) : [];
912 $current_extras = is_array($current_extras) ? $current_extras : [];
913 $new_extras = [];
914 foreach ($options['add_extra_services'] as $extras) {
915 if (!is_array($extras) || (!isset($extras['name']) && !isset($extras['cost']))) {
916 // invalid extra service structure
917 continue;
918 }
919
920 // build new extra service
921 $new_extra = [
922 'name' => (string) ($extras['name'] ?? 'Custom Extra'),
923 'cost' => (float) ($extras['cost'] ?? 0),
924 'idtax' => null,
925 ];
926
927 // push custom extra service
928 $current_extras[] = $new_extra;
929
930 // push the custom extra service in the new list
931 $new_extras[] = $new_extra;
932 }
933
934 if ($new_extras) {
935 // update the extra services ONLY on the first room booked
936 $q = $dbo->getQuery(true)
937 ->update($dbo->qn('#__vikbooking_ordersrooms'))
938 ->set($dbo->qn('extracosts') . ' = ' . $dbo->q(json_encode($current_extras)))
939 ->where($dbo->qn('idorder') . ' = ' . (int) $booking_id);
940 $dbo->setQuery($q, 0, 1);
941 $dbo->execute();
942
943 // register history description row
944 $history_descr_rows[] = sprintf(
945 'New extras: %s.',
946 implode(', ', array_column($new_extras, 'name'))
947 );
948
949 // check if we need to increase the booking total amount
950 $new_extras_cost = array_sum(array_column($new_extras, 'cost'));
951
952 if ($new_extras_cost > 0 && (float) ($options['cost_difference'] ?? 0) < $new_extras_cost) {
953 // increase the "cost difference" due to the newly added extra services
954 $options['cost_difference'] = ($options['cost_difference'] ?? 0) + $new_extras_cost;
955 }
956 }
957 }
958
959 // check if the booking total amount should change
960 if ($options['cost_difference'] ?? null) {
961 // this difference should be summed to (or deducted from) the current booking total value
962 $bookingQ->set($dbo->qn('total') . ' = ' . ($prev_booking['total'] + (float) $options['cost_difference']));
963
964 // register history description row
965 $history_descr_rows[] = sprintf(
966 'Booking total cost difference calculated: %d.',
967 (float) $options['cost_difference']
968 );
969
970 // calculate the cost difference just for the rooms
971 $rooms_cost_difference = (float) $options['cost_difference'] - $new_extras_cost;
972
973 if ($rooms_cost_difference) {
974 // this value should be summed to (or deducted from) the current room rate to have a proper calculation
975 $new_room_cost = null;
976 $room_cost_prop = null;
977 if (!empty($roomBooking[0]['cust_cost'])) {
978 $new_room_cost = $roomBooking[0]['cust_cost'] + $rooms_cost_difference;
979 $room_cost_prop = 'cust_cost';
980 } elseif (!empty($roomBooking[0]['room_cost'])) {
981 $new_room_cost = $roomBooking[0]['room_cost'] + $rooms_cost_difference;
982 $room_cost_prop = 'room_cost';
983 }
984
985 if ($room_cost_prop) {
986 // we can update the room cost for the difference calculated ONLY on the first room booked
987 // if no room cost was found, maybe because of a tariff, we would keep just the total changed
988 $q = $dbo->getQuery(true)
989 ->update($dbo->qn('#__vikbooking_ordersrooms'))
990 ->set($dbo->qn($room_cost_prop) . ' = ' . $new_room_cost)
991 ->where($dbo->qn('idorder') . ' = ' . (int) $booking_id);
992 $dbo->setQuery($q, 0, 1);
993 $dbo->execute();
994 }
995 }
996 }
997
998 if ($options['extra_notes'] ?? '') {
999 // update administrator notes
1000 $bookingQ->set($dbo->qn('adminnotes') . ' = ' . $dbo->q(trim($prev_booking['adminnotes'] . "\n" . $options['extra_notes'])));
1001 }
1002
1003 // finally, update the booking record
1004 try {
1005 // make sure something to update was set by using the apposite getter magic method
1006 if ($bookingQ->set) {
1007 // some booking record values should be updated
1008 $dbo->setQuery($bookingQ);
1009 $dbo->execute();
1010 }
1011 } catch (Throwable $e) {
1012 $this->setError($e->getMessage());
1013 return false;
1014 }
1015
1016 // update booking history
1017 $history_obj = VikBooking::getBookingHistoryInstance($booking_id);
1018
1019 $now_user = JFactory::getUser();
1020 $caller_id = $now_user->name ? "({$now_user->name})" : '';
1021 if ($this->getCaller()) {
1022 $caller_id = '(' . $this->getCaller() . ')';
1023 if ($this->getHistoryData()) {
1024 $history_obj->setExtraData($this->getHistoryData());
1025 }
1026 }
1027
1028 // update Booking History
1029 $history_obj->store('MB', $caller_id . ($history_descr_rows ? "\n" . implode("\n", $history_descr_rows) : ''));
1030
1031 // check for the operations to perform
1032 if (in_array('shared_calendars', $trigger_operations)) {
1033 // unset any previously booked room due to calendar sharing
1034 VikBooking::cleanSharedCalendarsBusy($booking_id);
1035 // check if some of the rooms booked have shared calendars
1036 VikBooking::updateSharedCalendars($booking_id);
1037 }
1038
1039 if (in_array('vcm_sync', $trigger_operations)) {
1040 // invoke Channel Manager
1041 $vcm_autosync = VikBooking::vcmAutoUpdate();
1042 if ($vcm_autosync > 0) {
1043 $vcm_obj = VikBooking::getVcmInvoker();
1044 $vcm_obj->setOids([$booking_id])->setSyncType('modify')->setOriginalBooking($prev_booking);
1045 $sync_result = $vcm_obj->doSync();
1046 if ($sync_result === false) {
1047 // set error message
1048 $vcm_err = $vcm_obj->getError();
1049 $this->setError(JText::translate('VBCHANNELMANAGERRESULTKO') . (!empty($vcm_err) ? ' - ' . $vcm_err : ''));
1050 }
1051 } elseif (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'synch.vikbooking.php')) {
1052 // set the necessary action to invoke VCM manually
1053 $this->setChannelManagerAction(
1054 JText::translate('VBCHANNELMANAGERINVOKEASK') . ' ' .
1055 '<form action="index.php?option=com_vikbooking" method="post">' .
1056 '<input type="hidden" name="option" value="com_vikbooking"/>' .
1057 '<input type="hidden" name="task" value="invoke_vcm"/>' .
1058 '<input type="hidden" name="stype" value="modify"/>' .
1059 '<input type="hidden" name="cid[]" value="' . $booking_id . '"/>' .
1060 '<input type="hidden" name="origb" value="' . urlencode(json_encode($prev_booking)) . '"/>' .
1061 '<button type="submit" class="btn btn-primary">' . JText::translate('VBCHANNELMANAGERSENDRQ') . '</button>' .
1062 '</form>'
1063 );
1064 }
1065 }
1066
1067 if (($options['ota_reporting'] ?? null) && $diff_stay_dates) {
1068 // perform the OTA reporting action, if allowed
1069 if (class_exists('VCMOtaReporting') && VCMOtaReporting::getInstance($ord)->stayChangeAllowed()) {
1070 // check if an OTA reporting action is needed
1071 $ota_stay_change_data = [];
1072 foreach ($roomBooking as $kor => $or) {
1073 // set room data for stay change
1074 $ota_stay_change_room = [
1075 'idroom' => $or['idroom'],
1076 'checkin' => $set_checkin,
1077 'checkout' => $set_checkout,
1078 ];
1079 if (isset($or['modified_price'])) {
1080 $ota_stay_change_room['price'] = $or['modified_price'];
1081 }
1082 // push room data for stay change
1083 $ota_stay_change_data[] = $ota_stay_change_room;
1084 }
1085
1086 // notify the OTA through Vik Channel Manager
1087 $ota_reporting = VCMOtaReporting::getInstance();
1088 $ota_result = $ota_reporting->notifyStayChange($ota_stay_change_data);
1089 if (!$ota_result) {
1090 // register error message
1091 $this->setError($ota_reporting->getError());
1092 }
1093 }
1094 }
1095
1096 return true;
1097 }
1098
1099 /**
1100 * Deletes the requested booking ID.
1101 *
1102 * @param array $options List of details to perform the cancellation.
1103 *
1104 * @return bool
1105 *
1106 * @since 1.16.10 (J) - 1.6.10 (WP)
1107 */
1108 public function delete(array $options)
1109 {
1110 $dbo = JFactory::getDbo();
1111
1112 $booking_id = $options['booking_id'] ?? 0;
1113 $canc_reason = $options['cancellation_reason'] ?? '';
1114 $purge_remove = $options['purge_remove'] ?? false;
1115
1116 $booking = VikBooking::getBookingInfoFromID($booking_id);
1117
1118 if (!$booking) {
1119 $this->setError('Booking not found.');
1120 return false;
1121 }
1122
1123 if ($booking['status'] === 'cancelled' && !$purge_remove) {
1124 $this->setError(sprintf('Booking ID %d is already cancelled.', $booking['id']));
1125 return false;
1126 }
1127
1128 if (class_exists('VCMFeesCancellation')) {
1129 // let VCM detect if there are any constraints for the cancellation
1130 $canc_denied = VCMFeesCancellation::getInstance($booking, $anew = true)->isBookingConstrained();
1131 if ($canc_denied) {
1132 // set error message
1133 $canc_deny_error = VCMFeesCancellation::getInstance()->getError();
1134 $this->setError($canc_deny_error ?: 'Booking cannot be cancelled due to OTA contraints.');
1135 return false;
1136 }
1137 }
1138
1139 // access the current user
1140 $now_user = JFactory::getUser();
1141
1142 // whether OTAs should be notified
1143 $notify_otas = false;
1144
1145 if ($booking['status'] != 'cancelled') {
1146 // update status to cancelled
1147 $q = $dbo->getQuery(true)
1148 ->update($dbo->qn('#__vikbooking_orders'))
1149 ->set($dbo->qn('status') . ' = ' . $dbo->q('cancelled'))
1150 ->where($dbo->qn('id') . ' = ' . (int) $booking['id']);
1151 if (!empty($canc_reason)) {
1152 $set_canc_reason = (!empty($booking['adminnotes']) ? $booking['adminnotes'] . "\n" : '') . $canc_reason;
1153 $q->set($dbo->qn('adminnotes') . ' = ' . $dbo->q($set_canc_reason));
1154 }
1155 $dbo->setQuery($q);
1156 $dbo->execute();
1157
1158 // delete temporarily locked records, if any
1159 $dbo->setQuery(
1160 $dbo->getQuery(true)
1161 ->delete($dbo->qn('#__vikbooking_tmplock'))
1162 ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
1163 );
1164 $dbo->execute();
1165
1166 if ($booking['status'] == 'confirmed') {
1167 // turn flag on
1168 $notify_otas = true;
1169 }
1170
1171 // access history object
1172 $history_obj = VikBooking::getBookingHistoryInstance($booking['id']);
1173
1174 $caller_id = $now_user->name ? "({$now_user->name})" : '';
1175 if ($this->getCaller()) {
1176 $caller_id = '(' . $this->getCaller() . ')';
1177 if ($this->getHistoryData()) {
1178 $history_obj->setExtraData($this->getHistoryData());
1179 }
1180 }
1181
1182 // update Booking History
1183 $history_obj->store('CB', $caller_id);
1184 }
1185
1186 // always attempt to free records up
1187 $dbo->setQuery(
1188 $dbo->getQuery(true)
1189 ->select('*')
1190 ->from($dbo->qn('#__vikbooking_ordersbusy'))
1191 ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
1192 );
1193 foreach ($dbo->loadAssocList() as $ob) {
1194 // delete busy record
1195 $dbo->setQuery(
1196 $dbo->getQuery(true)
1197 ->delete($dbo->qn('#__vikbooking_busy'))
1198 ->where($dbo->qn('id') . ' = ' . (int) $ob['idbusy'])
1199 );
1200 $dbo->execute();
1201 }
1202
1203 // delete booking-busy-record relations
1204 $dbo->setQuery(
1205 $dbo->getQuery(true)
1206 ->delete($dbo->qn('#__vikbooking_ordersbusy'))
1207 ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
1208 );
1209 $dbo->execute();
1210
1211 // check for purge removal
1212 if ($booking['status'] === 'cancelled' && $purge_remove) {
1213 // delete booking-customer relation
1214 $dbo->setQuery(
1215 $dbo->getQuery(true)
1216 ->delete($dbo->qn('#__vikbooking_customers_orders'))
1217 ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
1218 );
1219 $dbo->execute();
1220
1221 // delete booking-room relations
1222 $dbo->setQuery(
1223 $dbo->getQuery(true)
1224 ->delete($dbo->qn('#__vikbooking_ordersrooms'))
1225 ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
1226 );
1227 $dbo->execute();
1228
1229 // delete booking-history relations
1230 $dbo->setQuery(
1231 $dbo->getQuery(true)
1232 ->delete($dbo->qn('#__vikbooking_orderhistory'))
1233 ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
1234 );
1235 $dbo->execute();
1236
1237 // delete the booking record
1238 $dbo->setQuery(
1239 $dbo->getQuery(true)
1240 ->delete($dbo->qn('#__vikbooking_orders'))
1241 ->where($dbo->qn('id') . ' = ' . (int) $booking['id'])
1242 );
1243 $dbo->execute();
1244
1245 // in case of split stay booking, remove the transient
1246 if ($booking['split_stay']) {
1247 VBOFactory::getConfig()->remove('split_stay_' . $booking['id']);
1248 }
1249 }
1250
1251 if ($notify_otas) {
1252 $vcm_autosync = VikBooking::vcmAutoUpdate();
1253 if ($vcm_autosync > 0) {
1254 $vcm_obj = VikBooking::getVcmInvoker();
1255 $vcm_obj->setOids([$booking['id']])->setSyncType('cancel');
1256 $sync_result = $vcm_obj->doSync();
1257 if ($sync_result === false) {
1258 // set error message
1259 $vcm_err = $vcm_obj->getError();
1260 $this->setError(JText::translate('VBCHANNELMANAGERRESULTKO') . (!empty($vcm_err) ? ' - ' . $vcm_err : ''));
1261 }
1262 }
1263 }
1264
1265 return true;
1266 }
1267
1268 /**
1269 * Tells if the booking record set can be modified and/or cancelled.
1270 *
1271 * @return array
1272 *
1273 * @since 1.16.10 (J) - 1.6.10 (WP)
1274 */
1275 public function getAlterationDetails()
1276 {
1277 $dbo = JFactory::getDbo();
1278
1279 $booking = $this->getBooking();
1280 $roomBooking = $this->getRoomBooking();
1281
1282 if (!$booking || !$roomBooking) {
1283 $this->setError('Missing booking or room booking record details.');
1284 return [];
1285 }
1286
1287 // gather room booking tariffs
1288 $tars = [];
1289 foreach ($roomBooking as $kor => $or) {
1290 $num = $kor + 1;
1291 if (!empty($order['pkg']) || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
1292 // package or custom cost set from the back-end
1293 continue;
1294 }
1295
1296 // get room tariff details
1297 $dbo->setQuery(
1298 $dbo->getQuery(true)
1299 ->select($dbo->qn('t') . '.*')
1300 ->select([
1301 $dbo->qn('p.name'),
1302 $dbo->qn('p.free_cancellation'),
1303 $dbo->qn('p.canc_deadline'),
1304 $dbo->qn('p.canc_policy'),
1305 ])
1306 ->from($dbo->qn('#__vikbooking_dispcost', 't'))
1307 ->leftJoin($dbo->qn('#__vikbooking_prices', 'p') . ' ON ' . $dbo->qn('t.idprice') . ' = ' . $dbo->qn('p.id'))
1308 ->where($dbo->qn('t.id') . ' = ' . (int) ($or['idtar'] ?? 0))
1309 );
1310 $tar = $dbo->loadAssoc();
1311
1312 if ($tar) {
1313 // push room booking tariff
1314 $tars[$num] = $tar;
1315 }
1316 }
1317
1318 // count days to arrival
1319 $days_to_arrival = 0;
1320 $now_info = getdate();
1321 $checkin_info = getdate($booking['checkin'] ?? 0);
1322 if ($now_info[0] < $checkin_info[0]) {
1323 while ($now_info[0] < $checkin_info[0]) {
1324 if (!($now_info['mday'] != $checkin_info['mday'] || $now_info['mon'] != $checkin_info['mon'] || $now_info['year'] != $checkin_info['year'])) {
1325 break;
1326 }
1327 $days_to_arrival++;
1328 $now_info = getdate(mktime(0, 0, 0, $now_info['mon'], ($now_info['mday'] + 1), $now_info['year']));
1329 }
1330 }
1331
1332 // check if the rate plan(s) are refundable
1333 $is_refundable = 0;
1334 $daysadv_refund_arr = [];
1335 $daysadv_refund = 0;
1336 $canc_policy = '';
1337 foreach ($tars as $num => $tar) {
1338 if (!$tar['free_cancellation']) {
1339 // if at least one rate plan is non-refundable, the whole reservation cannot be cancelled
1340 $is_refundable = 0;
1341 $daysadv_refund_arr = [];
1342 break;
1343 }
1344 $is_refundable = 1;
1345 $daysadv_refund_arr[] = $tar['canc_deadline'];
1346 }
1347
1348 // get the rate plan with the lowest cancellation deadline
1349 $daysadv_refund = $daysadv_refund_arr ? min($daysadv_refund_arr) : $daysadv_refund;
1350 if ($daysadv_refund > 0) {
1351 foreach ($tars as $num => $tar) {
1352 if ($tar['free_cancellation'] && $tar['canc_deadline'] == $daysadv_refund) {
1353 // get the cancellation policy from the first rate plan with free cancellation and same cancellation deadline
1354 $canc_policy = $tar['canc_policy'];
1355 break;
1356 }
1357 }
1358 }
1359
1360 // access global settings to determine the alterations available
1361 $resmodcanc = VikBooking::getReservationModCanc();
1362 $resmodcanc = !$days_to_arrival ? 0 : $resmodcanc;
1363 $resmodcancmin = VikBooking::getReservationModCancMin();
1364
1365 // build alteration deadline date
1366 $checkin_dt = JFactory::getDate(date('Y-m-d', ($booking['checkin'] ?? 0)));
1367 $checkin_dt->modify("-{$resmodcancmin} days");
1368 $alteration_deadline = $checkin_dt->format('Y-m-d');
1369
1370 return [
1371 'refundable' => ($resmodcanc > 1 && $resmodcanc != 2 && $is_refundable > 0 && $daysadv_refund <= $days_to_arrival && $days_to_arrival >= $resmodcancmin),
1372 'modifiable' => ($resmodcanc > 1 && $resmodcanc != 3 && $days_to_arrival >= $resmodcancmin),
1373 'alteration_disabled' => $resmodcanc === 0,
1374 'request_alteration' => $resmodcanc === 1,
1375 'cancellation_policy' => $canc_policy ?: null,
1376 'alteration_deadline' => $alteration_deadline,
1377 ];
1378 }
1379
1380 /**
1381 * Attempts to invoke the payment processor assigned to the current booking.
1382 *
1383 * @param array $card Optional credit card details to bind.
1384 *
1385 * @return object The payment processor dispatcher instance.
1386 *
1387 * @throws Exception
1388 *
1389 * @since 1.16.10 (J) - 1.6.10 (WP)
1390 */
1391 public function getPaymentProcessor(array $card = [])
1392 {
1393 $booking = $this->getProperties();
1394 $processor = null;
1395 $payment = [];
1396
1397 if (!$booking) {
1398 throw new Exception('Missing booking details', 500);
1399 }
1400
1401 if (!empty($booking['idpayment'])) {
1402 $payment = VikBooking::getPayment($booking['idpayment']);
1403 }
1404
1405 if (!$payment) {
1406 throw new Exception('Missing payment method details', 500);
1407 }
1408
1409 // set payment details internally
1410 $this->set('_payment_info', $payment);
1411
1412 if ($card) {
1413 // inject CC details for the payment processor
1414 $booking['card'] = $card;
1415 }
1416
1417 if (VBOPlatformDetection::isWordPress()) {
1418 /**
1419 * @wponly The payment gateway is loaded
1420 * through the apposite dispatcher.
1421 */
1422 JLoader::import('adapter.payment.dispatcher');
1423 $processor = JPaymentDispatcher::getInstance('vikbooking', $payment['file'], $booking, $payment['params']);
1424 } elseif (VBOPlatformDetection::isJoomla()) {
1425 /**
1426 * @joomlaonly The Payment Factory library will invoke the gateway.
1427 */
1428 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'payments' . DIRECTORY_SEPARATOR . 'libraries' . DIRECTORY_SEPARATOR . 'factory.php';
1429 $processor = VBOPaymentFactory::getPaymentInstance($payment['file'], $booking, $payment['params']);
1430 }
1431
1432 if (!$processor) {
1433 throw new Exception('Could not invoke the payment processor', 500);
1434 }
1435
1436 // return the valid payment processor instance
1437 return $processor;
1438 }
1439
1440 /**
1441 * Gets the reservation's payment method name.
1442 *
1443 * @return string
1444 *
1445 * @since 1.16.10 (J) - 1.6.10 (WP)
1446 */
1447 public function getPaymentName()
1448 {
1449 // access the reserved property
1450 $payment = (array) $this->get('_payment_info', []);
1451
1452 if (!$payment) {
1453 return '';
1454 }
1455
1456 return $payment['name'] ?? '';
1457 }
1458
1459 /**
1460 * Attempts to get the credit card value pairs from the current booking.
1461 *
1462 * @return array Associative list of CC value-pairs, if any.
1463 *
1464 * @since 1.16.10 (J) - 1.6.10 (WP)
1465 */
1466 public function getCardValuePairs()
1467 {
1468 $booking_info = $this->getProperties();
1469
1470 if (empty($booking_info['paymentlog'])) {
1471 return [];
1472 }
1473
1474 // build complete credit card payload, if available
1475 $cc_payload_str = '';
1476
1477 // extract CC data from payment logs by ensuring they're not null
1478 $booking_info['paymentlog'] = (string) $booking_info['paymentlog'];
1479 if (stripos($booking_info['paymentlog'], 'card number') !== false && strpos($booking_info['paymentlog'], '*') !== false) {
1480 // matched a log for an OTA CC
1481 $cc_payload_str = $booking_info['paymentlog'];
1482 } elseif (preg_match("/(([\d\*]{4,4}\s*){4,4})|(([\d\*]{4,6}\s*){3,3})/", $booking_info['paymentlog'])) {
1483 // matched a credit card
1484 $cc_payload_str = $booking_info['paymentlog'];
1485 }
1486
1487 // check if this is an OTA reservation with remotely decoded CC details required
1488 $remote_cc_data = [];
1489 if (!empty($booking_info['idorderota']) && !empty($booking_info['channel'])) {
1490 // channel source
1491 $channel_source = (string)$booking_info['channel'];
1492 if (strpos($booking_info['channel'], '_') !== false) {
1493 $channelparts = explode('_', $booking_info['channel']);
1494 $channel_source = $channelparts[0];
1495 }
1496
1497 // only updated versions of VCM will support remote CC decoding for OTA reservations
1498 if (class_exists('VCMOtaBooking')) {
1499 // invoke the OTA Booking helper class from VCM
1500 $cc_helper = VCMOtaBooking::getInstance([
1501 'channel_source' => $channel_source,
1502 'ota_id' => $booking_info['idorderota'],
1503 ], $anew = true);
1504
1505 if (method_exists($cc_helper, 'decodeCreditCardDetails')) {
1506 $remote_cc_data = $cc_helper->decodeCreditCardDetails();
1507 // make sure the response was valid
1508 if (!$remote_cc_data || !empty($remote_cc_data['error'])) {
1509 // we ignore the error by simply resetting the array
1510 $remote_cc_data = [];
1511 }
1512 }
1513 }
1514 }
1515
1516 // merge remotely decoded CC details with parsed payment log (if any)
1517 return array_merge($remote_cc_data, $this->parseCreditCardValuePairs($cc_payload_str, $remote_cc_data));
1518 }
1519
1520 /**
1521 * Given a raw string of credit card key-value pairs from payments log,
1522 * parse the corresponding keys and values into an associative array.
1523 * In case of conflicting keys with the remotely decoded CC details,
1524 * attempts to replace the masked numbers with asterisks.
1525 *
1526 * @param string $cc_payload the raw CC details from payment logs.
1527 * @param array $remote_cc_data associative array of decoded CC data.
1528 *
1529 * @return array associative or empty array.
1530 *
1531 * @since 1.16.10 (J) - 1.6.10 (WP) moved from widget Virtual Terminal.
1532 */
1533 protected function parseCreditCardValuePairs($cc_payload, array $remote_cc_data = [])
1534 {
1535 $cc_value_pairs = [];
1536
1537 if (empty($cc_payload)) {
1538 return $cc_value_pairs;
1539 }
1540
1541 $cc_lines = preg_split("/(\r\n|\n|\r)/", $cc_payload);
1542
1543 foreach ($cc_lines as $cc_line) {
1544 if (strpos($cc_line, ':') === false) {
1545 continue;
1546 }
1547
1548 $cc_line_parts = explode(':', $cc_line);
1549
1550 if (empty($cc_line_parts[0]) || !strlen(trim($cc_line_parts[1]))) {
1551 continue;
1552 }
1553
1554 $key = str_replace(' ', '_', strtolower($cc_line_parts[0]));
1555 $value = trim($cc_line_parts[1]);
1556
1557 if (isset($cc_value_pairs[$key])) {
1558 /**
1559 * Do not overwrite existing keys because this probably means that the
1560 * credit card was updated by an OTA like Booking.com, hence the payment
1561 * logs string in VBO may contain the information of two different cards.
1562 * New credit card details are always pre-pended by VCM in the payment logs.
1563 */
1564 continue;
1565 }
1566
1567 if (!empty($remote_cc_data[$key]) && is_string($remote_cc_data[$key]) && strpos($value, '*') !== false) {
1568 // replace masked numbers with remote content
1569 $value = $this->replaceMaskedNumbers($value, $remote_cc_data[$key]);
1570 }
1571
1572 $cc_value_pairs[$key] = $value;
1573 }
1574
1575 return $cc_value_pairs;
1576 }
1577
1578 /**
1579 * Given a local and a remote credit card number string with
1580 * masked symbols, replaces the values in the corresponding
1581 * positions with the unmasked numbers.
1582 *
1583 * @param string $local current string with masked values.
1584 * @param string $remote remote string with unmasked values.
1585 *
1586 * @return string the local string with unmasked values.
1587 *
1588 * @since 1.16.10 (J) - 1.6.10 (WP) moved from widget Virtual Terminal.
1589 */
1590 protected function replaceMaskedNumbers($local, $remote)
1591 {
1592 // split anything but numbers
1593 $numbers = preg_split("/([^0-9]+)/", trim($remote));
1594
1595 if ($numbers) {
1596 // filter empty values
1597 $numbers = array_filter($numbers);
1598 }
1599
1600 if (!$numbers) {
1601 // unable to proceed
1602 return $local;
1603 }
1604
1605 // split anything but stars (asterisks)
1606 $stars = preg_split("/([^\*]+)/", trim($local));
1607
1608 if ($stars) {
1609 // filter empty values
1610 $stars = array_filter($stars);
1611 }
1612
1613 if (!$stars) {
1614 // unable to proceed
1615 return $local;
1616 }
1617
1618 // replace masked symbols with numbers at their first occurrence
1619 foreach ($numbers as $k => $unmasked) {
1620 if (!isset($stars[$k])) {
1621 continue;
1622 }
1623
1624 $masked_pos = strpos($local, $stars[$k]);
1625
1626 if ($masked_pos === false) {
1627 continue;
1628 }
1629
1630 $local = substr_replace($local, $unmasked, $masked_pos, strlen($stars[$k]));
1631 }
1632
1633 // return the string with possibly unmasked values
1634 return $local;
1635 }
1636
1637 /**
1638 * Tells whether the booking can be created. By default this
1639 * is only allowed from the administrator section of the site.
1640 *
1641 * @return bool
1642 */
1643 protected function canCreate()
1644 {
1645 return $this->get('_isAdministrator') || JFactory::getApplication()->isClient('administrator');
1646 }
1647
1648 /**
1649 * Gets and sets the tariff ID if a rate plan was set.
1650 *
1651 * @return int the tariff ID or 0.
1652 */
1653 protected function loadTariffID()
1654 {
1655 $dbo = JFactory::getDbo();
1656
1657 $id_tariff = 0;
1658 $room = $this->getRoom();
1659 $daysdiff = (int)$this->get('nights', 1);
1660
1661 if (!empty($room['id']) && !empty($room['id_price']) && !empty($room['room_cost']) && $room['room_cost'] > 0 && !(int)$this->get('set_closed') && !$this->get('split_stay', [])) {
1662 $room['id_price'] = (int)$room['id_price'];
1663
1664 $q = "SELECT `id` FROM `#__vikbooking_dispcost` WHERE `idroom`={$room['id']} AND `days`={$daysdiff} AND `idprice`={$room['id_price']};";
1665 $dbo->setQuery($q);
1666 $id_tariff = $dbo->loadResult();
1667 }
1668
1669 $this->set('id_tariff', (int)$id_tariff);
1670
1671 return (int)$id_tariff;
1672 }
1673
1674 /**
1675 * Applies the turnover time to the checkout timestamp and sets its value.
1676 *
1677 * @return int the turnover seconds applied.
1678 */
1679 protected function applyTurnover()
1680 {
1681 $turnover_secs = 0;
1682 $checkout = $this->get('checkout', 0);
1683
1684 if ($checkout) {
1685 // turnover time
1686 $turnover_secs = VikBooking::getHoursRoomAvail() * 3600;
1687
1688 $this->set('checkout_real', ($checkout + $turnover_secs));
1689 }
1690
1691 $this->set('turnover_secs', $turnover_secs);
1692
1693 return $turnover_secs;
1694 }
1695
1696 /**
1697 * Returns the details of a specific room ID.
1698 *
1699 * @param int $rid the room ID.
1700 *
1701 * @return array the record found or empty array.
1702 */
1703 protected function getRoomDetails($rid = null)
1704 {
1705 $all_rooms = VikBooking::getAvailabilityInstance()->loadRooms();
1706
1707 if (!$rid) {
1708 $inj_room = $this->getRoom();
1709 if (!empty($inj_room['id'])) {
1710 $rid = $inj_room['id'];
1711 }
1712 }
1713
1714 if ($rid && isset($all_rooms[$rid])) {
1715 return $all_rooms[$rid];
1716 }
1717
1718 return [];
1719 }
1720
1721 /**
1722 * Gets the list of rooms involved in the reservation in case of closures.
1723 *
1724 * @return array the list of rooms involved
1725 */
1726 protected function getRoomsPool()
1727 {
1728 $room = $this->getRoom();
1729 if (empty($room['id'])) {
1730 return [];
1731 }
1732 $room = $this->getRoomDetails($room['id']);
1733
1734 // gather values
1735 $set_close_others = (array)$this->get('close_others', []);
1736 $split_stay_data = $this->get('split_stay', []);
1737 $set_closed = (int)$this->get('set_closed');
1738 $turnover_secs = $this->get('turnover_secs', 0);
1739 $hcheckin = $this->get('checkin_h', 12);
1740 $mcheckin = $this->get('checkin_m', 0);
1741 $hcheckout = $this->get('checkout_h', 10);
1742 $mcheckout = $this->get('checkout_m', 0);
1743
1744 $av_helper = VikBooking::getAvailabilityInstance();
1745 $all_rooms = $av_helper->loadRooms();
1746 $rooms_pool = [];
1747 $closeothers = [];
1748
1749 if ($set_close_others && $set_closed) {
1750 // prepend current room for closing
1751 array_unshift($set_close_others, $room['id']);
1752 }
1753 $set_close_others = array_unique($set_close_others);
1754
1755 foreach ($set_close_others as $closeid) {
1756 if (empty($closeid)) {
1757 continue;
1758 }
1759 if ((int)$closeid === -1) {
1760 // close all rooms
1761 $closeothers = [];
1762 foreach ($all_rooms as $cr) {
1763 array_push($closeothers, $cr);
1764 }
1765 break;
1766 }
1767 foreach ($all_rooms as $cr) {
1768 if ((int)$cr['id'] == (int)$closeid) {
1769 // push the main room or one of the other rooms requested for closure
1770 array_push($closeothers, $cr);
1771 break;
1772 }
1773 }
1774 }
1775
1776 if (!$closeothers || !$set_closed) {
1777 $rooms_pool = [$room];
1778 } else {
1779 $rooms_pool = $closeothers;
1780 }
1781
1782 // check split stay rooms booking
1783 if (!empty($split_stay_data)) {
1784 // reset pool and set it with the split stay rooms
1785 $rooms_pool = [];
1786 foreach ($split_stay_data as $sps_k => $split_stay) {
1787 if (!isset($all_rooms[$split_stay['idroom']])) {
1788 continue;
1789 }
1790 // calculate and set the exact check-in and check-out timestamps for this split-room
1791 $split_stay['checkin_ts'] = VikBooking::getDateTimestamp($split_stay['checkin'], $hcheckin, $mcheckin);
1792 $split_stay['checkout_ts'] = VikBooking::getDateTimestamp($split_stay['checkout'], $hcheckout, $mcheckout);
1793 $split_stay['realback_ts'] = $turnover_secs + $split_stay['checkout_ts'];
1794 $split_stay['nights'] = $av_helper->countNightsOfStay($split_stay['checkin_ts'], $split_stay['checkout_ts']);
1795 $split_stay_data[$sps_k] = $split_stay;
1796 // push room data to pool after storing additional information
1797 $room_data = $all_rooms[$split_stay['idroom']];
1798 $room_data['checkin_ts'] = $split_stay['checkin_ts'];
1799 $room_data['checkout_ts'] = $split_stay['checkout_ts'];
1800 $rooms_pool[] = $room_data;
1801 }
1802 if (!$rooms_pool) {
1803 $this->setError('No valid rooms for the split stay booking');
1804 return [];
1805 }
1806 // update split stay data manipulated
1807 $this->set('split_stay', $split_stay_data);
1808 }
1809
1810 return $rooms_pool;
1811 }
1812
1813 /**
1814 * Checks that the room is available on the requested dates.
1815 * Call this method only after getting the rooms pool.
1816 *
1817 * @return bool
1818 */
1819 protected function isRoomAvailable()
1820 {
1821 $inj_room = $this->getRoom();
1822 if (empty($inj_room['id'])) {
1823 return false;
1824 }
1825 $room = $this->getRoomDetails($inj_room['id']);
1826 if (!$room) {
1827 return false;
1828 }
1829
1830 $split_stay_data = $this->get('split_stay', []);
1831 $set_closed = $this->get('set_closed', 0);
1832 $num_rooms = $this->get('num_rooms', 1);
1833
1834 $room_available = true;
1835 $all_rooms = VikBooking::getAvailabilityInstance()->loadRooms();
1836
1837 if (empty($split_stay_data)) {
1838 // make sure the rooms are available
1839 $check_units = $room['units'];
1840 if ($num_rooms > 1 && $num_rooms <= $room['units'] && !$set_closed) {
1841 // only when non closing the room we check the availability for the units requested for booking
1842 $check_units = $room['units'] - $num_rooms + 1;
1843 }
1844 $room_available = VikBooking::roomBookable($room['id'], $check_units, $this->get('checkin', 0), $this->get('checkout', 0));
1845 } else {
1846 // make sure the rooms for the split stay are available
1847 foreach ($split_stay_data as $split_stay) {
1848 if (!isset($all_rooms[$split_stay['idroom']])) {
1849 $room_available = false;
1850 break;
1851 }
1852 $room_available = $room_available && VikBooking::roomBookable($split_stay['idroom'], $all_rooms[$split_stay['idroom']]['units'], $split_stay['checkin_ts'], $split_stay['checkout_ts']);
1853 }
1854 }
1855
1856 return $room_available;
1857 }
1858
1859 /**
1860 * In case the reservation is forced or is a closure, we detect the
1861 * forced reason to eventually attach it to the booking history.
1862 *
1863 * @param bool $room_available whether the room is available.
1864 *
1865 * @return void
1866 */
1867 protected function detectForcedReason($room_available = true)
1868 {
1869 $split_stay_data = $this->get('split_stay', []);
1870 $force_booking = $this->get('force_booking', 0);
1871 $set_closed = $this->get('set_closed', 0);
1872
1873 $forced_reason = $this->get('forced_reason', '');
1874 $all_rooms = VikBooking::getAvailabilityInstance()->loadRooms();
1875
1876 if (empty($split_stay_data)) {
1877 // eventually build string for the description of the history event
1878 if (($force_booking || $set_closed) && !$room_available) {
1879 $forced_reason = JText::translate('VBO_FORCED_BOOKDATES');
1880 }
1881 } else {
1882 // set "split stay" as the description of the history event
1883 $forced_reason = JText::translate('VBO_SPLIT_STAY') . "\n";
1884 foreach ($split_stay_data as $sps_k => $split_stay) {
1885 // describe the split stay for each room
1886 if (!isset($all_rooms[$split_stay['idroom']])) {
1887 continue;
1888 }
1889 $room_stay_nights = $split_stay['nights'];
1890 $forced_reason .= $all_rooms[$split_stay['idroom']]['name'] . ': ' . $room_stay_nights . ' ' . ($room_stay_nights > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY')) . ', ';
1891 $forced_reason .= $split_stay['checkin'] . ' - ' . $split_stay['checkout'] . "\n";
1892 }
1893 $forced_reason = rtrim($forced_reason, "\n");
1894 }
1895
1896 $this->set('forced_reason', $forced_reason);
1897 }
1898
1899 /**
1900 * Stores the customer information to a new or existing record.
1901 * In case of success, the customer ID property is updated.
1902 * The customer shall be stored before the reservation records.
1903 *
1904 * @return bool
1905 */
1906 protected function storeCustomer()
1907 {
1908 $dbo = JFactory::getDbo();
1909
1910 $inj_customer = $this->getCustomer();
1911 $first_name = !empty($inj_customer['first_name']) ? $inj_customer['first_name'] : '';
1912 $last_name = !empty($inj_customer['last_name']) ? $inj_customer['last_name'] : '';
1913 $custdata = !empty($inj_customer['data']) ? $inj_customer['data'] : '';
1914 $email = !empty($inj_customer['email']) ? $inj_customer['email'] : '';
1915 $country = !empty($inj_customer['country']) ? $inj_customer['country'] : '';
1916 $phone = !empty($inj_customer['phone']) ? $inj_customer['phone'] : '';
1917
1918 // custom fields
1919 $q = "SELECT * FROM `#__vikbooking_custfields` ORDER BY `ordering` ASC;";
1920 $dbo->setQuery($q);
1921 $all_cfields = $dbo->loadAssocList();
1922
1923 $customer_cfields = [];
1924 $customer_extrainfo = [];
1925 $custdata_parts = explode("\n", $custdata);
1926 foreach ($custdata_parts as $cdataline) {
1927 if (!strlen(trim($cdataline))) {
1928 continue;
1929 }
1930 $cdata_parts = explode(':', $cdataline);
1931 if (count($cdata_parts) < 2 || !strlen(trim($cdata_parts[0])) || !strlen(trim($cdata_parts[1]))) {
1932 continue;
1933 }
1934 foreach ($all_cfields as $cf) {
1935 $needle = JText::translate($cf['name']);
1936 if (!empty($needle) && strpos($cdata_parts[0], $needle) !== false && !array_key_exists($cf['id'], $customer_cfields) && $cf['type'] != 'country') {
1937 $user_input_val = trim($cdata_parts[1]);
1938 $customer_cfields[$cf['id']] = $user_input_val;
1939 if (!empty($cf['flag'])) {
1940 $customer_extrainfo[$cf['flag']] = $user_input_val;
1941 } elseif ($cf['type'] == 'state') {
1942 $customer_extrainfo['state'] = $user_input_val;
1943 }
1944 break;
1945 }
1946 }
1947 }
1948
1949 $cpin = VikBooking::getCPinInstance();
1950 $cpin->is_admin = true;
1951 $cpin->setCustomerExtraInfo($customer_extrainfo);
1952 $cpin->saveCustomerDetails($first_name, $last_name, $email, $phone, $country, $customer_cfields);
1953
1954 $customer_id = $cpin->getNewCustomerId();
1955 if (!$customer_id) {
1956 return false;
1957 }
1958
1959 $inj_customer['id'] = $customer_id;
1960 $this->setCustomer($inj_customer);
1961
1962 return true;
1963 }
1964
1965 /**
1966 * Returns the calculated total booking amount and total taxes.
1967 * Sets the necessary properties with the calculated amounts.
1968 *
1969 * @return array list of booking total amount and total taxes.
1970 */
1971 protected function calculateTotal()
1972 {
1973 // the values to calculate
1974 $set_total = 0;
1975 $set_taxes = 0;
1976
1977 $dbo = JFactory::getDbo();
1978
1979 // get data
1980 $inj_room = $this->getRoom();
1981 $set_closed = $this->get('set_closed', 0);
1982 $daysdiff = (int)$this->get('nights', 1);
1983 $num_rooms = (int)$this->get('num_rooms', 1);
1984 $totalpnight = !empty($inj_room['total_or_pnight']) ? $inj_room['total_or_pnight'] : 'total';
1985 $cust_cost = !empty($inj_room['cust_cost']) ? (float)$inj_room['cust_cost'] : 0;
1986 $room_cost = !empty($inj_room['room_cost']) ? (float)$inj_room['room_cost'] : 0;
1987 $id_price = !empty($inj_room['id_price']) ? (int)$inj_room['id_price'] : 0;
1988 $id_tax = !empty($inj_room['id_tax']) ? (int)$inj_room['id_tax'] : 0;
1989
1990 $split_stay_data = $this->get('split_stay', []);
1991
1992 $room = $this->getRoomDetails();
1993 if (!$room) {
1994 return [$set_total, $set_taxes];
1995 }
1996
1997 if ($cust_cost > 0 && !$set_closed) {
1998 // custom cost can be per night
1999 if ($totalpnight == 'pnight') {
2000 $cust_cost = $cust_cost * $daysdiff;
2001 }
2002 $set_total = $cust_cost;
2003
2004 if (!$id_tax && ($inj_room['guess_tax'] ?? false)) {
2005 // try to guess the tax rate
2006 $dbo->setQuery(
2007 $dbo->getQuery(true)
2008 ->select($dbo->qn('id'))
2009 ->from($dbo->qn('#__vikbooking_iva'))
2010 ->order($dbo->qn('aliq') . ' ASC'),
2011 0, 1);
2012 $guessed_id_tax = $dbo->loadResult();
2013 if ($guessed_id_tax) {
2014 // update the id_tax values
2015 $id_tax = $guessed_id_tax;
2016 $inj_room['id_tax'] = $guessed_id_tax;
2017 $this->setRoom($inj_room);
2018 }
2019 }
2020
2021 // apply taxes, if necessary
2022 if ($id_tax) {
2023 $dbo->setQuery(
2024 $dbo->getQuery(true)
2025 ->select([
2026 $dbo->qn('i.aliq'),
2027 $dbo->qn('i.taxcap'),
2028 ])
2029 ->from($dbo->qn('#__vikbooking_iva', 'i'))
2030 ->where($dbo->qn('i.id') . ' = ' . (int) $id_tax),
2031 0, 1);
2032 $taxdata = $dbo->loadAssoc();
2033 if ($taxdata) {
2034 $aliq = $taxdata['aliq'];
2035 if (floatval($aliq) > 0.00) {
2036 if (!VikBooking::ivaInclusa()) {
2037 // add tax to the total amount
2038 $subt = 100 + (float)$aliq;
2039 $set_total = ($set_total * $subt / 100);
2040 /**
2041 * Tax Cap implementation for prices tax excluded (most common).
2042 *
2043 * @since 1.12 (J) - 1.2 (WP)
2044 */
2045 if ($taxdata['taxcap'] > 0 && ($set_total - $cust_cost) > $taxdata['taxcap']) {
2046 $set_total = ($cust_cost + $taxdata['taxcap']);
2047 }
2048 // calculate tax
2049 $set_taxes = $set_total - $cust_cost;
2050 } else {
2051 // calculate tax
2052 $cost_minus_tax = VikBooking::sayPackageMinusIva($cust_cost, $id_tax);
2053 $set_taxes += ($cust_cost - $cost_minus_tax);
2054 }
2055 }
2056 }
2057 }
2058 } elseif (!empty($id_price) && $room_cost > 0 && !$set_closed && empty($split_stay_data)) {
2059 // one website rate plan was selected, so we calculate total and taxes
2060 $set_total = $room_cost;
2061
2062 // find tax rate assigned to this rate plan
2063 $q = "SELECT `p`.`id`,`p`.`idiva`,`i`.`aliq`,`i`.`taxcap` FROM `#__vikbooking_prices` AS `p` LEFT JOIN `#__vikbooking_iva` AS `i` ON `p`.`idiva`=`i`.`id` WHERE `p`.`id`=" . $id_price . ";";
2064 $dbo->setQuery($q);
2065 $taxdata = $dbo->loadAssoc();
2066 if ($taxdata) {
2067 $aliq = $taxdata['aliq'];
2068 if (floatval($aliq) > 0.00) {
2069 if (!VikBooking::ivaInclusa()) {
2070 // add tax to the total amount
2071 $subt = 100 + (float)$aliq;
2072 $set_total = ($set_total * $subt / 100);
2073 /**
2074 * Tax Cap implementation for prices tax excluded (most common).
2075 *
2076 * @since 1.12 (J) - 1.2 (WP)
2077 */
2078 if ($taxdata['taxcap'] > 0 && ($set_total - $room_cost) > $taxdata['taxcap']) {
2079 $set_total = ($room_cost + $taxdata['taxcap']);
2080 }
2081 // calculate tax
2082 $set_taxes = $set_total - $room_cost;
2083 } else {
2084 // calculate tax
2085 $cost_minus_tax = VikBooking::sayPackageMinusIva($room_cost, $taxdata['idiva']);
2086 $set_taxes += ($room_cost - $cost_minus_tax);
2087 }
2088 }
2089 }
2090
2091 // total and taxes should be multiplied by the number of rooms booked when using a website rate plan
2092 if ($set_closed) {
2093 $set_total *= $room['units'];
2094 $set_taxes *= $room['units'];
2095 } elseif ($num_rooms > 1 && $num_rooms <= $room['units']) {
2096 $set_total *= $num_rooms;
2097 $set_taxes *= $num_rooms;
2098 }
2099 }
2100
2101 // set values
2102 $this->set('_total', $set_total);
2103 $this->set('_total_tax', $set_taxes);
2104
2105 return [$set_total, $set_taxes];
2106 }
2107
2108 /**
2109 * Stores the booking and room-booking records.
2110 * If no errors, the newly generated booking id is set.
2111 *
2112 * @param array $rooms_pool list of rooms involved.
2113 *
2114 * @return bool
2115 */
2116 protected function storeReservationRecords(array $rooms_pool)
2117 {
2118 $dbo = JFactory::getDbo();
2119
2120 $set_closed = $this->get('set_closed', 0);
2121 $units_closed = $this->get('units_closed', 0);
2122 $daysdiff = (int) $this->get('nights', 1);
2123 $num_rooms = (int) $this->get('num_rooms', 1);
2124 $adults = (int) $this->get('adults', 1);
2125 $children = (int) $this->get('children', 0);
2126 $children_age = (array) $this->get('children_age', []);
2127 $status = $this->get('status', 'confirmed');
2128
2129 $split_stay_data = $this->get('split_stay', []);
2130 $room = $this->getRoomDetails();
2131 if (!$room || !$rooms_pool) {
2132 return false;
2133 }
2134
2135 // get current Joomla/WordPress User ID
2136 $now_user = JFactory::getUser();
2137 $store_ujid = property_exists($now_user, 'id') ? (int)$now_user->id : 0;
2138
2139 // forced booking reason, status validation and additional data
2140 $forced_reason = $this->get('forced_reason', '');
2141 $valid_statuses = ['confirmed', 'standby'];
2142 $status = in_array($status, $valid_statuses) ? $status : 'confirmed';
2143 $paymentmeth = $this->get('id_payment', '');
2144 $set_total = (float) $this->get('_total', 0);
2145 $set_taxes = (float) $this->get('_total_tax', 0);
2146
2147 // stay dates
2148 $now_ts = time();
2149 $checkin_ts = $this->get('checkin');
2150 $checkout_ts = $this->get('checkout');
2151 $realback_ts = $this->get('checkout_real', $checkout_ts);
2152
2153 // room
2154 $inj_room = $this->getRoom();
2155 $cust_cost = !empty($inj_room['cust_cost']) ? (float) $inj_room['cust_cost'] : 0;
2156 $room_cost = !empty($inj_room['room_cost']) ? (float) $inj_room['room_cost'] : 0;
2157 $id_price = !empty($inj_room['id_price']) ? (int) $inj_room['id_price'] : 0;
2158 $id_tax = !empty($inj_room['id_tax']) ? (int) $inj_room['id_tax'] : 0;
2159 $id_tariff = $this->get('id_tariff', 0);
2160
2161 // custom rate modifier per night
2162 $totalpnight = !empty($inj_room['total_or_pnight']) ? $inj_room['total_or_pnight'] : 'total';
2163 if ($cust_cost > 0.00 && !$set_closed && $totalpnight == 'pnight') {
2164 $cust_cost = $cust_cost * $daysdiff;
2165 }
2166
2167 // customer
2168 $cpin = VikBooking::getCPinInstance();
2169 $inj_customer = $this->getCustomer();
2170 $customer_id = !empty($inj_customer['id']) ? $inj_customer['id'] : 0;
2171 $customer_pin = !empty($inj_customer['pin']) ? $inj_customer['pin'] : '';
2172 $t_first_name = !empty($inj_customer['first_name']) ? $inj_customer['first_name'] : '';
2173 $t_last_name = !empty($inj_customer['last_name']) ? $inj_customer['last_name'] : '';
2174 $customer_data = !empty($inj_customer['data']) ? $inj_customer['data'] : '';
2175 $customer_email = !empty($inj_customer['email']) ? $inj_customer['email'] : '';
2176 $country_code = !empty($inj_customer['country']) ? $inj_customer['country'] : '';
2177 $phone_number = !empty($inj_customer['phone']) ? $inj_customer['phone'] : '';
2178
2179 if ($set_closed) {
2180 $customer_data = JText::translate('VBDBTEXTROOMCLOSED');
2181 }
2182
2183 // check for default customer raw data
2184 if (!$customer_data && $t_first_name) {
2185 // build a default raw data string
2186 $customer_data = "Name: {$t_first_name}\n";
2187 if ($t_last_name) {
2188 $customer_data .= "Last Name: {$t_last_name}\n";
2189 }
2190 if ($customer_email) {
2191 $customer_data .= "eMail: {$customer_email}\n";
2192 }
2193 if ($country_code) {
2194 $customer_data .= "Country: {$country_code}\n";
2195 }
2196 if ($phone_number) {
2197 $customer_data .= "Phone: {$phone_number}\n";
2198 }
2199 $customer_data = rtrim($customer_data, "\n");
2200 }
2201
2202 // generate booking SID
2203 $sid = VikBooking::getSecretLink();
2204
2205 // assign room specific unit
2206 $set_room_indexes = !$set_closed ? VikBooking::autoRoomUnit() : false;
2207 $num_rooms = $num_rooms > 0 ? $num_rooms : 1;
2208
2209 // occupancy and loop limits
2210 $forend = 1;
2211 $or_forend = 1;
2212 $adults_map = [];
2213 $children_map = [];
2214 if ($set_closed && empty($split_stay_data)) {
2215 $forend = $room['units'];
2216 } elseif ($num_rooms > 1 && $num_rooms <= $room['units'] && empty($split_stay_data)) {
2217 $forend = $num_rooms;
2218 $or_forend = $num_rooms;
2219 // assign adults/children proportionally
2220 if (($adults + $children) < $num_rooms) {
2221 // the number of guests does not make much sense but we build the maps anyway
2222 for ($r = 1; $r <= $or_forend; $r++) {
2223 $adults_map[$r] = $adults;
2224 $children_map[$r] = $children;
2225 }
2226 } else {
2227 $adults_per_room = floor(($adults / $num_rooms));
2228 $adults_left = ($adults % $num_rooms);
2229 $children_per_room = floor(($children / $num_rooms));
2230 $children_left = ($children % $num_rooms);
2231 for ($r = 1; $r <= $or_forend; $r++) {
2232 $adults_map[$r] = $adults_per_room;
2233 $children_map[$r] = $children_per_room;
2234 if ($r == $or_forend) {
2235 $adults_map[$r] += $adults_left;
2236 $children_map[$r] += $children_left;
2237 }
2238 }
2239 }
2240 }
2241
2242 // count total rooms booked
2243 $totalrooms = ($set_closed && $status == 'confirmed' ? count($rooms_pool) : ($num_rooms > 1 && $num_rooms <= $room['units'] ? $num_rooms : 1));
2244 $totalrooms = !empty($split_stay_data) ? count($split_stay_data) : $totalrooms;
2245
2246 // prepare booking record
2247 $booking = new stdClass;
2248 $booking->custdata = $customer_data;
2249 $booking->ts = $now_ts;
2250 $booking->status = $status;
2251 $booking->days = $daysdiff;
2252 $booking->checkin = $checkin_ts;
2253 $booking->checkout = $checkout_ts;
2254 $booking->custmail = $customer_email;
2255 $booking->sid = $sid;
2256 $booking->idpayment = $paymentmeth;
2257 $booking->ujid = (int)$store_ujid;
2258 $booking->roomsnum = $totalrooms;
2259 $booking->total = $set_total > 0 ? $set_total : null;
2260 if ($this->get('admin_notes')) {
2261 $booking->adminnotes = $this->get('admin_notes', '');
2262 }
2263 $booking->lang = VikBooking::guessBookingLangFromCountry($country_code);
2264 $booking->country = $country_code;
2265 $booking->tot_taxes = $set_taxes > 0 ? $set_taxes : null;
2266 $booking->phone = $phone_number;
2267 $booking->closure = ($status == 'standby' ? 0 : ($set_closed || $units_closed ? 1 : 0));
2268 $booking->split_stay = !empty($split_stay_data) ? 1 : 0;
2269
2270 // store reservation
2271 if ($status == 'confirmed') {
2272 // occupy the rooms
2273 $insertedbusy = [];
2274 if (empty($split_stay_data)) {
2275 // only when closing other rooms we have an array containing multiple rooms info
2276 foreach ($rooms_pool as $nowroom) {
2277 $nowforend = $set_closed ? $nowroom['units'] : $forend;
2278 for ($b = 1; $b <= $nowforend; $b++) {
2279 $busy_record = new stdClass;
2280 $busy_record->idroom = (int)$nowroom['id'];
2281 $busy_record->checkin = (int)$checkin_ts;
2282 $busy_record->checkout = (int)$checkout_ts;
2283 $busy_record->realback = (int)$realback_ts;
2284
2285 // store busy record
2286 $dbo->insertObject('#__vikbooking_busy', $busy_record, 'id');
2287
2288 if (isset($busy_record->id)) {
2289 $insertedbusy[] = $busy_record->id;
2290 }
2291 }
2292 }
2293 } else {
2294 // for split stay bookings we occupy the rooms on the individual stay dates
2295 foreach ($split_stay_data as $split_stay) {
2296 $busy_record = new stdClass;
2297 $busy_record->idroom = (int)$split_stay['idroom'];
2298 $busy_record->checkin = (int)$split_stay['checkin_ts'];
2299 $busy_record->checkout = (int)$split_stay['checkout_ts'];
2300 $busy_record->realback = (int)$split_stay['realback_ts'];
2301
2302 // store busy record
2303 $dbo->insertObject('#__vikbooking_busy', $busy_record, 'id');
2304
2305 if (isset($busy_record->id)) {
2306 $insertedbusy[] = $busy_record->id;
2307 }
2308 }
2309 }
2310
2311 if (!$insertedbusy) {
2312 $this->setError('No records were occupied');
2313 return false;
2314 }
2315
2316 // store booking record
2317 $dbo->insertObject('#__vikbooking_orders', $booking, 'id');
2318
2319 if (!isset($booking->id)) {
2320 $this->setError('Could not store the reservation record');
2321 return false;
2322 }
2323
2324 // get the newly generated booking ID
2325 $newoid = $booking->id;
2326
2327 // set the new booking ID
2328 $this->setNewBookingID($newoid);
2329
2330 if (!empty($split_stay_data)) {
2331 // save transient on db for split stay information
2332 VBOFactory::getConfig()->set('split_stay_' . $newoid, json_encode($split_stay_data));
2333 }
2334
2335 // check if some of the rooms booked have shared calendars
2336 VikBooking::updateSharedCalendars($newoid, [$room['id']], $checkin_ts, $checkout_ts);
2337
2338 // confirmation number
2339 $confirmnumber = VikBooking::generateConfirmNumber($newoid, true);
2340
2341 // store busy records/booking relations
2342 foreach ($insertedbusy as $lid) {
2343 $obusy_record = new stdClass;
2344 $obusy_record->idorder = (int)$newoid;
2345 $obusy_record->idbusy = (int)$lid;
2346
2347 // store busy relation record
2348 $dbo->insertObject('#__vikbooking_ordersbusy', $obusy_record, 'id');
2349 }
2350
2351 // write room records
2352 foreach ($rooms_pool as $rind => $nowroom) {
2353 $room_indexes_usemap = [];
2354 for ($r = 1; $r <= $or_forend; $r++) {
2355 // Assign room specific unit
2356 $info_room_avail = [
2357 'id' => $newoid,
2358 'checkin' => (!empty($nowroom['checkin_ts']) ? $nowroom['checkin_ts'] : $checkin_ts),
2359 'checkout' => (!empty($nowroom['checkout_ts']) ? $nowroom['checkout_ts'] : $checkout_ts),
2360 ];
2361 $room_indexes = $set_room_indexes === true ? VikBooking::getRoomUnitNumsAvailable($info_room_avail, $nowroom['id']) : [];
2362 $use_ind_key = 0;
2363 if ($room_indexes) {
2364 if (!array_key_exists($nowroom['id'], $room_indexes_usemap)) {
2365 $room_indexes_usemap[$nowroom['id']] = $use_ind_key;
2366 } else {
2367 $use_ind_key = $room_indexes_usemap[$nowroom['id']];
2368 }
2369 }
2370
2371 // room custom cost
2372 $or_cust_cost = $cust_cost > 0.00 ? $cust_cost : 0;
2373 $or_cust_cost = $or_forend > 1 && $or_cust_cost > 0 ? round(($or_cust_cost / $or_forend), 2) : $or_cust_cost;
2374 // room cost from website rate plan is always based on one room
2375 $or_room_cost = $room_cost > 0.00 ? $room_cost : 0;
2376 if (!empty($split_stay_data) && $cust_cost > 0) {
2377 // set the average cost per room in case of split stay
2378 $cost_per_room = ($cust_cost / count($split_stay_data));
2379 $or_cust_cost = round($cost_per_room, 2);
2380 if (isset($split_stay_data[$rind]) && isset($split_stay_data[$rind]['nights'])) {
2381 // count the average cost per room depending on the number of nights of stay
2382 $cost_per_room = $cust_cost / $daysdiff * $split_stay_data[$rind]['nights'];
2383 $or_cust_cost = round($cost_per_room, 2);
2384 }
2385 }
2386
2387 // room guests
2388 $room_adults = isset($adults_map[$r]) && empty($split_stay_data) ? $adults_map[$r] : $adults;
2389 $room_children = isset($children_map[$r]) && empty($split_stay_data) ? $children_map[$r] : $children;
2390
2391 // attempt to gather the children age for this room
2392 $room_children_age = null;
2393 if ($room_children && $children_age) {
2394 $children_age_pool = [];
2395 for ($ic = 0; $ic < $room_children; $ic++) {
2396 if (!$children_age) {
2397 $children_age_pool[] = 0;
2398 continue;
2399 }
2400 // shorten the list and push the current child age
2401 $current_child_age = array_shift($children_age);
2402 $children_age_pool[] = (int) $current_child_age;
2403 }
2404 $room_children_age = json_encode(['age' => $children_age_pool]);
2405 }
2406
2407 // store room record
2408 $room_record = new stdClass;
2409 $room_record->idorder = (int) $newoid;
2410 $room_record->idroom = (int) $nowroom['id'];
2411 $room_record->adults = $room_adults;
2412 $room_record->children = $room_children;
2413 $room_record->idtar = !empty($id_tariff) ? $id_tariff : null;
2414 $room_record->childrenage = $room_children_age;
2415 $room_record->t_first_name = $t_first_name;
2416 $room_record->t_last_name = $t_last_name;
2417 $room_record->roomindex = count($room_indexes) ? (int) $room_indexes[$use_ind_key] : null;
2418 $room_record->cust_cost = $cust_cost > 0.00 ? $or_cust_cost : null;
2419 $room_record->cust_idiva = $cust_cost > 0.00 && !empty($id_tax) ? $id_tax : null;
2420 $room_record->room_cost = $room_cost > 0.00 ? $or_room_cost : null;
2421
2422 $dbo->insertObject('#__vikbooking_ordersrooms', $room_record, 'id');
2423
2424 if (!isset($room_record->id)) {
2425 $this->setError('Could not store room reservation record for booking ID ' . $room_record->idorder);
2426 continue;
2427 }
2428
2429 // Assign room specific unit
2430 if ($room_indexes) {
2431 $room_indexes_usemap[$nowroom['id']]++;
2432 }
2433 }
2434 }
2435 } elseif ($status == 'standby') {
2436 // store booking record
2437 $dbo->insertObject('#__vikbooking_orders', $booking, 'id');
2438
2439 if (!isset($booking->id)) {
2440 $this->setError('Could not store the reservation record');
2441 return false;
2442 }
2443
2444 // get the newly generated booking ID
2445 $newoid = $booking->id;
2446
2447 // set the new booking ID
2448 $this->setNewBookingID($newoid);
2449
2450 if (!empty($split_stay_data)) {
2451 // save transient on db for split stay information
2452 VBOFactory::getConfig()->set('split_stay_' . $newoid, json_encode($split_stay_data));
2453 }
2454
2455 // write room records
2456 foreach ($rooms_pool as $rind => $nowroom) {
2457 for ($r = 1; $r <= $or_forend; $r++) {
2458 // room custom cost
2459 $or_cust_cost = $cust_cost > 0.00 ? $cust_cost : 0;
2460 $or_cust_cost = $or_forend > 1 && $or_cust_cost > 0 ? round(($or_cust_cost / $or_forend), 2) : $or_cust_cost;
2461 // room cost from website rate plan is always based on one room
2462 $or_room_cost = $room_cost > 0.00 ? $room_cost : 0;
2463 if (!empty($split_stay_data) && $cust_cost > 0) {
2464 // set the average cost per room in case of split stay
2465 $cost_per_room = ($cust_cost / count($split_stay_data));
2466 $or_cust_cost = round($cost_per_room, 2);
2467 if (isset($split_stay_data[$rind]) && isset($split_stay_data[$rind]['nights'])) {
2468 // count the average cost per room depending on the number of nights of stay
2469 $cost_per_room = $cust_cost / $daysdiff * $split_stay_data[$rind]['nights'];
2470 $or_cust_cost = round($cost_per_room, 2);
2471 }
2472 }
2473
2474 // room guests
2475 $room_adults = isset($adults_map[$r]) && empty($split_stay_data) ? $adults_map[$r] : $adults;
2476 $room_children = isset($children_map[$r]) && empty($split_stay_data) ? $children_map[$r] : $children;
2477
2478 // attempt to gather the children age for this room
2479 $room_children_age = null;
2480 if ($room_children && $children_age) {
2481 $children_age_pool = [];
2482 for ($ic = 0; $ic < $room_children; $ic++) {
2483 if (!$children_age) {
2484 $children_age_pool[] = 0;
2485 continue;
2486 }
2487 // shorten the list and push the current child age
2488 $current_child_age = array_shift($children_age);
2489 $children_age_pool[] = (int) $current_child_age;
2490 }
2491 $room_children_age = json_encode(['age' => $children_age_pool]);
2492 }
2493
2494 // store room record
2495 $room_record = new stdClass;
2496 $room_record->idorder = (int) $newoid;
2497 $room_record->idroom = (int) $nowroom['id'];
2498 $room_record->adults = $room_adults;
2499 $room_record->children = $room_children;
2500 $room_record->idtar = !empty($id_tariff) ? $id_tariff : null;
2501 $room_record->childrenage = $room_children_age;
2502 $room_record->t_first_name = $t_first_name;
2503 $room_record->t_last_name = $t_last_name;
2504 $room_record->cust_cost = $cust_cost > 0.00 ? $or_cust_cost : null;
2505 $room_record->cust_idiva = $cust_cost > 0.00 && !empty($id_tax) ? $id_tax : null;
2506 $room_record->room_cost = $room_cost > 0.00 ? $or_room_cost : null;
2507
2508 $dbo->insertObject('#__vikbooking_ordersrooms', $room_record, 'id');
2509
2510 if (!isset($room_record->id)) {
2511 $this->setError('Could not store room reservation record for booking ID ' . $room_record->idorder);
2512 continue;
2513 }
2514
2515 if (empty($split_stay_data)) {
2516 // lock room for pending status
2517 $tmplock_record = new stdClass;
2518 $tmplock_record->idroom = (int)$room['id'];
2519 $tmplock_record->checkin = $checkin_ts;
2520 $tmplock_record->checkout = $checkout_ts;
2521 $tmplock_record->until = VikBooking::getMinutesLock(true);
2522 $tmplock_record->realback = $realback_ts;
2523 $tmplock_record->idorder = (int)$newoid;
2524
2525 $dbo->insertObject('#__vikbooking_tmplock', $tmplock_record, 'id');
2526 }
2527 }
2528 }
2529
2530 if (!empty($split_stay_data)) {
2531 // lock rooms for pending status on proper stay dates
2532 foreach ($split_stay_data as $split_stay) {
2533 $tmplock_record = new stdClass;
2534 $tmplock_record->idroom = (int)$split_stay['idroom'];
2535 $tmplock_record->checkin = (int)$split_stay['checkin_ts'];
2536 $tmplock_record->checkout = (int)$split_stay['checkout_ts'];
2537 $tmplock_record->until = VikBooking::getMinutesLock(true);
2538 $tmplock_record->realback = (int)$split_stay['realback_ts'];
2539 $tmplock_record->idorder = (int)$newoid;
2540
2541 $dbo->insertObject('#__vikbooking_tmplock', $tmplock_record, 'id');
2542 }
2543 }
2544 }
2545
2546 $newoid = $this->getNewBookingID();
2547 if (!$newoid) {
2548 return false;
2549 }
2550
2551 // assign booking to customer
2552 if (!$cpin->getNewCustomerId() && !empty($customer_id)) {
2553 $cpin->setNewPin($customer_pin);
2554 $cpin->setNewCustomerId($customer_id);
2555 }
2556 $cpin->saveCustomerBooking($newoid);
2557
2558 // Booking History
2559 $history_obj = VikBooking::getBookingHistoryInstance($newoid);
2560 $forced_reason = !empty($forced_reason) ? " {$forced_reason}" : $forced_reason;
2561 $caller_id = $now_user->name ? "({$now_user->name})" : '';
2562 if ($this->getCaller()) {
2563 $caller_id = '(' . $this->getCaller() . ')';
2564 if ($this->getHistoryData()) {
2565 $history_obj->setExtraData($this->getHistoryData());
2566 }
2567 }
2568 $history_obj->store('NB', trim($caller_id . $forced_reason));
2569
2570 if ($status == 'confirmed' || ($status == 'standby' && class_exists('VCMRequestAvailability'))) {
2571 // Invoke Channel Manager
2572 $vcm_autosync = VikBooking::vcmAutoUpdate();
2573 if ($vcm_autosync > 0) {
2574 $vcm_obj = VikBooking::getVcmInvoker();
2575 $vcm_obj->setOids([$newoid])->setSyncType('new');
2576 $sync_result = $vcm_obj->doSync();
2577 if ($sync_result === false) {
2578 // set error message
2579 $vcm_err = $vcm_obj->getError();
2580 $this->setError(JText::translate('VBCHANNELMANAGERRESULTKO') . (!empty($vcm_err) ? ' - ' . $vcm_err : ''));
2581
2582 // return true because the booking was actually stored
2583 return true;
2584 }
2585 } elseif (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'synch.vikbooking.php')) {
2586 // set the necessary action to invoke VCM
2587 $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']);
2588
2589 $this->setChannelManagerAction(JText::translate('VBCHANNELMANAGERINVOKEASK') . ' <button type="button" class="btn btn-primary" onclick="document.location.href=\'' . $vcm_sync_url . '\';">' . JText::translate('VBCHANNELMANAGERSENDRQ') . '</button>');
2590 }
2591 }
2592
2593 if (VikBooking::isAdmin()) {
2594 /**
2595 * Trigger event to allow third party plugins to intercept the admin new booking event.
2596 *
2597 * @since 1.16.8 (J) - 1.6.8 (WP)
2598 */
2599 VBOFactory::getPlatform()->getDispatcher()->trigger('onAfterCreateNewBookingAdmin', [$newoid]);
2600 }
2601
2602 return true;
2603 }
2604 }
2605