PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.7
VikBooking Hotel Booking Engine & PMS v1.8.7
1.8.16 1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 All 37 releases
vikbooking / admin / helpers / src / model / reservation.php

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

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