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

2,928 lines 95.1 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(true);
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(true);
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 if (($options['custmail'] ?? '') || ($options['customer_email'] ?? '')) {
1004 // update guest email address at booking level
1005 $set_cust_mail = $options['custmail'] ?? $options['customer_email'] ?? '';
1006 if (preg_match("/^[^@]+@[^@]+\.[^@]+$/", $set_cust_mail)) {
1007 // email pattern is safe
1008 $bookingQ->set($dbo->qn('custmail') . ' = ' . $dbo->q(trim($set_cust_mail)));
1009 }
1010 }
1011
1012 // finally, update the booking record
1013 try {
1014 // make sure something to update was set by using the apposite getter magic method
1015 if ($bookingQ->set) {
1016 // some booking record values should be updated
1017 $dbo->setQuery($bookingQ);
1018 $dbo->execute();
1019 }
1020 } catch (Throwable $e) {
1021 $this->setError($e->getMessage());
1022 return false;
1023 }
1024
1025 // update booking history
1026 $history_obj = VikBooking::getBookingHistoryInstance($booking_id);
1027
1028 $now_user = JFactory::getUser();
1029 $caller_id = $now_user->name ? "({$now_user->name})" : '';
1030 if ($this->getCaller()) {
1031 $caller_id = '(' . $this->getCaller() . ')';
1032 if ($this->getHistoryData()) {
1033 $history_obj->setExtraData($this->getHistoryData());
1034 }
1035 }
1036
1037 // update Booking History
1038 $history_obj->store('MB', $caller_id . ($history_descr_rows ? "\n" . implode("\n", $history_descr_rows) : ''));
1039
1040 // check for the operations to perform
1041 if (in_array('shared_calendars', $trigger_operations)) {
1042 // unset any previously booked room due to calendar sharing
1043 VikBooking::cleanSharedCalendarsBusy($booking_id);
1044 // check if some of the rooms booked have shared calendars
1045 VikBooking::updateSharedCalendars($booking_id);
1046 }
1047
1048 if (in_array('vcm_sync', $trigger_operations)) {
1049 // invoke Channel Manager
1050 $vcm_autosync = VikBooking::vcmAutoUpdate();
1051 if ($vcm_autosync > 0) {
1052 $vcm_obj = VikBooking::getVcmInvoker();
1053 $vcm_obj->setOids([$booking_id])->setSyncType('modify')->setOriginalBooking($prev_booking);
1054 $sync_result = $vcm_obj->doSync();
1055 if ($sync_result === false) {
1056 // set error message
1057 $vcm_err = $vcm_obj->getError();
1058 $this->setError(JText::translate('VBCHANNELMANAGERRESULTKO') . (!empty($vcm_err) ? ' - ' . $vcm_err : ''));
1059 }
1060 } elseif (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'synch.vikbooking.php')) {
1061 // set the necessary action to invoke VCM manually
1062 $this->setChannelManagerAction(
1063 JText::translate('VBCHANNELMANAGERINVOKEASK') . ' ' .
1064 '<form action="index.php?option=com_vikbooking" method="post">' .
1065 '<input type="hidden" name="option" value="com_vikbooking"/>' .
1066 '<input type="hidden" name="task" value="invoke_vcm"/>' .
1067 '<input type="hidden" name="stype" value="modify"/>' .
1068 '<input type="hidden" name="cid[]" value="' . $booking_id . '"/>' .
1069 '<input type="hidden" name="origb" value="' . urlencode(json_encode($prev_booking)) . '"/>' .
1070 '<button type="submit" class="btn btn-primary">' . JText::translate('VBCHANNELMANAGERSENDRQ') . '</button>' .
1071 '</form>'
1072 );
1073 }
1074 }
1075
1076 if (($options['ota_reporting'] ?? null) && $diff_stay_dates) {
1077 // perform the OTA reporting action, if allowed
1078 if (class_exists('VCMOtaReporting') && VCMOtaReporting::getInstance($ord)->stayChangeAllowed()) {
1079 // check if an OTA reporting action is needed
1080 $ota_stay_change_data = [];
1081 foreach ($roomBooking as $kor => $or) {
1082 // set room data for stay change
1083 $ota_stay_change_room = [
1084 'idroom' => $or['idroom'],
1085 'checkin' => $set_checkin,
1086 'checkout' => $set_checkout,
1087 ];
1088 if (isset($or['modified_price'])) {
1089 $ota_stay_change_room['price'] = $or['modified_price'];
1090 }
1091 // push room data for stay change
1092 $ota_stay_change_data[] = $ota_stay_change_room;
1093 }
1094
1095 // notify the OTA through Vik Channel Manager
1096 $ota_reporting = VCMOtaReporting::getInstance();
1097 $ota_result = $ota_reporting->notifyStayChange($ota_stay_change_data);
1098 if (!$ota_result) {
1099 // register error message
1100 $this->setError($ota_reporting->getError());
1101 }
1102 }
1103 }
1104
1105 return true;
1106 }
1107
1108 /**
1109 * Deletes the requested booking ID.
1110 *
1111 * @param array $options List of details to perform the cancellation.
1112 *
1113 * @return bool
1114 *
1115 * @since 1.16.10 (J) - 1.6.10 (WP)
1116 */
1117 public function delete(array $options)
1118 {
1119 $dbo = JFactory::getDbo();
1120
1121 $booking_id = $options['booking_id'] ?? 0;
1122 $canc_reason = $options['cancellation_reason'] ?? '';
1123 $purge_remove = $options['purge_remove'] ?? false;
1124
1125 $booking = VikBooking::getBookingInfoFromID($booking_id);
1126
1127 if (!$booking) {
1128 $this->setError('Booking not found.');
1129 return false;
1130 }
1131
1132 if ($booking['status'] === 'cancelled' && !$purge_remove) {
1133 $this->setError(sprintf('Booking ID %d is already cancelled.', $booking['id']));
1134 return false;
1135 }
1136
1137 if (class_exists('VCMFeesCancellation')) {
1138 // let VCM detect if there are any constraints for the cancellation
1139 $canc_denied = VCMFeesCancellation::getInstance($booking, $anew = true)->isBookingConstrained();
1140 if ($canc_denied) {
1141 // set error message
1142 $canc_deny_error = VCMFeesCancellation::getInstance()->getError();
1143 $this->setError($canc_deny_error ?: 'Booking cannot be cancelled due to OTA contraints.');
1144 return false;
1145 }
1146 }
1147
1148 // access the current user
1149 $now_user = JFactory::getUser();
1150
1151 // whether OTAs should be notified
1152 $notify_otas = false;
1153
1154 if ($booking['status'] != 'cancelled') {
1155 // update status to cancelled
1156 $q = $dbo->getQuery(true)
1157 ->update($dbo->qn('#__vikbooking_orders'))
1158 ->set($dbo->qn('status') . ' = ' . $dbo->q('cancelled'))
1159 ->where($dbo->qn('id') . ' = ' . (int) $booking['id']);
1160 if (!empty($canc_reason)) {
1161 $set_canc_reason = (!empty($booking['adminnotes']) ? $booking['adminnotes'] . "\n" : '') . $canc_reason;
1162 $q->set($dbo->qn('adminnotes') . ' = ' . $dbo->q($set_canc_reason));
1163 }
1164 $dbo->setQuery($q);
1165 $dbo->execute();
1166
1167 // delete temporarily locked records, if any
1168 $dbo->setQuery(
1169 $dbo->getQuery(true)
1170 ->delete($dbo->qn('#__vikbooking_tmplock'))
1171 ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
1172 );
1173 $dbo->execute();
1174
1175 if ($booking['status'] == 'confirmed') {
1176 // turn flag on
1177 $notify_otas = true;
1178 }
1179
1180 // access history object
1181 $history_obj = VikBooking::getBookingHistoryInstance($booking['id']);
1182
1183 $caller_id = $now_user->name ? "({$now_user->name})" : '';
1184 if ($this->getCaller()) {
1185 $caller_id = '(' . $this->getCaller() . ')';
1186 if ($this->getHistoryData()) {
1187 $history_obj->setExtraData($this->getHistoryData());
1188 }
1189 }
1190
1191 // update Booking History
1192 $history_obj->store('CB', $caller_id);
1193 }
1194
1195 // always attempt to free records up
1196 $dbo->setQuery(
1197 $dbo->getQuery(true)
1198 ->select('*')
1199 ->from($dbo->qn('#__vikbooking_ordersbusy'))
1200 ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
1201 );
1202 foreach ($dbo->loadAssocList() as $ob) {
1203 // delete busy record
1204 $dbo->setQuery(
1205 $dbo->getQuery(true)
1206 ->delete($dbo->qn('#__vikbooking_busy'))
1207 ->where($dbo->qn('id') . ' = ' . (int) $ob['idbusy'])
1208 );
1209 $dbo->execute();
1210 }
1211
1212 // delete booking-busy-record relations
1213 $dbo->setQuery(
1214 $dbo->getQuery(true)
1215 ->delete($dbo->qn('#__vikbooking_ordersbusy'))
1216 ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
1217 );
1218 $dbo->execute();
1219
1220 // check for purge removal
1221 if ($booking['status'] === 'cancelled' && $purge_remove) {
1222 // delete booking-customer relation
1223 $dbo->setQuery(
1224 $dbo->getQuery(true)
1225 ->delete($dbo->qn('#__vikbooking_customers_orders'))
1226 ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
1227 );
1228 $dbo->execute();
1229
1230 // delete booking-room relations
1231 $dbo->setQuery(
1232 $dbo->getQuery(true)
1233 ->delete($dbo->qn('#__vikbooking_ordersrooms'))
1234 ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
1235 );
1236 $dbo->execute();
1237
1238 // delete booking-history relations
1239 $dbo->setQuery(
1240 $dbo->getQuery(true)
1241 ->delete($dbo->qn('#__vikbooking_orderhistory'))
1242 ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
1243 );
1244 $dbo->execute();
1245
1246 // delete the booking record
1247 $dbo->setQuery(
1248 $dbo->getQuery(true)
1249 ->delete($dbo->qn('#__vikbooking_orders'))
1250 ->where($dbo->qn('id') . ' = ' . (int) $booking['id'])
1251 );
1252 $dbo->execute();
1253
1254 // in case of split stay booking, remove the transient
1255 if ($booking['split_stay']) {
1256 VBOFactory::getConfig()->remove('split_stay_' . $booking['id']);
1257 }
1258 }
1259
1260 if ($notify_otas) {
1261 $vcm_autosync = VikBooking::vcmAutoUpdate();
1262 if ($vcm_autosync > 0) {
1263 $vcm_obj = VikBooking::getVcmInvoker();
1264 $vcm_obj->setOids([$booking['id']])->setSyncType('cancel');
1265 $sync_result = $vcm_obj->doSync();
1266 if ($sync_result === false) {
1267 // set error message
1268 $vcm_err = $vcm_obj->getError();
1269 $this->setError(JText::translate('VBCHANNELMANAGERRESULTKO') . (!empty($vcm_err) ? ' - ' . $vcm_err : ''));
1270 }
1271 }
1272 }
1273
1274 return true;
1275 }
1276
1277 /**
1278 * Sets a booking ID to confirmed according to the provided options.
1279 *
1280 * @param array $options List of details to perform the update.
1281 *
1282 * @return bool
1283 *
1284 * @since 1.17.3 (J) - 1.7.3 (WP)
1285 */
1286 public function setConfirmed(array $options)
1287 {
1288 $dbo = JFactory::getDbo();
1289
1290 // access the current booking details, if any
1291 $booking = $this->getBooking();
1292
1293 // access the current rooms booked, if any
1294 $roomBooking = $this->getRoomBooking();
1295
1296 // gather modification options
1297 $booking_id = $options['booking_id'] ?? $booking['id'] ?? 0;
1298
1299 if (!$booking_id) {
1300 $this->setError('Missing booking ID.');
1301 return false;
1302 }
1303
1304 if (!$booking) {
1305 // load current booking record if not injected
1306 $booking = VikBooking::getBookingInfoFromID($booking_id);
1307 if (!$booking) {
1308 $this->setError('Booking not found.');
1309 return false;
1310 }
1311 }
1312
1313 if (!$roomBooking) {
1314 // load current rooms booked
1315 $roomBooking = VikBooking::loadOrdersRoomsData($booking_id);
1316 }
1317
1318 // make sure the booking status is not already confirmed
1319 if (!strcasecmp($booking['status'], 'confirmed')) {
1320 $this->setError('Booking is already confirmed.');
1321 return false;
1322 }
1323
1324 // memorize the original booking status for VCM in case of OTA booking
1325 $original_book_status = null;
1326 if (!empty($booking['idorderota']) && !empty($booking['channel'])) {
1327 $original_book_status = $booking['status'];
1328 }
1329
1330 // availability helper
1331 $av_helper = VikBooking::getAvailabilityInstance(true);
1332
1333 // room stay dates in case of split stay
1334 $room_stay_dates = [];
1335 if ($booking['split_stay']) {
1336 $room_stay_dates = VBOFactory::getConfig()->getArray('split_stay_' . $booking['id'], []);
1337 }
1338
1339 // make sure all rooms are available for confirmation
1340 $turnover_secs = VikBooking::getHoursRoomAvail() * 3600;
1341 $realback = $turnover_secs + $booking['checkout'];
1342 $allbook = true;
1343 $notavail = [];
1344
1345 /**
1346 * We need to calculate a minus operator for each room that was booked more than once.
1347 * In case we are confirming a booking for more than one unit of the same room, we need to
1348 * make sure the calculation is made properly, as only one unit of that room could be free.
1349 */
1350 $units_minus_oper = [];
1351 foreach ($roomBooking as $ind => $or) {
1352 if (!isset($units_minus_oper[$or['idroom']])) {
1353 $units_minus_oper[$or['idroom']] = -1;
1354 }
1355 // increase counter
1356 $units_minus_oper[$or['idroom']]++;
1357 if (!empty($room_stay_dates)) {
1358 // split stay rooms never have the same stay dates, but they should also be different rooms
1359 $units_minus_oper[$or['idroom']] = 0;
1360 }
1361 }
1362
1363 // check availability for each room involved
1364 foreach ($roomBooking as $ind => $or) {
1365 // determine proper values for this room
1366 $room_stay_checkin = $booking['checkin'];
1367 $room_stay_checkout = $booking['checkout'];
1368 $room_stay_nights = $booking['days'];
1369 if ($booking['split_stay'] && $room_stay_dates && isset($room_stay_dates[$ind]) && $room_stay_dates[$ind]['idroom'] == $or['idroom']) {
1370 $room_stay_checkin = $room_stay_dates[$ind]['checkin_ts'] ?: $room_stay_dates[$ind]['checkin'];
1371 $room_stay_checkout = $room_stay_dates[$ind]['checkout_ts'] ?: $room_stay_dates[$ind]['checkout'];
1372 $room_stay_nights = $av_helper->countNightsOfStay($room_stay_checkin, $room_stay_checkout);
1373 // inject nights calculated for this room
1374 $room_stay_dates[$ind]['nights'] = $room_stay_nights;
1375 }
1376
1377 // get room record
1378 $room_record = VikBooking::getRoomInfo($or['idroom']);
1379
1380 // check if the room is available
1381 if (!VikBooking::roomBookable($or['idroom'], (($room_record['units'] ?? 0) - $units_minus_oper[$or['idroom']]), $room_stay_checkin, $room_stay_checkout)) {
1382 $allbook = false;
1383 $notavail[] = $room_record['name'] ?? '?';
1384 }
1385 }
1386
1387 // ensure all rooms involved were available or forced to be
1388 if (!$allbook && !($options['force_availability'] ?? false)) {
1389 $this->setError(sprintf('Some rooms are no longer available: %s', implode(', ', $notavail)));
1390 return false;
1391 }
1392
1393 // occupy the involved rooms on the db
1394 foreach ($roomBooking as $ind => $or) {
1395 // determine proper values for this room
1396 $room_stay_checkin = $booking['checkin'];
1397 $room_stay_checkout = $booking['checkout'];
1398 $room_stay_realback = $realback;
1399 if ($booking['split_stay'] && $room_stay_dates && isset($room_stay_dates[$ind]) && $room_stay_dates[$ind]['idroom'] == $or['idroom']) {
1400 $room_stay_checkin = $room_stay_dates[$ind]['checkin_ts'] ?: $room_stay_dates[$ind]['checkin'];
1401 $room_stay_checkout = $room_stay_dates[$ind]['checkout_ts'] ?: $room_stay_dates[$ind]['checkout'];
1402 $room_stay_realback = $turnover_secs + $room_stay_checkout;
1403 }
1404
1405 // build busy record
1406 $busy_record = new stdClass;
1407 $busy_record->idroom = (int) $or['idroom'];
1408 $busy_record->checkin = (int) $room_stay_checkin;
1409 $busy_record->checkout = (int) $room_stay_checkout;
1410 $busy_record->realback = (int) $room_stay_realback;
1411
1412 // store busy record and obtain the newly created ID
1413 $dbo->insertObject('#__vikbooking_busy', $busy_record, 'id');
1414 $lid = $busy_record->id ?? 0;
1415
1416 // build busy relation record
1417 $obusy_record = new stdClass;
1418 $obusy_record->idorder = (int) $booking['id'];
1419 $obusy_record->idbusy = (int) $lid;
1420
1421 // store busy relation record
1422 $dbo->insertObject('#__vikbooking_ordersbusy', $obusy_record, 'id');
1423 }
1424
1425 // delete temporarily locked records, if any
1426 $dbo->setQuery(
1427 $dbo->getQuery(true)
1428 ->delete($dbo->qn('#__vikbooking_tmplock'))
1429 ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
1430 );
1431 $dbo->execute();
1432
1433 // update booking status (and notes, if any)
1434 $q = $dbo->getQuery(true)
1435 ->update($dbo->qn('#__vikbooking_orders'))
1436 ->set($dbo->qn('status') . ' = ' . $dbo->q('confirmed'))
1437 ->where($dbo->qn('id') . ' = ' . (int) $booking['id']);
1438 if ($options['extra_notes'] ?? '') {
1439 // update administrator notes
1440 $q->set($dbo->qn('adminnotes') . ' = ' . $dbo->q(trim($booking['adminnotes'] . "\n" . $options['extra_notes'])));
1441 }
1442 $dbo->setQuery($q);
1443 $dbo->execute();
1444
1445 // set booking confirmation number
1446 $confirmnumber = VikBooking::generateConfirmNumber($booking['id'], true);
1447
1448 // update booking history
1449 $history_obj = VikBooking::getBookingHistoryInstance($booking['id']);
1450
1451 $now_user = JFactory::getUser();
1452 $caller_id = $now_user->name ? "({$now_user->name})" : '';
1453 if ($this->getCaller()) {
1454 $caller_id = '(' . $this->getCaller() . ')';
1455 if ($this->getHistoryData()) {
1456 $history_obj->setExtraData($this->getHistoryData());
1457 }
1458 }
1459
1460 // update Booking History
1461 $history_obj->store('TC', $caller_id);
1462
1463 // check if some of the rooms booked have shared calendars
1464 VikBooking::updateSharedCalendars($booking['id'], array_column($roomBooking, 'idroom'), $booking['checkin'], $booking['checkout']);
1465
1466 // assign room specific unit(s)
1467 $set_room_indexes = VikBooking::autoRoomUnit();
1468 $room_indexes_usemap = [];
1469
1470 foreach ($roomBooking as $kor => $or) {
1471 // determine proper values for this room
1472 $room_stay_checkin = $booking['checkin'];
1473 $room_stay_checkout = $booking['checkout'];
1474 $room_stay_nights = $booking['days'];
1475 if ($booking['split_stay'] && $room_stay_dates && isset($room_stay_dates[$kor]) && $room_stay_dates[$kor]['idroom'] == $or['idroom']) {
1476 $room_stay_checkin = $room_stay_dates[$kor]['checkin_ts'] ?: $room_stay_dates[$kor]['checkin'];
1477 $room_stay_checkout = $room_stay_dates[$kor]['checkout_ts'] ?: $room_stay_dates[$kor]['checkout'];
1478 $room_stay_nights = $room_stay_dates[$kor]['nights'];
1479 }
1480
1481 // assign room specific unit
1482 if ($set_room_indexes === true) {
1483 $room_indexes = VikBooking::getRoomUnitNumsAvailable($booking, $or['idroom']);
1484 $use_ind_key = 0;
1485 if ($room_indexes) {
1486 if (!isset($room_indexes_usemap[$or['idroom']])) {
1487 $room_indexes_usemap[$or['idroom']] = $use_ind_key;
1488 } else {
1489 $use_ind_key = $room_indexes_usemap[$or['idroom']];
1490 }
1491
1492 // update room-reservation record by assigning the room index (unit)
1493 $dbo->setQuery(
1494 $dbo->getQuery(true)
1495 ->update($dbo->qn('#__vikbooking_ordersrooms'))
1496 ->set($dbo->qn('roomindex') . ' = ' . (int) $room_indexes[$use_ind_key])
1497 ->where($dbo->qn('id') . ' = ' . (int) $or['id'])
1498 );
1499 $dbo->execute();
1500
1501 // increase index counter
1502 $room_indexes_usemap[$or['idroom']]++;
1503 }
1504 }
1505 }
1506
1507 // Invoke Channel Manager
1508 $vcm_autosync = VikBooking::vcmAutoUpdate();
1509 if ($vcm_autosync > 0) {
1510 $vcm_obj = VikBooking::getVcmInvoker();
1511 $vcm_obj->setOids([$booking['id']])->setSyncType('new')->setOriginalStatuses([$original_book_status]);
1512 $sync_result = $vcm_obj->doSync();
1513 if ($sync_result === false) {
1514 // set error message
1515 $vcm_err = $vcm_obj->getError();
1516 $this->setError(JText::translate('VBCHANNELMANAGERRESULTKO') . (!empty($vcm_err) ? ' - ' . $vcm_err : ''));
1517
1518 // return true because the booking was actually confirmed
1519 return true;
1520 }
1521 } elseif (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'synch.vikbooking.php')) {
1522 // set the necessary action to invoke VCM
1523 $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']);
1524
1525 $this->setChannelManagerAction(JText::translate('VBCHANNELMANAGERINVOKEASK') . ' <button type="button" class="btn btn-primary" onclick="document.location.href=\'' . $vcm_sync_url . '\';">' . JText::translate('VBCHANNELMANAGERSENDRQ') . '</button>');
1526 }
1527
1528 // check if the guest should be notified via email
1529 if ($options['notify'] ?? null) {
1530 // send email notification to guest
1531 VikBooking::sendBookingEmail($booking['id'], ['guest']);
1532
1533 // SMS skipping the administrator
1534 VikBooking::sendBookingSMS($booking['id'], ['admin']);
1535 }
1536
1537 return true;
1538 }
1539
1540 /**
1541 * Tells if the booking record set can be modified and/or cancelled.
1542 *
1543 * @return array
1544 *
1545 * @since 1.16.10 (J) - 1.6.10 (WP)
1546 */
1547 public function getAlterationDetails()
1548 {
1549 $dbo = JFactory::getDbo();
1550
1551 $booking = $this->getBooking();
1552 $roomBooking = $this->getRoomBooking();
1553
1554 if (!$booking || !$roomBooking) {
1555 $this->setError('Missing booking or room booking record details.');
1556 return [];
1557 }
1558
1559 // gather room booking tariffs
1560 $tars = [];
1561 foreach ($roomBooking as $kor => $or) {
1562 $num = $kor + 1;
1563 if (!empty($order['pkg']) || (!empty($or['cust_cost']) && $or['cust_cost'] > 0.00)) {
1564 // package or custom cost set from the back-end
1565 continue;
1566 }
1567
1568 // get room tariff details
1569 $dbo->setQuery(
1570 $dbo->getQuery(true)
1571 ->select($dbo->qn('t') . '.*')
1572 ->select([
1573 $dbo->qn('p.name'),
1574 $dbo->qn('p.free_cancellation'),
1575 $dbo->qn('p.canc_deadline'),
1576 $dbo->qn('p.canc_policy'),
1577 ])
1578 ->from($dbo->qn('#__vikbooking_dispcost', 't'))
1579 ->leftJoin($dbo->qn('#__vikbooking_prices', 'p') . ' ON ' . $dbo->qn('t.idprice') . ' = ' . $dbo->qn('p.id'))
1580 ->where($dbo->qn('t.id') . ' = ' . (int) ($or['idtar'] ?? 0))
1581 );
1582 $tar = $dbo->loadAssoc();
1583
1584 if ($tar) {
1585 // push room booking tariff
1586 $tars[$num] = $tar;
1587 }
1588 }
1589
1590 // count days to arrival
1591 $days_to_arrival = 0;
1592 $now_info = getdate();
1593 $checkin_info = getdate($booking['checkin'] ?? 0);
1594 if ($now_info[0] < $checkin_info[0]) {
1595 while ($now_info[0] < $checkin_info[0]) {
1596 if (!($now_info['mday'] != $checkin_info['mday'] || $now_info['mon'] != $checkin_info['mon'] || $now_info['year'] != $checkin_info['year'])) {
1597 break;
1598 }
1599 $days_to_arrival++;
1600 $now_info = getdate(mktime(0, 0, 0, $now_info['mon'], ($now_info['mday'] + 1), $now_info['year']));
1601 }
1602 }
1603
1604 // check if the rate plan(s) are refundable
1605 $is_refundable = 0;
1606 $daysadv_refund_arr = [];
1607 $daysadv_refund = 0;
1608 $canc_policy = '';
1609 foreach ($tars as $num => $tar) {
1610 if (!$tar['free_cancellation']) {
1611 // if at least one rate plan is non-refundable, the whole reservation cannot be cancelled
1612 $is_refundable = 0;
1613 $daysadv_refund_arr = [];
1614 break;
1615 }
1616 $is_refundable = 1;
1617 $daysadv_refund_arr[] = $tar['canc_deadline'];
1618 }
1619
1620 // get the rate plan with the lowest cancellation deadline
1621 $daysadv_refund = $daysadv_refund_arr ? min($daysadv_refund_arr) : $daysadv_refund;
1622 if ($daysadv_refund > 0) {
1623 foreach ($tars as $num => $tar) {
1624 if ($tar['free_cancellation'] && $tar['canc_deadline'] == $daysadv_refund) {
1625 // get the cancellation policy from the first rate plan with free cancellation and same cancellation deadline
1626 $canc_policy = $tar['canc_policy'];
1627 break;
1628 }
1629 }
1630 }
1631
1632 // access global settings to determine the alterations available
1633 $resmodcanc = VikBooking::getReservationModCanc();
1634 $resmodcanc = !$days_to_arrival ? 0 : $resmodcanc;
1635 $resmodcancmin = VikBooking::getReservationModCancMin();
1636
1637 // build alteration deadline date
1638 $checkin_dt = JFactory::getDate(date('Y-m-d', ($booking['checkin'] ?? 0)));
1639 $checkin_dt->modify("-{$resmodcancmin} days");
1640 $alteration_deadline = $checkin_dt->format('Y-m-d');
1641
1642 return [
1643 'refundable' => ($resmodcanc > 1 && $resmodcanc != 2 && $is_refundable > 0 && $daysadv_refund <= $days_to_arrival && $days_to_arrival >= $resmodcancmin),
1644 'modifiable' => ($resmodcanc > 1 && $resmodcanc != 3 && $days_to_arrival >= $resmodcancmin),
1645 'alteration_disabled' => $resmodcanc === 0,
1646 'request_alteration' => $resmodcanc === 1,
1647 'cancellation_policy' => $canc_policy ?: null,
1648 'alteration_deadline' => $alteration_deadline,
1649 ];
1650 }
1651
1652 /**
1653 * Attempts to invoke the payment processor assigned to the current booking.
1654 *
1655 * @param array $card Optional credit card details to bind.
1656 *
1657 * @return object The payment processor dispatcher instance.
1658 *
1659 * @throws Exception
1660 *
1661 * @since 1.16.10 (J) - 1.6.10 (WP)
1662 */
1663 public function getPaymentProcessor(array $card = [])
1664 {
1665 $booking = $this->getProperties();
1666 $processor = null;
1667 $payment = [];
1668
1669 if (!$booking) {
1670 throw new Exception('Missing booking details', 500);
1671 }
1672
1673 if (!empty($booking['idpayment'])) {
1674 $payment = VikBooking::getPayment($booking['idpayment']);
1675 }
1676
1677 if (!$payment) {
1678 throw new Exception('Missing payment method details', 500);
1679 }
1680
1681 // set payment details internally
1682 $this->set('_payment_info', $payment);
1683
1684 if ($card) {
1685 // inject CC details for the payment processor
1686 $booking['card'] = $card;
1687 }
1688
1689 if (VBOPlatformDetection::isWordPress()) {
1690 /**
1691 * @wponly The payment gateway is loaded
1692 * through the apposite dispatcher.
1693 */
1694 JLoader::import('adapter.payment.dispatcher');
1695 $processor = JPaymentDispatcher::getInstance('vikbooking', $payment['file'], $booking, $payment['params']);
1696 } elseif (VBOPlatformDetection::isJoomla()) {
1697 /**
1698 * @joomlaonly The Payment Factory library will invoke the gateway.
1699 */
1700 require_once VBO_ADMIN_PATH . DIRECTORY_SEPARATOR . 'payments' . DIRECTORY_SEPARATOR . 'libraries' . DIRECTORY_SEPARATOR . 'factory.php';
1701 $processor = VBOPaymentFactory::getPaymentInstance($payment['file'], $booking, $payment['params']);
1702 }
1703
1704 if (!$processor) {
1705 throw new Exception('Could not invoke the payment processor', 500);
1706 }
1707
1708 // return the valid payment processor instance
1709 return $processor;
1710 }
1711
1712 /**
1713 * Gets the reservation's payment method name.
1714 *
1715 * @return string
1716 *
1717 * @since 1.16.10 (J) - 1.6.10 (WP)
1718 */
1719 public function getPaymentName()
1720 {
1721 // access the reserved property
1722 $payment = (array) $this->get('_payment_info', []);
1723
1724 if (!$payment) {
1725 return '';
1726 }
1727
1728 return $payment['name'] ?? '';
1729 }
1730
1731 /**
1732 * Attempts to get the credit card value pairs from the current booking.
1733 *
1734 * @return array Associative list of CC value-pairs, if any.
1735 *
1736 * @since 1.16.10 (J) - 1.6.10 (WP)
1737 */
1738 public function getCardValuePairs()
1739 {
1740 $booking_info = $this->getProperties();
1741
1742 if (empty($booking_info['paymentlog'])) {
1743 return [];
1744 }
1745
1746 // build complete credit card payload, if available
1747 $cc_payload_str = '';
1748
1749 // extract CC data from payment logs by ensuring they're not null
1750 $booking_info['paymentlog'] = (string) $booking_info['paymentlog'];
1751 if (stripos($booking_info['paymentlog'], 'card number') !== false && strpos($booking_info['paymentlog'], '*') !== false) {
1752 // matched a log for an OTA CC
1753 $cc_payload_str = $booking_info['paymentlog'];
1754 } elseif (preg_match("/(([\d\*]{4,4}\s*){4,4})|(([\d\*]{4,6}\s*){3,3})/", $booking_info['paymentlog'])) {
1755 // matched a credit card
1756 $cc_payload_str = $booking_info['paymentlog'];
1757 }
1758
1759 // check if this is an OTA reservation with remotely decoded CC details required
1760 $remote_cc_data = [];
1761 if (!empty($booking_info['idorderota']) && !empty($booking_info['channel'])) {
1762 // channel source
1763 $channel_source = (string)$booking_info['channel'];
1764 if (strpos($booking_info['channel'], '_') !== false) {
1765 $channelparts = explode('_', $booking_info['channel']);
1766 $channel_source = $channelparts[0];
1767 }
1768
1769 // only updated versions of VCM will support remote CC decoding for OTA reservations
1770 if (class_exists('VCMOtaBooking')) {
1771 // invoke the OTA Booking helper class from VCM
1772 $cc_helper = VCMOtaBooking::getInstance([
1773 'channel_source' => $channel_source,
1774 'ota_id' => $booking_info['idorderota'],
1775 ], $anew = true);
1776
1777 if (method_exists($cc_helper, 'decodeCreditCardDetails')) {
1778 $remote_cc_data = $cc_helper->decodeCreditCardDetails();
1779 // make sure the response was valid
1780 if (!$remote_cc_data || !empty($remote_cc_data['error'])) {
1781 // we ignore the error by simply resetting the array
1782 $remote_cc_data = [];
1783 }
1784 }
1785 }
1786 }
1787
1788 // merge remotely decoded CC details with parsed payment log (if any)
1789 return array_merge($remote_cc_data, $this->parseCreditCardValuePairs($cc_payload_str, $remote_cc_data));
1790 }
1791
1792 /**
1793 * Given a raw string of credit card key-value pairs from payments log,
1794 * parse the corresponding keys and values into an associative array.
1795 * In case of conflicting keys with the remotely decoded CC details,
1796 * attempts to replace the masked numbers with asterisks.
1797 *
1798 * @param string $cc_payload the raw CC details from payment logs.
1799 * @param array $remote_cc_data associative array of decoded CC data.
1800 *
1801 * @return array associative or empty array.
1802 *
1803 * @since 1.16.10 (J) - 1.6.10 (WP) moved from widget Virtual Terminal.
1804 */
1805 protected function parseCreditCardValuePairs($cc_payload, array $remote_cc_data = [])
1806 {
1807 $cc_value_pairs = [];
1808
1809 if (empty($cc_payload)) {
1810 return $cc_value_pairs;
1811 }
1812
1813 $cc_lines = preg_split("/(\r\n|\n|\r)/", $cc_payload);
1814
1815 foreach ($cc_lines as $cc_line) {
1816 if (strpos($cc_line, ':') === false) {
1817 continue;
1818 }
1819
1820 $cc_line_parts = explode(':', $cc_line);
1821
1822 if (empty($cc_line_parts[0]) || !strlen(trim($cc_line_parts[1]))) {
1823 continue;
1824 }
1825
1826 $key = str_replace(' ', '_', strtolower($cc_line_parts[0]));
1827 $value = trim($cc_line_parts[1]);
1828
1829 if (isset($cc_value_pairs[$key])) {
1830 /**
1831 * Do not overwrite existing keys because this probably means that the
1832 * credit card was updated by an OTA like Booking.com, hence the payment
1833 * logs string in VBO may contain the information of two different cards.
1834 * New credit card details are always pre-pended by VCM in the payment logs.
1835 */
1836 continue;
1837 }
1838
1839 if (!empty($remote_cc_data[$key]) && is_string($remote_cc_data[$key]) && strpos($value, '*') !== false) {
1840 // replace masked numbers with remote content
1841 $value = $this->replaceMaskedNumbers($value, $remote_cc_data[$key]);
1842 }
1843
1844 $cc_value_pairs[$key] = $value;
1845 }
1846
1847 return $cc_value_pairs;
1848 }
1849
1850 /**
1851 * Given a local and a remote credit card number string with
1852 * masked symbols, replaces the values in the corresponding
1853 * positions with the unmasked numbers.
1854 *
1855 * @param string $local current string with masked values.
1856 * @param string $remote remote string with unmasked values.
1857 *
1858 * @return string the local string with unmasked values.
1859 *
1860 * @since 1.16.10 (J) - 1.6.10 (WP) moved from widget Virtual Terminal.
1861 */
1862 protected function replaceMaskedNumbers($local, $remote)
1863 {
1864 // split anything but numbers
1865 $numbers = preg_split("/([^0-9]+)/", trim($remote));
1866
1867 if ($numbers) {
1868 // filter empty values
1869 $numbers = array_filter($numbers);
1870 }
1871
1872 if (!$numbers) {
1873 // unable to proceed
1874 return $local;
1875 }
1876
1877 // split anything but stars (asterisks)
1878 $stars = preg_split("/([^\*]+)/", trim($local));
1879
1880 if ($stars) {
1881 // filter empty values
1882 $stars = array_filter($stars);
1883 }
1884
1885 if (!$stars) {
1886 // unable to proceed
1887 return $local;
1888 }
1889
1890 // replace masked symbols with numbers at their first occurrence
1891 foreach ($numbers as $k => $unmasked) {
1892 if (!isset($stars[$k])) {
1893 continue;
1894 }
1895
1896 $masked_pos = strpos($local, $stars[$k]);
1897
1898 if ($masked_pos === false) {
1899 continue;
1900 }
1901
1902 $local = substr_replace($local, $unmasked, $masked_pos, strlen($stars[$k]));
1903 }
1904
1905 // return the string with possibly unmasked values
1906 return $local;
1907 }
1908
1909 /**
1910 * Tells whether the booking can be created. By default this
1911 * is only allowed from the administrator section of the site.
1912 *
1913 * @return bool
1914 */
1915 protected function canCreate()
1916 {
1917 return $this->get('_isAdministrator') || JFactory::getApplication()->isClient('administrator');
1918 }
1919
1920 /**
1921 * Gets and sets the tariff ID if a rate plan was set.
1922 *
1923 * @return int the tariff ID or 0.
1924 */
1925 protected function loadTariffID()
1926 {
1927 $dbo = JFactory::getDbo();
1928
1929 $id_tariff = 0;
1930 $room = $this->getRoom();
1931 $daysdiff = (int)$this->get('nights', 1);
1932
1933 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', [])) {
1934 $room['id_price'] = (int)$room['id_price'];
1935
1936 $q = "SELECT `id` FROM `#__vikbooking_dispcost` WHERE `idroom`={$room['id']} AND `days`={$daysdiff} AND `idprice`={$room['id_price']};";
1937 $dbo->setQuery($q);
1938 $id_tariff = $dbo->loadResult();
1939 }
1940
1941 $this->set('id_tariff', (int)$id_tariff);
1942
1943 return (int)$id_tariff;
1944 }
1945
1946 /**
1947 * Applies the turnover time to the checkout timestamp and sets its value.
1948 *
1949 * @return int the turnover seconds applied.
1950 */
1951 protected function applyTurnover()
1952 {
1953 $turnover_secs = 0;
1954 $checkout = $this->get('checkout', 0);
1955
1956 if ($checkout) {
1957 // turnover time
1958 $turnover_secs = VikBooking::getHoursRoomAvail() * 3600;
1959
1960 $this->set('checkout_real', ($checkout + $turnover_secs));
1961 }
1962
1963 $this->set('turnover_secs', $turnover_secs);
1964
1965 return $turnover_secs;
1966 }
1967
1968 /**
1969 * Returns the details of a specific room ID.
1970 *
1971 * @param int $rid the room ID.
1972 *
1973 * @return array the record found or empty array.
1974 */
1975 protected function getRoomDetails($rid = null)
1976 {
1977 $all_rooms = VikBooking::getAvailabilityInstance(true)->loadRooms();
1978
1979 if (!$rid) {
1980 $inj_room = $this->getRoom();
1981 if (!empty($inj_room['id'])) {
1982 $rid = $inj_room['id'];
1983 }
1984 }
1985
1986 if ($rid && isset($all_rooms[$rid])) {
1987 return $all_rooms[$rid];
1988 }
1989
1990 return [];
1991 }
1992
1993 /**
1994 * Gets the list of rooms involved in the reservation in case of closures.
1995 *
1996 * @return array the list of rooms involved
1997 */
1998 protected function getRoomsPool()
1999 {
2000 $room = $this->getRoom();
2001 if (empty($room['id'])) {
2002 return [];
2003 }
2004 $room = $this->getRoomDetails($room['id']);
2005
2006 // gather values
2007 $set_close_others = (array)$this->get('close_others', []);
2008 $split_stay_data = $this->get('split_stay', []);
2009 $set_closed = (int)$this->get('set_closed');
2010 $turnover_secs = $this->get('turnover_secs', 0);
2011 $hcheckin = $this->get('checkin_h', 12);
2012 $mcheckin = $this->get('checkin_m', 0);
2013 $hcheckout = $this->get('checkout_h', 10);
2014 $mcheckout = $this->get('checkout_m', 0);
2015
2016 $av_helper = VikBooking::getAvailabilityInstance(true);
2017 $all_rooms = $av_helper->loadRooms();
2018 $rooms_pool = [];
2019 $closeothers = [];
2020
2021 if ($set_close_others && $set_closed) {
2022 // prepend current room for closing
2023 array_unshift($set_close_others, $room['id']);
2024 }
2025 $set_close_others = array_unique($set_close_others);
2026
2027 foreach ($set_close_others as $closeid) {
2028 if (empty($closeid)) {
2029 continue;
2030 }
2031 if ((int)$closeid === -1) {
2032 // close all rooms
2033 $closeothers = [];
2034 foreach ($all_rooms as $cr) {
2035 array_push($closeothers, $cr);
2036 }
2037 break;
2038 }
2039 foreach ($all_rooms as $cr) {
2040 if ((int)$cr['id'] == (int)$closeid) {
2041 // push the main room or one of the other rooms requested for closure
2042 array_push($closeothers, $cr);
2043 break;
2044 }
2045 }
2046 }
2047
2048 if (!$closeothers || !$set_closed) {
2049 $rooms_pool = [$room];
2050 } else {
2051 $rooms_pool = $closeothers;
2052 }
2053
2054 // check split stay rooms booking
2055 if (!empty($split_stay_data)) {
2056 // reset pool and set it with the split stay rooms
2057 $rooms_pool = [];
2058 foreach ($split_stay_data as $sps_k => $split_stay) {
2059 if (!isset($all_rooms[$split_stay['idroom']])) {
2060 continue;
2061 }
2062 // calculate and set the exact check-in and check-out timestamps for this split-room
2063 $split_stay['checkin_ts'] = VikBooking::getDateTimestamp($split_stay['checkin'], $hcheckin, $mcheckin);
2064 $split_stay['checkout_ts'] = VikBooking::getDateTimestamp($split_stay['checkout'], $hcheckout, $mcheckout);
2065 $split_stay['realback_ts'] = $turnover_secs + $split_stay['checkout_ts'];
2066 $split_stay['nights'] = $av_helper->countNightsOfStay($split_stay['checkin_ts'], $split_stay['checkout_ts']);
2067 $split_stay_data[$sps_k] = $split_stay;
2068 // push room data to pool after storing additional information
2069 $room_data = $all_rooms[$split_stay['idroom']];
2070 $room_data['checkin_ts'] = $split_stay['checkin_ts'];
2071 $room_data['checkout_ts'] = $split_stay['checkout_ts'];
2072 $rooms_pool[] = $room_data;
2073 }
2074 if (!$rooms_pool) {
2075 $this->setError('No valid rooms for the split stay booking');
2076 return [];
2077 }
2078 // update split stay data manipulated
2079 $this->set('split_stay', $split_stay_data);
2080 }
2081
2082 return $rooms_pool;
2083 }
2084
2085 /**
2086 * Checks that the room is available on the requested dates.
2087 * Call this method only after getting the rooms pool.
2088 *
2089 * @return bool
2090 */
2091 protected function isRoomAvailable()
2092 {
2093 $inj_room = $this->getRoom();
2094 if (empty($inj_room['id'])) {
2095 return false;
2096 }
2097 $room = $this->getRoomDetails($inj_room['id']);
2098 if (!$room) {
2099 return false;
2100 }
2101
2102 $split_stay_data = $this->get('split_stay', []);
2103 $set_closed = $this->get('set_closed', 0);
2104 $num_rooms = $this->get('num_rooms', 1);
2105
2106 $room_available = true;
2107
2108 if (empty($split_stay_data)) {
2109 // make sure the rooms are available
2110 $check_units = $room['units'];
2111 if ($num_rooms > 1 && $num_rooms <= $room['units'] && !$set_closed) {
2112 // only when non closing the room we check the availability for the units requested for booking
2113 $check_units = $room['units'] - $num_rooms + 1;
2114 }
2115 $room_available = VikBooking::roomBookable($room['id'], $check_units, $this->get('checkin', 0), $this->get('checkout', 0));
2116 } else {
2117 $all_rooms = VikBooking::getAvailabilityInstance(true)->loadRooms();
2118 // make sure the rooms for the split stay are available
2119 foreach ($split_stay_data as $split_stay) {
2120 if (!isset($all_rooms[$split_stay['idroom']])) {
2121 $room_available = false;
2122 break;
2123 }
2124 $room_available = $room_available && VikBooking::roomBookable($split_stay['idroom'], $all_rooms[$split_stay['idroom']]['units'], $split_stay['checkin_ts'], $split_stay['checkout_ts']);
2125 }
2126 }
2127
2128 return $room_available;
2129 }
2130
2131 /**
2132 * In case the reservation is forced or is a closure, we detect the
2133 * forced reason to eventually attach it to the booking history.
2134 *
2135 * @param bool $room_available whether the room is available.
2136 *
2137 * @return void
2138 */
2139 protected function detectForcedReason($room_available = true)
2140 {
2141 $split_stay_data = $this->get('split_stay', []);
2142 $force_booking = $this->get('force_booking', 0);
2143 $set_closed = $this->get('set_closed', 0);
2144
2145 $forced_reason = $this->get('forced_reason', '');
2146
2147 if (empty($split_stay_data)) {
2148 // eventually build string for the description of the history event
2149 if (($force_booking || $set_closed) && !$room_available) {
2150 $forced_reason = JText::translate('VBO_FORCED_BOOKDATES');
2151 }
2152 } else {
2153 $all_rooms = VikBooking::getAvailabilityInstance(true)->loadRooms();
2154 // set "split stay" as the description of the history event
2155 $forced_reason = JText::translate('VBO_SPLIT_STAY') . "\n";
2156 foreach ($split_stay_data as $sps_k => $split_stay) {
2157 // describe the split stay for each room
2158 if (!isset($all_rooms[$split_stay['idroom']])) {
2159 continue;
2160 }
2161 $room_stay_nights = $split_stay['nights'];
2162 $forced_reason .= $all_rooms[$split_stay['idroom']]['name'] . ': ' . $room_stay_nights . ' ' . ($room_stay_nights > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY')) . ', ';
2163 $forced_reason .= $split_stay['checkin'] . ' - ' . $split_stay['checkout'] . "\n";
2164 }
2165 $forced_reason = rtrim($forced_reason, "\n");
2166 }
2167
2168 $this->set('forced_reason', $forced_reason);
2169 }
2170
2171 /**
2172 * Stores the customer information to a new or existing record.
2173 * In case of success, the customer ID property is updated.
2174 * The customer shall be stored before the reservation records.
2175 *
2176 * @return bool
2177 */
2178 protected function storeCustomer()
2179 {
2180 $dbo = JFactory::getDbo();
2181
2182 $inj_customer = $this->getCustomer();
2183 $first_name = !empty($inj_customer['first_name']) ? $inj_customer['first_name'] : '';
2184 $last_name = !empty($inj_customer['last_name']) ? $inj_customer['last_name'] : '';
2185 $custdata = !empty($inj_customer['data']) ? $inj_customer['data'] : '';
2186 $email = !empty($inj_customer['email']) ? $inj_customer['email'] : '';
2187 $country = !empty($inj_customer['country']) ? $inj_customer['country'] : '';
2188 $phone = !empty($inj_customer['phone']) ? $inj_customer['phone'] : '';
2189
2190 // custom fields
2191 $q = "SELECT * FROM `#__vikbooking_custfields` ORDER BY `ordering` ASC;";
2192 $dbo->setQuery($q);
2193 $all_cfields = $dbo->loadAssocList();
2194
2195 $customer_cfields = [];
2196 $customer_extrainfo = [];
2197 $custdata_parts = explode("\n", $custdata);
2198 foreach ($custdata_parts as $cdataline) {
2199 if (!strlen(trim($cdataline))) {
2200 continue;
2201 }
2202 $cdata_parts = explode(':', $cdataline);
2203 if (count($cdata_parts) < 2 || !strlen(trim($cdata_parts[0])) || !strlen(trim($cdata_parts[1]))) {
2204 continue;
2205 }
2206 foreach ($all_cfields as $cf) {
2207 $needle = JText::translate($cf['name']);
2208 if (!empty($needle) && strpos($cdata_parts[0], $needle) !== false && !array_key_exists($cf['id'], $customer_cfields) && $cf['type'] != 'country') {
2209 $user_input_val = trim($cdata_parts[1]);
2210 $customer_cfields[$cf['id']] = $user_input_val;
2211 if (!empty($cf['flag'])) {
2212 $customer_extrainfo[$cf['flag']] = $user_input_val;
2213 } elseif ($cf['type'] == 'state') {
2214 $customer_extrainfo['state'] = $user_input_val;
2215 }
2216 break;
2217 }
2218 }
2219 }
2220
2221 $cpin = VikBooking::getCPinInstance();
2222 $cpin->is_admin = true;
2223 $cpin->setCustomerExtraInfo($customer_extrainfo);
2224 $cpin->saveCustomerDetails($first_name, $last_name, $email, $phone, $country, $customer_cfields);
2225
2226 $customer_id = $cpin->getNewCustomerId();
2227 if (!$customer_id) {
2228 return false;
2229 }
2230
2231 $inj_customer['id'] = $customer_id;
2232 $this->setCustomer($inj_customer);
2233
2234 return true;
2235 }
2236
2237 /**
2238 * Returns the calculated total booking amount and total taxes.
2239 * Sets the necessary properties with the calculated amounts.
2240 *
2241 * @return array list of booking total amount and total taxes.
2242 */
2243 protected function calculateTotal()
2244 {
2245 // the values to calculate
2246 $set_total = 0;
2247 $set_taxes = 0;
2248
2249 $dbo = JFactory::getDbo();
2250
2251 // get data
2252 $inj_room = $this->getRoom();
2253 $set_closed = $this->get('set_closed', 0);
2254 $daysdiff = (int)$this->get('nights', 1);
2255 $num_rooms = (int)$this->get('num_rooms', 1);
2256 $totalpnight = !empty($inj_room['total_or_pnight']) ? $inj_room['total_or_pnight'] : 'total';
2257 $cust_cost = !empty($inj_room['cust_cost']) ? (float)$inj_room['cust_cost'] : 0;
2258 $room_cost = !empty($inj_room['room_cost']) ? (float)$inj_room['room_cost'] : 0;
2259 $id_price = !empty($inj_room['id_price']) ? (int)$inj_room['id_price'] : 0;
2260 $id_tax = !empty($inj_room['id_tax']) ? (int)$inj_room['id_tax'] : 0;
2261
2262 $split_stay_data = $this->get('split_stay', []);
2263
2264 $room = $this->getRoomDetails();
2265 if (!$room) {
2266 return [$set_total, $set_taxes];
2267 }
2268
2269 if ($cust_cost > 0 && !$set_closed) {
2270 // custom cost can be per night
2271 if ($totalpnight == 'pnight') {
2272 $cust_cost = $cust_cost * $daysdiff;
2273 }
2274 $set_total = $cust_cost;
2275
2276 if (!$id_tax && ($inj_room['guess_tax'] ?? false)) {
2277 // try to guess the tax rate
2278 $dbo->setQuery(
2279 $dbo->getQuery(true)
2280 ->select($dbo->qn('id'))
2281 ->from($dbo->qn('#__vikbooking_iva'))
2282 ->order($dbo->qn('aliq') . ' ASC'),
2283 0, 1);
2284 $guessed_id_tax = $dbo->loadResult();
2285 if ($guessed_id_tax) {
2286 // update the id_tax values
2287 $id_tax = $guessed_id_tax;
2288 $inj_room['id_tax'] = $guessed_id_tax;
2289 $this->setRoom($inj_room);
2290 }
2291 }
2292
2293 // apply taxes, if necessary
2294 if ($id_tax) {
2295 $dbo->setQuery(
2296 $dbo->getQuery(true)
2297 ->select([
2298 $dbo->qn('i.aliq'),
2299 $dbo->qn('i.taxcap'),
2300 ])
2301 ->from($dbo->qn('#__vikbooking_iva', 'i'))
2302 ->where($dbo->qn('i.id') . ' = ' . (int) $id_tax),
2303 0, 1);
2304 $taxdata = $dbo->loadAssoc();
2305 if ($taxdata) {
2306 $aliq = $taxdata['aliq'];
2307 if (floatval($aliq) > 0.00) {
2308 if (!VikBooking::ivaInclusa()) {
2309 // add tax to the total amount
2310 $subt = 100 + (float)$aliq;
2311 $set_total = ($set_total * $subt / 100);
2312 /**
2313 * Tax Cap implementation for prices tax excluded (most common).
2314 *
2315 * @since 1.12 (J) - 1.2 (WP)
2316 */
2317 if ($taxdata['taxcap'] > 0 && ($set_total - $cust_cost) > $taxdata['taxcap']) {
2318 $set_total = ($cust_cost + $taxdata['taxcap']);
2319 }
2320 // calculate tax
2321 $set_taxes = $set_total - $cust_cost;
2322 } else {
2323 // calculate tax
2324 $cost_minus_tax = VikBooking::sayPackageMinusIva($cust_cost, $id_tax);
2325 $set_taxes += ($cust_cost - $cost_minus_tax);
2326 }
2327 }
2328 }
2329 }
2330 } elseif (!empty($id_price) && $room_cost > 0 && !$set_closed && empty($split_stay_data)) {
2331 // one website rate plan was selected, so we calculate total and taxes
2332 $set_total = $room_cost;
2333
2334 // find tax rate assigned to this rate plan
2335 $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 . ";";
2336 $dbo->setQuery($q);
2337 $taxdata = $dbo->loadAssoc();
2338 if ($taxdata) {
2339 $aliq = $taxdata['aliq'];
2340 if (floatval($aliq) > 0.00) {
2341 if (!VikBooking::ivaInclusa()) {
2342 // add tax to the total amount
2343 $subt = 100 + (float)$aliq;
2344 $set_total = ($set_total * $subt / 100);
2345 /**
2346 * Tax Cap implementation for prices tax excluded (most common).
2347 *
2348 * @since 1.12 (J) - 1.2 (WP)
2349 */
2350 if ($taxdata['taxcap'] > 0 && ($set_total - $room_cost) > $taxdata['taxcap']) {
2351 $set_total = ($room_cost + $taxdata['taxcap']);
2352 }
2353 // calculate tax
2354 $set_taxes = $set_total - $room_cost;
2355 } else {
2356 // calculate tax
2357 $cost_minus_tax = VikBooking::sayPackageMinusIva($room_cost, $taxdata['idiva']);
2358 $set_taxes += ($room_cost - $cost_minus_tax);
2359 }
2360 }
2361 }
2362
2363 // total and taxes should be multiplied by the number of rooms booked when using a website rate plan
2364 if ($set_closed) {
2365 $set_total *= $room['units'];
2366 $set_taxes *= $room['units'];
2367 } elseif ($num_rooms > 1 && $num_rooms <= $room['units']) {
2368 $set_total *= $num_rooms;
2369 $set_taxes *= $num_rooms;
2370 }
2371 }
2372
2373 // set values
2374 $this->set('_total', $set_total);
2375 $this->set('_total_tax', $set_taxes);
2376
2377 return [$set_total, $set_taxes];
2378 }
2379
2380 /**
2381 * Stores the booking and room-booking records.
2382 * If no errors, the newly generated booking id is set.
2383 *
2384 * @param array $rooms_pool list of rooms involved.
2385 *
2386 * @return bool
2387 */
2388 protected function storeReservationRecords(array $rooms_pool)
2389 {
2390 $dbo = JFactory::getDbo();
2391
2392 $set_closed = $this->get('set_closed', 0);
2393 $units_closed = $this->get('units_closed', 0);
2394 $daysdiff = (int) $this->get('nights', 1);
2395 $num_rooms = (int) $this->get('num_rooms', 1);
2396 $adults = (int) $this->get('adults', 1);
2397 $children = (int) $this->get('children', 0);
2398 $children_age = (array) $this->get('children_age', []);
2399 $status = $this->get('status', 'confirmed');
2400
2401 $split_stay_data = $this->get('split_stay', []);
2402 $room = $this->getRoomDetails();
2403 if (!$room || !$rooms_pool) {
2404 return false;
2405 }
2406
2407 // get current Joomla/WordPress User ID
2408 $now_user = JFactory::getUser();
2409 $store_ujid = property_exists($now_user, 'id') ? (int)$now_user->id : 0;
2410
2411 // forced booking reason, status validation and additional data
2412 $forced_reason = $this->get('forced_reason', '');
2413 $valid_statuses = ['confirmed', 'standby'];
2414 $status = in_array($status, $valid_statuses) ? $status : 'confirmed';
2415 $paymentmeth = $this->get('id_payment', '');
2416 $auto_paymeth = (bool) $this->get('auto_payment_method', false);
2417 $set_total = (float) $this->get('_total', 0);
2418 $set_taxes = (float) $this->get('_total_tax', 0);
2419
2420 // stay dates
2421 $now_ts = time();
2422 $checkin_ts = $this->get('checkin');
2423 $checkout_ts = $this->get('checkout');
2424 $realback_ts = $this->get('checkout_real', $checkout_ts);
2425
2426 // room
2427 $inj_room = $this->getRoom();
2428 $cust_cost = !empty($inj_room['cust_cost']) ? (float) $inj_room['cust_cost'] : 0;
2429 $room_cost = !empty($inj_room['room_cost']) ? (float) $inj_room['room_cost'] : 0;
2430 $id_price = !empty($inj_room['id_price']) ? (int) $inj_room['id_price'] : 0;
2431 $id_tax = !empty($inj_room['id_tax']) ? (int) $inj_room['id_tax'] : 0;
2432 $id_tariff = $this->get('id_tariff', 0);
2433
2434 // custom rate modifier per night
2435 $totalpnight = !empty($inj_room['total_or_pnight']) ? $inj_room['total_or_pnight'] : 'total';
2436 if ($cust_cost > 0.00 && !$set_closed && $totalpnight == 'pnight') {
2437 $cust_cost = $cust_cost * $daysdiff;
2438 }
2439
2440 // customer
2441 $cpin = VikBooking::getCPinInstance();
2442 $inj_customer = $this->getCustomer();
2443 $customer_id = !empty($inj_customer['id']) ? $inj_customer['id'] : 0;
2444 $customer_pin = !empty($inj_customer['pin']) ? $inj_customer['pin'] : '';
2445 $t_first_name = !empty($inj_customer['first_name']) ? $inj_customer['first_name'] : '';
2446 $t_last_name = !empty($inj_customer['last_name']) ? $inj_customer['last_name'] : '';
2447 $customer_data = !empty($inj_customer['data']) ? $inj_customer['data'] : '';
2448 $customer_email = !empty($inj_customer['email']) ? $inj_customer['email'] : '';
2449 $country_code = !empty($inj_customer['country']) ? $inj_customer['country'] : '';
2450 $phone_number = !empty($inj_customer['phone']) ? $inj_customer['phone'] : '';
2451
2452 if ($set_closed) {
2453 $customer_data = JText::translate('VBDBTEXTROOMCLOSED');
2454 }
2455
2456 // check for default customer raw data
2457 if (!$customer_data && $t_first_name) {
2458 // build a default raw data string
2459 $customer_data = "Name: {$t_first_name}\n";
2460 if ($t_last_name) {
2461 $customer_data .= "Last Name: {$t_last_name}\n";
2462 }
2463 if ($customer_email) {
2464 $customer_data .= "eMail: {$customer_email}\n";
2465 }
2466 if ($country_code) {
2467 $customer_data .= "Country: {$country_code}\n";
2468 }
2469 if ($phone_number) {
2470 $customer_data .= "Phone: {$phone_number}\n";
2471 }
2472 $customer_data = rtrim($customer_data, "\n");
2473 }
2474
2475 // generate booking SID
2476 $sid = VikBooking::getSecretLink();
2477
2478 // assign room specific unit
2479 $set_room_indexes = !$set_closed ? VikBooking::autoRoomUnit() : false;
2480 $num_rooms = $num_rooms > 0 ? $num_rooms : 1;
2481
2482 // occupancy and loop limits
2483 $forend = 1;
2484 $or_forend = 1;
2485 $adults_map = [];
2486 $children_map = [];
2487 if ($set_closed && empty($split_stay_data)) {
2488 $forend = $room['units'];
2489 } elseif ($num_rooms > 1 && $num_rooms <= $room['units'] && empty($split_stay_data)) {
2490 $forend = $num_rooms;
2491 $or_forend = $num_rooms;
2492 // assign adults/children proportionally
2493 if (($adults + $children) < $num_rooms) {
2494 // the number of guests does not make much sense but we build the maps anyway
2495 for ($r = 1; $r <= $or_forend; $r++) {
2496 $adults_map[$r] = $adults;
2497 $children_map[$r] = $children;
2498 }
2499 } else {
2500 $adults_per_room = floor(($adults / $num_rooms));
2501 $adults_left = ($adults % $num_rooms);
2502 $children_per_room = floor(($children / $num_rooms));
2503 $children_left = ($children % $num_rooms);
2504 for ($r = 1; $r <= $or_forend; $r++) {
2505 $adults_map[$r] = $adults_per_room;
2506 $children_map[$r] = $children_per_room;
2507 if ($r == $or_forend) {
2508 $adults_map[$r] += $adults_left;
2509 $children_map[$r] += $children_left;
2510 }
2511 }
2512 }
2513 }
2514
2515 // count total rooms booked
2516 $totalrooms = ($set_closed && $status == 'confirmed' ? count($rooms_pool) : ($num_rooms > 1 && $num_rooms <= $room['units'] ? $num_rooms : 1));
2517 $totalrooms = !empty($split_stay_data) ? count($split_stay_data) : $totalrooms;
2518
2519 // attempt to get the default payment method
2520 if (!$paymentmeth && ($auto_paymeth || $status == 'standby')) {
2521 // get the default payment method, if any
2522 $paymentmeth = $this->getDefaultPaymentMethod($auto_paymeth);
2523 }
2524
2525 // prepare booking record
2526 $booking = new stdClass;
2527 $booking->custdata = $customer_data;
2528 $booking->ts = $now_ts;
2529 $booking->status = $status;
2530 $booking->days = $daysdiff;
2531 $booking->checkin = $checkin_ts;
2532 $booking->checkout = $checkout_ts;
2533 $booking->custmail = $customer_email;
2534 $booking->sid = $sid;
2535 $booking->idpayment = $paymentmeth;
2536 $booking->ujid = (int)$store_ujid;
2537 $booking->roomsnum = $totalrooms;
2538 $booking->total = $set_total > 0 ? $set_total : null;
2539 if ($this->get('admin_notes')) {
2540 $booking->adminnotes = $this->get('admin_notes', '');
2541 }
2542 $booking->lang = VikBooking::guessBookingLangFromCountry($country_code);
2543 $booking->country = $country_code;
2544 $booking->tot_taxes = $set_taxes > 0 ? $set_taxes : null;
2545 $booking->phone = $phone_number;
2546 $booking->closure = ($status == 'standby' ? 0 : ($set_closed || $units_closed ? 1 : 0));
2547 $booking->split_stay = !empty($split_stay_data) ? 1 : 0;
2548
2549 // store reservation
2550 if ($status == 'confirmed') {
2551 // occupy the rooms
2552 $insertedbusy = [];
2553 if (empty($split_stay_data)) {
2554 // only when closing other rooms we have an array containing multiple rooms info
2555 foreach ($rooms_pool as $nowroom) {
2556 $nowforend = $set_closed ? $nowroom['units'] : $forend;
2557 for ($b = 1; $b <= $nowforend; $b++) {
2558 $busy_record = new stdClass;
2559 $busy_record->idroom = (int)$nowroom['id'];
2560 $busy_record->checkin = (int)$checkin_ts;
2561 $busy_record->checkout = (int)$checkout_ts;
2562 $busy_record->realback = (int)$realback_ts;
2563
2564 // store busy record
2565 $dbo->insertObject('#__vikbooking_busy', $busy_record, 'id');
2566
2567 if (isset($busy_record->id)) {
2568 $insertedbusy[] = $busy_record->id;
2569 }
2570 }
2571 }
2572 } else {
2573 // for split stay bookings we occupy the rooms on the individual stay dates
2574 foreach ($split_stay_data as $split_stay) {
2575 $busy_record = new stdClass;
2576 $busy_record->idroom = (int)$split_stay['idroom'];
2577 $busy_record->checkin = (int)$split_stay['checkin_ts'];
2578 $busy_record->checkout = (int)$split_stay['checkout_ts'];
2579 $busy_record->realback = (int)$split_stay['realback_ts'];
2580
2581 // store busy record
2582 $dbo->insertObject('#__vikbooking_busy', $busy_record, 'id');
2583
2584 if (isset($busy_record->id)) {
2585 $insertedbusy[] = $busy_record->id;
2586 }
2587 }
2588 }
2589
2590 if (!$insertedbusy) {
2591 $this->setError('No records were occupied');
2592 return false;
2593 }
2594
2595 // store booking record
2596 $dbo->insertObject('#__vikbooking_orders', $booking, 'id');
2597
2598 if (!isset($booking->id)) {
2599 $this->setError('Could not store the reservation record');
2600 return false;
2601 }
2602
2603 // get the newly generated booking ID
2604 $newoid = $booking->id;
2605
2606 // set the new booking ID
2607 $this->setNewBookingID($newoid);
2608
2609 if (!empty($split_stay_data)) {
2610 // save transient on db for split stay information
2611 VBOFactory::getConfig()->set('split_stay_' . $newoid, json_encode($split_stay_data));
2612 }
2613
2614 // check if some of the rooms booked have shared calendars
2615 VikBooking::updateSharedCalendars($newoid, [$room['id']], $checkin_ts, $checkout_ts);
2616
2617 // confirmation number
2618 $confirmnumber = VikBooking::generateConfirmNumber($newoid, true);
2619
2620 // store busy records/booking relations
2621 foreach ($insertedbusy as $lid) {
2622 $obusy_record = new stdClass;
2623 $obusy_record->idorder = (int)$newoid;
2624 $obusy_record->idbusy = (int)$lid;
2625
2626 // store busy relation record
2627 $dbo->insertObject('#__vikbooking_ordersbusy', $obusy_record, 'id');
2628 }
2629
2630 // write room records
2631 foreach ($rooms_pool as $rind => $nowroom) {
2632 $room_indexes_usemap = [];
2633 for ($r = 1; $r <= $or_forend; $r++) {
2634 // Assign room specific unit
2635 $info_room_avail = [
2636 'id' => $newoid,
2637 'checkin' => (!empty($nowroom['checkin_ts']) ? $nowroom['checkin_ts'] : $checkin_ts),
2638 'checkout' => (!empty($nowroom['checkout_ts']) ? $nowroom['checkout_ts'] : $checkout_ts),
2639 ];
2640 $room_indexes = $set_room_indexes === true ? VikBooking::getRoomUnitNumsAvailable($info_room_avail, $nowroom['id']) : [];
2641 $use_ind_key = 0;
2642 if ($room_indexes) {
2643 if (!array_key_exists($nowroom['id'], $room_indexes_usemap)) {
2644 $room_indexes_usemap[$nowroom['id']] = $use_ind_key;
2645 } else {
2646 $use_ind_key = $room_indexes_usemap[$nowroom['id']];
2647 }
2648 }
2649
2650 // room custom cost
2651 $or_cust_cost = $cust_cost > 0.00 ? $cust_cost : 0;
2652 $or_cust_cost = $or_forend > 1 && $or_cust_cost > 0 ? round(($or_cust_cost / $or_forend), 2) : $or_cust_cost;
2653 // room cost from website rate plan is always based on one room
2654 $or_room_cost = $room_cost > 0.00 ? $room_cost : 0;
2655 if (!empty($split_stay_data) && $cust_cost > 0) {
2656 // set the average cost per room in case of split stay
2657 $cost_per_room = ($cust_cost / count($split_stay_data));
2658 $or_cust_cost = round($cost_per_room, 2);
2659 if (isset($split_stay_data[$rind]) && isset($split_stay_data[$rind]['nights'])) {
2660 // count the average cost per room depending on the number of nights of stay
2661 $cost_per_room = $cust_cost / $daysdiff * $split_stay_data[$rind]['nights'];
2662 $or_cust_cost = round($cost_per_room, 2);
2663 }
2664 }
2665
2666 // room guests
2667 $room_adults = isset($adults_map[$r]) && empty($split_stay_data) ? $adults_map[$r] : $adults;
2668 $room_children = isset($children_map[$r]) && empty($split_stay_data) ? $children_map[$r] : $children;
2669
2670 // attempt to gather the children age for this room
2671 $room_children_age = null;
2672 if ($room_children && $children_age) {
2673 $children_age_pool = [];
2674 for ($ic = 0; $ic < $room_children; $ic++) {
2675 if (!$children_age) {
2676 $children_age_pool[] = 0;
2677 continue;
2678 }
2679 // shorten the list and push the current child age
2680 $current_child_age = array_shift($children_age);
2681 $children_age_pool[] = (int) $current_child_age;
2682 }
2683 $room_children_age = json_encode(['age' => $children_age_pool]);
2684 }
2685
2686 // store room record
2687 $room_record = new stdClass;
2688 $room_record->idorder = (int) $newoid;
2689 $room_record->idroom = (int) $nowroom['id'];
2690 $room_record->adults = $room_adults;
2691 $room_record->children = $room_children;
2692 $room_record->idtar = !empty($id_tariff) ? $id_tariff : null;
2693 $room_record->childrenage = $room_children_age;
2694 $room_record->t_first_name = $t_first_name;
2695 $room_record->t_last_name = $t_last_name;
2696 $room_record->roomindex = count($room_indexes) ? (int) $room_indexes[$use_ind_key] : null;
2697 $room_record->cust_cost = $cust_cost > 0.00 ? $or_cust_cost : null;
2698 $room_record->cust_idiva = $cust_cost > 0.00 && !empty($id_tax) ? $id_tax : null;
2699 $room_record->room_cost = $room_cost > 0.00 ? $or_room_cost : null;
2700
2701 $dbo->insertObject('#__vikbooking_ordersrooms', $room_record, 'id');
2702
2703 if (!isset($room_record->id)) {
2704 $this->setError('Could not store room reservation record for booking ID ' . $room_record->idorder);
2705 continue;
2706 }
2707
2708 // Assign room specific unit
2709 if ($room_indexes) {
2710 $room_indexes_usemap[$nowroom['id']]++;
2711 }
2712 }
2713 }
2714 } elseif ($status == 'standby') {
2715 // store booking record
2716 $dbo->insertObject('#__vikbooking_orders', $booking, 'id');
2717
2718 if (!isset($booking->id)) {
2719 $this->setError('Could not store the reservation record');
2720 return false;
2721 }
2722
2723 // get the newly generated booking ID
2724 $newoid = $booking->id;
2725
2726 // set the new booking ID
2727 $this->setNewBookingID($newoid);
2728
2729 if (!empty($split_stay_data)) {
2730 // save transient on db for split stay information
2731 VBOFactory::getConfig()->set('split_stay_' . $newoid, json_encode($split_stay_data));
2732 }
2733
2734 // write room records
2735 foreach ($rooms_pool as $rind => $nowroom) {
2736 for ($r = 1; $r <= $or_forend; $r++) {
2737 // room custom cost
2738 $or_cust_cost = $cust_cost > 0.00 ? $cust_cost : 0;
2739 $or_cust_cost = $or_forend > 1 && $or_cust_cost > 0 ? round(($or_cust_cost / $or_forend), 2) : $or_cust_cost;
2740 // room cost from website rate plan is always based on one room
2741 $or_room_cost = $room_cost > 0.00 ? $room_cost : 0;
2742 if (!empty($split_stay_data) && $cust_cost > 0) {
2743 // set the average cost per room in case of split stay
2744 $cost_per_room = ($cust_cost / count($split_stay_data));
2745 $or_cust_cost = round($cost_per_room, 2);
2746 if (isset($split_stay_data[$rind]) && isset($split_stay_data[$rind]['nights'])) {
2747 // count the average cost per room depending on the number of nights of stay
2748 $cost_per_room = $cust_cost / $daysdiff * $split_stay_data[$rind]['nights'];
2749 $or_cust_cost = round($cost_per_room, 2);
2750 }
2751 }
2752
2753 // room guests
2754 $room_adults = isset($adults_map[$r]) && empty($split_stay_data) ? $adults_map[$r] : $adults;
2755 $room_children = isset($children_map[$r]) && empty($split_stay_data) ? $children_map[$r] : $children;
2756
2757 // attempt to gather the children age for this room
2758 $room_children_age = null;
2759 if ($room_children && $children_age) {
2760 $children_age_pool = [];
2761 for ($ic = 0; $ic < $room_children; $ic++) {
2762 if (!$children_age) {
2763 $children_age_pool[] = 0;
2764 continue;
2765 }
2766 // shorten the list and push the current child age
2767 $current_child_age = array_shift($children_age);
2768 $children_age_pool[] = (int) $current_child_age;
2769 }
2770 $room_children_age = json_encode(['age' => $children_age_pool]);
2771 }
2772
2773 // store room record
2774 $room_record = new stdClass;
2775 $room_record->idorder = (int) $newoid;
2776 $room_record->idroom = (int) $nowroom['id'];
2777 $room_record->adults = $room_adults;
2778 $room_record->children = $room_children;
2779 $room_record->idtar = !empty($id_tariff) ? $id_tariff : null;
2780 $room_record->childrenage = $room_children_age;
2781 $room_record->t_first_name = $t_first_name;
2782 $room_record->t_last_name = $t_last_name;
2783 $room_record->cust_cost = $cust_cost > 0.00 ? $or_cust_cost : null;
2784 $room_record->cust_idiva = $cust_cost > 0.00 && !empty($id_tax) ? $id_tax : null;
2785 $room_record->room_cost = $room_cost > 0.00 ? $or_room_cost : null;
2786
2787 $dbo->insertObject('#__vikbooking_ordersrooms', $room_record, 'id');
2788
2789 if (!isset($room_record->id)) {
2790 $this->setError('Could not store room reservation record for booking ID ' . $room_record->idorder);
2791 continue;
2792 }
2793
2794 if (empty($split_stay_data)) {
2795 // lock room for pending status
2796 $tmplock_record = new stdClass;
2797 $tmplock_record->idroom = (int)$room['id'];
2798 $tmplock_record->checkin = $checkin_ts;
2799 $tmplock_record->checkout = $checkout_ts;
2800 $tmplock_record->until = VikBooking::getMinutesLock(true);
2801 $tmplock_record->realback = $realback_ts;
2802 $tmplock_record->idorder = (int)$newoid;
2803
2804 $dbo->insertObject('#__vikbooking_tmplock', $tmplock_record, 'id');
2805 }
2806 }
2807 }
2808
2809 if (!empty($split_stay_data)) {
2810 // lock rooms for pending status on proper stay dates
2811 foreach ($split_stay_data as $split_stay) {
2812 $tmplock_record = new stdClass;
2813 $tmplock_record->idroom = (int)$split_stay['idroom'];
2814 $tmplock_record->checkin = (int)$split_stay['checkin_ts'];
2815 $tmplock_record->checkout = (int)$split_stay['checkout_ts'];
2816 $tmplock_record->until = VikBooking::getMinutesLock(true);
2817 $tmplock_record->realback = (int)$split_stay['realback_ts'];
2818 $tmplock_record->idorder = (int)$newoid;
2819
2820 $dbo->insertObject('#__vikbooking_tmplock', $tmplock_record, 'id');
2821 }
2822 }
2823 }
2824
2825 $newoid = $this->getNewBookingID();
2826 if (!$newoid) {
2827 return false;
2828 }
2829
2830 // assign booking to customer
2831 if (!$cpin->getNewCustomerId() && !empty($customer_id)) {
2832 $cpin->setNewPin($customer_pin);
2833 $cpin->setNewCustomerId($customer_id);
2834 }
2835 $cpin->saveCustomerBooking($newoid);
2836
2837 // Booking History
2838 $history_obj = VikBooking::getBookingHistoryInstance($newoid);
2839 $forced_reason = !empty($forced_reason) ? " {$forced_reason}" : $forced_reason;
2840 $caller_id = $now_user->name ? "({$now_user->name})" : '';
2841 if ($this->getCaller()) {
2842 $caller_id = '(' . $this->getCaller() . ')';
2843 if ($this->getHistoryData()) {
2844 $history_obj->setExtraData($this->getHistoryData());
2845 }
2846 }
2847 $history_obj->store('NB', trim($caller_id . $forced_reason));
2848
2849 if ($status == 'confirmed' || ($status == 'standby' && class_exists('VCMRequestAvailability'))) {
2850 // Invoke Channel Manager
2851 $vcm_autosync = VikBooking::vcmAutoUpdate();
2852 if ($vcm_autosync > 0) {
2853 $vcm_obj = VikBooking::getVcmInvoker();
2854 $vcm_obj->setOids([$newoid])->setSyncType('new');
2855 $sync_result = $vcm_obj->doSync();
2856 if ($sync_result === false) {
2857 // set error message
2858 $vcm_err = $vcm_obj->getError();
2859 $this->setError(JText::translate('VBCHANNELMANAGERRESULTKO') . (!empty($vcm_err) ? ' - ' . $vcm_err : ''));
2860
2861 // return true because the booking was actually stored
2862 return true;
2863 }
2864 } elseif (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'synch.vikbooking.php')) {
2865 // set the necessary action to invoke VCM
2866 $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']);
2867
2868 $this->setChannelManagerAction(JText::translate('VBCHANNELMANAGERINVOKEASK') . ' <button type="button" class="btn btn-primary" onclick="document.location.href=\'' . $vcm_sync_url . '\';">' . JText::translate('VBCHANNELMANAGERSENDRQ') . '</button>');
2869 }
2870 }
2871
2872 if (VikBooking::isAdmin()) {
2873 /**
2874 * Trigger event to allow third party plugins to intercept the admin new booking event.
2875 *
2876 * @since 1.16.8 (J) - 1.6.8 (WP)
2877 */
2878 VBOFactory::getPlatform()->getDispatcher()->trigger('onAfterCreateNewBookingAdmin', [$newoid]);
2879 }
2880
2881 return true;
2882 }
2883
2884 /**
2885 * Attempts to find the default payment method to be assigned to a booking.
2886 *
2887 * @param bool $auto If true, it was requested to automatically find the best payment method.
2888 *
2889 * @return string The payment method string for the reservation "ID=Name", or an empty string.
2890 *
2891 * @since 1.17.3 (J) - 1.7.3 (WP)
2892 */
2893 protected function getDefaultPaymentMethod($auto = false)
2894 {
2895 $dbo = JFactory::getDbo();
2896
2897 $dbo->setQuery(
2898 $dbo->getQuery(true)
2899 ->select('*')
2900 ->from($dbo->qn('#__vikbooking_gpayments'))
2901 ->order($dbo->qn('published') . ' DESC')
2902 ->order($dbo->qn('ordering') . ' ASC')
2903 ->order($dbo->qn('setconfirmed') . ' ASC')
2904 ->order($dbo->qn('name') . ' ASC')
2905 );
2906
2907 $methods = $dbo->loadAssocList();
2908
2909 if ($auto) {
2910 // exclude all the offline or unpublished payment methods
2911 $methods = array_filter($methods, function($method) {
2912 return !((bool) intval($method['setconfirmed'])) && (bool) intval($method['published']);
2913 });
2914
2915 // reset array keys
2916 $methods = array_values($methods);
2917 }
2918
2919 if ($methods) {
2920 // default payment method found
2921 return sprintf('%s=%s', $methods[0]['id'], $methods[0]['name']);
2922 }
2923
2924 // nothing was found
2925 return '';
2926 }
2927 }
2928