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

1,175 lines 37.2 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 * Proxy for immediately getting the object and bind data.
30 *
31 * @param array|object $data optional data to bind.
32 * @param boolean $anew true for forcing a new instance.
33 *
34 * @return self
35 */
36 public static function getInstance($data = [], $anew = false)
37 {
38 if (is_null(static::$instance) || $anew) {
39 static::$instance = new static($data);
40 }
41
42 return static::$instance;
43 }
44
45 /**
46 * Sets the customer information.
47 *
48 * @param array $customer the customer array.
49 *
50 * @return self
51 */
52 public function setCustomer(array $customer = [])
53 {
54 $this->set('_customer', $customer);
55
56 return $this;
57 }
58
59 /**
60 * Returns the customer information.
61 *
62 * @return array
63 */
64 public function getCustomer()
65 {
66 return (array)$this->get('_customer', []);
67 }
68
69 /**
70 * Sets the room information.
71 *
72 * @param array $room the room array.
73 *
74 * @return self
75 */
76 public function setRoom(array $room = [])
77 {
78 $this->set('_room', $room);
79
80 return $this;
81 }
82
83 /**
84 * Returns the room information.
85 *
86 * @return array
87 */
88 public function getRoom()
89 {
90 return (array)$this->get('_room', []);
91 }
92
93 /**
94 * Sets the new booking ID created.
95 *
96 * @param int $bid the newly added record ID.
97 *
98 * @return self
99 */
100 protected function setNewBookingID($bid = 0)
101 {
102 $this->set('_newBookingID', $bid);
103
104 return $this;
105 }
106
107 /**
108 * Returns the new booking ID created, or 0.
109 *
110 * @return int
111 */
112 public function getNewBookingID()
113 {
114 return (int)$this->get('_newBookingID', 0);
115 }
116
117 /**
118 * Sets the VCM action to be performed in order to sync the availability.
119 *
120 * @param string $action the VCM action, usually an HTML link.
121 *
122 * @return self
123 */
124 protected function setChannelManagerAction($action = '')
125 {
126 $this->set('_vcmAction', $action);
127
128 return $this;
129 }
130
131 /**
132 * Returns the VCM action (if any) to sync the availability.
133 *
134 * @return string
135 */
136 public function getChannelManagerAction()
137 {
138 return $this->get('_vcmAction', '');
139 }
140
141 /**
142 * Sets the check-in and check-out times with hours and minutes.
143 *
144 * @return array list of check-in and check-out hours and minutes.
145 */
146 public function loadCheckinOutTimes()
147 {
148 static $times_loaded = null;
149
150 if ($times_loaded) {
151 return [
152 $this->get('checkin_h'),
153 $this->get('checkin_m'),
154 $this->get('checkout_h'),
155 $this->get('checkout_m'),
156 ];
157 }
158
159 $timeopst = VikBooking::getTimeOpenStore();
160 if (is_array($timeopst) && $timeopst) {
161 $opent = VikBooking::getHoursMinutes($timeopst[0]);
162 $closet = VikBooking::getHoursMinutes($timeopst[1]);
163 $hcheckin = $opent[0];
164 $mcheckin = $opent[1];
165 $hcheckout = $closet[0];
166 $mcheckout = $closet[1];
167 } else {
168 $hcheckin = 0;
169 $mcheckin = 0;
170 $hcheckout = 0;
171 $mcheckout = 0;
172 }
173
174 $this->set('checkin_h', $hcheckin);
175 $this->set('checkin_m', $mcheckin);
176 $this->set('checkout_h', $hcheckout);
177 $this->set('checkout_m', $mcheckout);
178
179 $times_loaded = 1;
180
181 return [
182 $hcheckin,
183 $mcheckin,
184 $hcheckout,
185 $mcheckout,
186 ];
187 }
188
189 /**
190 * Attempts to extract the Special Requests from the customer raw data.
191 *
192 * @return string
193 *
194 * @since 1.16.5 (J) - 1.6.5 (WP)
195 */
196 public function extractSpecialRequests()
197 {
198 $raw_cust_data = $this->get('custdata', '');
199 if (empty($raw_cust_data)) {
200 return '';
201 }
202
203 $special_requests = '';
204 if (preg_match("/(?:special_?requests:\s*)(.*?)$/is", $raw_cust_data, $match)) {
205 $special_requests = $match[1];
206 } elseif (preg_match("/(?:special_?request:\s*)(.*?)$/is", $raw_cust_data, $match)) {
207 $special_requests = $match[1];
208 } elseif (preg_match("/(?:special_?request\s*)(.*?)$/is", $raw_cust_data, $match)) {
209 $special_requests = $match[1];
210 } elseif (preg_match("/(?:" . JText::translate('ORDER_SPREQUESTS') . ":\s*)(.*?)$/is", $raw_cust_data, $match)) {
211 $special_requests = $match[1];
212 }
213
214 return $special_requests;
215 }
216
217 /**
218 * Creates a new reservation record after having constructed the
219 * object by properly injecting all the necessary booking information.
220 *
221 * @return bool
222 */
223 public function create()
224 {
225 if (!$this->canCreate()) {
226 $this->setError('Forbidden');
227 return false;
228 }
229
230 // availability helper
231 $av_helper = VikBooking::getAvailabilityInstance();
232
233 // validate mandatory fields
234 $room = $this->getRoom();
235 if (!$this->get('checkin') || !$this->get('checkout') || empty($room['id'])) {
236 $this->setError('Missing mandatory fields');
237 return false;
238 }
239
240 if ($this->get('checkin') >= $this->get('checkout')) {
241 $this->setError('Invalid dates');
242 return false;
243 }
244
245 // make sure we have the time for check-in and check-out
246 if (!$this->get('checkin_h') || !$this->get('checkout_h')) {
247 // make sure to set the times (hours/minutes) for check-in and check-out
248 $this->loadCheckinOutTimes();
249 }
250
251 // number of nights of stay
252 if (!$this->get('nights')) {
253 $this->set('nights', $av_helper->countNightsOfStay($this->get('checkin'), $this->get('checkout')));
254 }
255
256 // fetch and apply turnover time before doing anything else
257 $this->applyTurnover();
258
259 // if rate plan selected, get the tariff ID
260 $this->loadTariffID();
261
262 // get pool of rooms involved
263 $rooms_pool = $this->getRoomsPool();
264 if (!$rooms_pool) {
265 if ($this->getError() === false) {
266 // set generic error if not set already
267 $this->setError('No rooms involved in the reservation');
268 }
269 return false;
270 }
271
272 // check if the room is available
273 $room_available = $this->isRoomAvailable();
274 if (!$this->get('force_booking', 0) && !$this->get('set_closed', 0) && !$room_available) {
275 // no forcing, no closure and room fully booked results into an error message
276 $this->setError(JText::translate('VBBOOKNOTMADE'));
277 return false;
278 }
279
280 // detect if we are forcing the reservation
281 $this->detectForcedReason($room_available);
282
283 // store the customer information
284 $this->storeCustomer();
285
286 // calculate total amount and total tax
287 $this->calculateTotal();
288
289 // store booking and room-booking records
290 if (!$this->storeReservationRecords($rooms_pool)) {
291 if ($this->getError() === false) {
292 // set generic error if not set already
293 $this->setError('Could not create the reservation');
294 }
295 return false;
296 }
297
298 return true;
299 }
300
301 /**
302 * Tells whether the operation is authenticated. By default this
303 * model is only available in the administrator section of the site.
304 *
305 * @return bool
306 */
307 protected function canCreate()
308 {
309 if (!JFactory::getApplication()->isClient('administrator') && !$this->get('_isAdministrator')) {
310 return false;
311 }
312
313 return true;
314 }
315
316 /**
317 * Gets and sets the tariff ID if a rate plan was set.
318 *
319 * @return int the tariff ID or 0.
320 */
321 protected function loadTariffID()
322 {
323 $dbo = JFactory::getDbo();
324
325 $id_tariff = 0;
326 $room = $this->getRoom();
327 $daysdiff = (int)$this->get('nights', 1);
328
329 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', [])) {
330 $room['id_price'] = (int)$room['id_price'];
331
332 $q = "SELECT `id` FROM `#__vikbooking_dispcost` WHERE `idroom`={$room['id']} AND `days`={$daysdiff} AND `idprice`={$room['id_price']};";
333 $dbo->setQuery($q);
334 $id_tariff = $dbo->loadResult();
335 }
336
337 $this->set('id_tariff', (int)$id_tariff);
338
339 return (int)$id_tariff;
340 }
341
342 /**
343 * Applies the turnover time to the checkout timestamp and sets its value.
344 *
345 * @return int the turnover seconds applied.
346 */
347 protected function applyTurnover()
348 {
349 $turnover_secs = 0;
350 $checkout = $this->get('checkout', 0);
351
352 if ($checkout) {
353 // turnover time
354 $turnover_secs = VikBooking::getHoursRoomAvail() * 3600;
355
356 $this->set('checkout_real', ($checkout + $turnover_secs));
357 }
358
359 $this->set('turnover_secs', $turnover_secs);
360
361 return $turnover_secs;
362 }
363
364 /**
365 * Returns the details of a specific room ID.
366 *
367 * @param int $rid the room ID.
368 *
369 * @return array the record found or empty array.
370 */
371 protected function getRoomDetails($rid = null)
372 {
373 $all_rooms = VikBooking::getAvailabilityInstance()->loadRooms();
374
375 if (!$rid) {
376 $inj_room = $this->getRoom();
377 if (!empty($inj_room['id'])) {
378 $rid = $inj_room['id'];
379 }
380 }
381
382 if ($rid && isset($all_rooms[$rid])) {
383 return $all_rooms[$rid];
384 }
385
386 return [];
387 }
388
389 /**
390 * Gets the list of rooms involved in the reservation in case of closures.
391 *
392 * @return array the list of rooms involved
393 */
394 protected function getRoomsPool()
395 {
396 $room = $this->getRoom();
397 if (empty($room['id'])) {
398 return [];
399 }
400 $room = $this->getRoomDetails($room['id']);
401
402 // gather values
403 $set_close_others = (array)$this->get('close_others', []);
404 $split_stay_data = $this->get('split_stay', []);
405 $set_closed = (int)$this->get('set_closed');
406 $turnover_secs = $this->get('turnover_secs', 0);
407 $hcheckin = $this->get('checkin_h', 12);
408 $mcheckin = $this->get('checkin_m', 0);
409 $hcheckout = $this->get('checkout_h', 10);
410 $mcheckout = $this->get('checkout_m', 0);
411
412 $av_helper = VikBooking::getAvailabilityInstance();
413 $all_rooms = $av_helper->loadRooms();
414 $rooms_pool = [];
415 $closeothers = [];
416
417 if ($set_close_others && $set_closed) {
418 // prepend current room for closing
419 array_unshift($set_close_others, $room['id']);
420 }
421 $set_close_others = array_unique($set_close_others);
422
423 foreach ($set_close_others as $closeid) {
424 if (empty($closeid)) {
425 continue;
426 }
427 if ((int)$closeid === -1) {
428 // close all rooms
429 $closeothers = [];
430 foreach ($all_rooms as $cr) {
431 array_push($closeothers, $cr);
432 }
433 break;
434 }
435 foreach ($all_rooms as $cr) {
436 if ((int)$cr['id'] == (int)$closeid) {
437 // push the main room or one of the other rooms requested for closure
438 array_push($closeothers, $cr);
439 break;
440 }
441 }
442 }
443
444 if (!$closeothers || !$set_closed) {
445 $rooms_pool = [$room];
446 } else {
447 $rooms_pool = $closeothers;
448 }
449
450 // check split stay rooms booking
451 if (!empty($split_stay_data)) {
452 // reset pool and set it with the split stay rooms
453 $rooms_pool = [];
454 foreach ($split_stay_data as $sps_k => $split_stay) {
455 if (!isset($all_rooms[$split_stay['idroom']])) {
456 continue;
457 }
458 // calculate and set the exact check-in and check-out timestamps for this split-room
459 $split_stay['checkin_ts'] = VikBooking::getDateTimestamp($split_stay['checkin'], $hcheckin, $mcheckin);
460 $split_stay['checkout_ts'] = VikBooking::getDateTimestamp($split_stay['checkout'], $hcheckout, $mcheckout);
461 $split_stay['realback_ts'] = $turnover_secs + $split_stay['checkout_ts'];
462 $split_stay['nights'] = $av_helper->countNightsOfStay($split_stay['checkin_ts'], $split_stay['checkout_ts']);
463 $split_stay_data[$sps_k] = $split_stay;
464 // push room data to pool after storing additional information
465 $room_data = $all_rooms[$split_stay['idroom']];
466 $room_data['checkin_ts'] = $split_stay['checkin_ts'];
467 $room_data['checkout_ts'] = $split_stay['checkout_ts'];
468 $rooms_pool[] = $room_data;
469 }
470 if (!$rooms_pool) {
471 $this->setError('No valid rooms for the split stay booking');
472 return [];
473 }
474 // update split stay data manipulated
475 $this->set('split_stay', $split_stay_data);
476 }
477
478 return $rooms_pool;
479 }
480
481 /**
482 * Checks that the room is available on the requested dates.
483 * Call this method only after getting the rooms pool.
484 *
485 * @return bool
486 */
487 protected function isRoomAvailable()
488 {
489 $inj_room = $this->getRoom();
490 if (empty($inj_room['id'])) {
491 return false;
492 }
493 $room = $this->getRoomDetails($inj_room['id']);
494 if (!$room) {
495 return false;
496 }
497
498 $split_stay_data = $this->get('split_stay', []);
499 $set_closed = $this->get('set_closed', 0);
500 $num_rooms = $this->get('num_rooms', 1);
501
502 $room_available = true;
503 $all_rooms = VikBooking::getAvailabilityInstance()->loadRooms();
504
505 if (empty($split_stay_data)) {
506 // make sure the rooms are available
507 $check_units = $room['units'];
508 if ($num_rooms > 1 && $num_rooms <= $room['units'] && !$set_closed) {
509 // only when non closing the room we check the availability for the units requested for booking
510 $check_units = $room['units'] - $num_rooms + 1;
511 }
512 $room_available = VikBooking::roomBookable($room['id'], $check_units, $this->get('checkin', 0), $this->get('checkout', 0));
513 } else {
514 // make sure the rooms for the split stay are available
515 foreach ($split_stay_data as $split_stay) {
516 if (!isset($all_rooms[$split_stay['idroom']])) {
517 $room_available = false;
518 break;
519 }
520 $room_available = $room_available && VikBooking::roomBookable($split_stay['idroom'], $all_rooms[$split_stay['idroom']]['units'], $split_stay['checkin_ts'], $split_stay['checkout_ts']);
521 }
522 }
523
524 return $room_available;
525 }
526
527 /**
528 * In case the reservation is forced or is a closure, we detect the
529 * forced reason to eventually attach it to the booking history.
530 *
531 * @param bool $room_available whether the room is available.
532 *
533 * @return void
534 */
535 protected function detectForcedReason($room_available = true)
536 {
537 $split_stay_data = $this->get('split_stay', []);
538 $force_booking = $this->get('force_booking', 0);
539 $set_closed = $this->get('set_closed', 0);
540
541 $forced_reason = '';
542 $all_rooms = VikBooking::getAvailabilityInstance()->loadRooms();
543
544 if (empty($split_stay_data)) {
545 // eventually build string for the description of the history event
546 if (($force_booking || $set_closed) && !$room_available) {
547 $forced_reason = JText::translate('VBO_FORCED_BOOKDATES');
548 }
549 } else {
550 // set "split stay" as the description of the history event
551 $forced_reason = JText::translate('VBO_SPLIT_STAY') . "\n";
552 foreach ($split_stay_data as $sps_k => $split_stay) {
553 // describe the split stay for each room
554 if (!isset($all_rooms[$split_stay['idroom']])) {
555 continue;
556 }
557 $room_stay_nights = $split_stay['nights'];
558 $forced_reason .= $all_rooms[$split_stay['idroom']]['name'] . ': ' . $room_stay_nights . ' ' . ($room_stay_nights > 1 ? JText::translate('VBDAYS') : JText::translate('VBDAY')) . ', ';
559 $forced_reason .= $split_stay['checkin'] . ' - ' . $split_stay['checkout'] . "\n";
560 }
561 $forced_reason = rtrim($forced_reason, "\n");
562 }
563
564 $this->set('forced_reason', $forced_reason);
565 }
566
567 /**
568 * Stores the customer information to a new or existing record.
569 * In case of success, the customer ID property is updated.
570 * The customer shall be stored before the reservation records.
571 *
572 * @return bool
573 */
574 protected function storeCustomer()
575 {
576 $dbo = JFactory::getDbo();
577
578 $inj_customer = $this->getCustomer();
579 $first_name = !empty($inj_customer['first_name']) ? $inj_customer['first_name'] : '';
580 $last_name = !empty($inj_customer['last_name']) ? $inj_customer['last_name'] : '';
581 $custdata = !empty($inj_customer['data']) ? $inj_customer['data'] : '';
582 $email = !empty($inj_customer['email']) ? $inj_customer['email'] : '';
583 $country = !empty($inj_customer['country']) ? $inj_customer['country'] : '';
584 $phone = !empty($inj_customer['phone']) ? $inj_customer['phone'] : '';
585
586 // custom fields
587 $q = "SELECT * FROM `#__vikbooking_custfields` ORDER BY `ordering` ASC;";
588 $dbo->setQuery($q);
589 $all_cfields = $dbo->loadAssocList();
590
591 $customer_cfields = [];
592 $customer_extrainfo = [];
593 $custdata_parts = explode("\n", $custdata);
594 foreach ($custdata_parts as $cdataline) {
595 if (!strlen(trim($cdataline))) {
596 continue;
597 }
598 $cdata_parts = explode(':', $cdataline);
599 if (count($cdata_parts) < 2 || !strlen(trim($cdata_parts[0])) || !strlen(trim($cdata_parts[1]))) {
600 continue;
601 }
602 foreach ($all_cfields as $cf) {
603 $needle = JText::translate($cf['name']);
604 if (!empty($needle) && strpos($cdata_parts[0], $needle) !== false && !array_key_exists($cf['id'], $customer_cfields) && $cf['type'] != 'country') {
605 $user_input_val = trim($cdata_parts[1]);
606 $customer_cfields[$cf['id']] = $user_input_val;
607 if (!empty($cf['flag'])) {
608 $customer_extrainfo[$cf['flag']] = $user_input_val;
609 } elseif ($cf['type'] == 'state') {
610 $customer_extrainfo['state'] = $user_input_val;
611 }
612 break;
613 }
614 }
615 }
616
617 $cpin = VikBooking::getCPinInstance();
618 $cpin->is_admin = true;
619 $cpin->setCustomerExtraInfo($customer_extrainfo);
620 $cpin->saveCustomerDetails($first_name, $last_name, $email, $phone, $country, $customer_cfields);
621
622 $customer_id = $cpin->getNewCustomerId();
623 if (!$customer_id) {
624 return false;
625 }
626
627 $inj_customer['id'] = $customer_id;
628 $this->setCustomer($inj_customer);
629
630 return true;
631 }
632
633 /**
634 * Returns the calculated total booking amount and total taxes.
635 * Sets the necessary properties with the calculated amounts.
636 *
637 * @return array list of booking total amount and total taxes.
638 */
639 protected function calculateTotal()
640 {
641 // the values to calculate
642 $set_total = 0;
643 $set_taxes = 0;
644
645 $dbo = JFactory::getDbo();
646
647 // get data
648 $inj_room = $this->getRoom();
649 $set_closed = $this->get('set_closed', 0);
650 $daysdiff = (int)$this->get('nights', 1);
651 $num_rooms = (int)$this->get('num_rooms', 1);
652 $totalpnight = !empty($inj_room['total_or_pnight']) ? $inj_room['total_or_pnight'] : 'total';
653 $cust_cost = !empty($inj_room['cust_cost']) ? (float)$inj_room['cust_cost'] : 0;
654 $room_cost = !empty($inj_room['room_cost']) ? (float)$inj_room['room_cost'] : 0;
655 $id_price = !empty($inj_room['id_price']) ? (int)$inj_room['id_price'] : 0;
656 $id_tax = !empty($inj_room['id_tax']) ? (int)$inj_room['id_tax'] : 0;
657
658 $split_stay_data = $this->get('split_stay', []);
659
660 $room = $this->getRoomDetails();
661 if (!$room) {
662 return [$set_total, $set_taxes];
663 }
664
665 if ($cust_cost > 0 && !$set_closed) {
666 // custom cost can be per night
667 if ($totalpnight == 'pnight') {
668 $cust_cost = $cust_cost * $daysdiff;
669 }
670 $set_total = $cust_cost;
671
672 // apply taxes, if necessary
673 if ($id_tax) {
674 $q = "SELECT `i`.`aliq`,`i`.`taxcap` FROM `#__vikbooking_iva` AS `i` WHERE `i`.`id`=" . $id_tax . ";";
675 $dbo->setQuery($q);
676 $taxdata = $dbo->loadAssoc();
677 if ($taxdata) {
678 $aliq = $taxdata['aliq'];
679 if (floatval($aliq) > 0.00) {
680 if (!VikBooking::ivaInclusa()) {
681 // add tax to the total amount
682 $subt = 100 + (float)$aliq;
683 $set_total = ($set_total * $subt / 100);
684 /**
685 * Tax Cap implementation for prices tax excluded (most common).
686 *
687 * @since 1.12 (J) - 1.2 (WP)
688 */
689 if ($taxdata['taxcap'] > 0 && ($set_total - $cust_cost) > $taxdata['taxcap']) {
690 $set_total = ($cust_cost + $taxdata['taxcap']);
691 }
692 // calculate tax
693 $set_taxes = $set_total - $cust_cost;
694 } else {
695 // calculate tax
696 $cost_minus_tax = VikBooking::sayPackageMinusIva($cust_cost, $id_tax);
697 $set_taxes += ($cust_cost - $cost_minus_tax);
698 }
699 }
700 }
701 }
702 } elseif (!empty($id_price) && $room_cost > 0 && !$set_closed && empty($split_stay_data)) {
703 // one website rate plan was selected, so we calculate total and taxes
704 $set_total = $room_cost;
705
706 // find tax rate assigned to this rate plan
707 $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 . ";";
708 $dbo->setQuery($q);
709 $taxdata = $dbo->loadAssoc();
710 if ($taxdata) {
711 $aliq = $taxdata['aliq'];
712 if (floatval($aliq) > 0.00) {
713 if (!VikBooking::ivaInclusa()) {
714 // add tax to the total amount
715 $subt = 100 + (float)$aliq;
716 $set_total = ($set_total * $subt / 100);
717 /**
718 * Tax Cap implementation for prices tax excluded (most common).
719 *
720 * @since 1.12 (J) - 1.2 (WP)
721 */
722 if ($taxdata['taxcap'] > 0 && ($set_total - $room_cost) > $taxdata['taxcap']) {
723 $set_total = ($room_cost + $taxdata['taxcap']);
724 }
725 // calculate tax
726 $set_taxes = $set_total - $room_cost;
727 } else {
728 // calculate tax
729 $cost_minus_tax = VikBooking::sayPackageMinusIva($room_cost, $taxdata['idiva']);
730 $set_taxes += ($room_cost - $cost_minus_tax);
731 }
732 }
733 }
734
735 // total and taxes should be multiplied by the number of rooms booked when using a website rate plan
736 if ($set_closed) {
737 $set_total *= $room['units'];
738 $set_taxes *= $room['units'];
739 } elseif ($num_rooms > 1 && $num_rooms <= $room['units']) {
740 $set_total *= $num_rooms;
741 $set_taxes *= $num_rooms;
742 }
743 }
744
745 // set values
746 $this->set('_total', $set_total);
747 $this->set('_total_tax', $set_taxes);
748
749 return [$set_total, $set_taxes];
750 }
751
752 /**
753 * Stores the booking and room-booking records.
754 * If no errors, the newly generated booking id is set.
755 *
756 * @param array $rooms_pool list of rooms involved.
757 *
758 * @return bool
759 */
760 protected function storeReservationRecords(array $rooms_pool)
761 {
762 $dbo = JFactory::getDbo();
763
764 $set_closed = $this->get('set_closed', 0);
765 $units_closed = $this->get('units_closed', 0);
766 $daysdiff = (int)$this->get('nights', 1);
767 $num_rooms = (int)$this->get('num_rooms', 1);
768 $adults = (int)$this->get('adults', 1);
769 $children = (int)$this->get('children', 0);
770 $status = $this->get('status', 'confirmed');
771
772 $split_stay_data = $this->get('split_stay', []);
773 $room = $this->getRoomDetails();
774 if (!$room || !$rooms_pool) {
775 return false;
776 }
777
778 // get current Joomla/WordPress User ID
779 $now_user = JFactory::getUser();
780 $store_ujid = property_exists($now_user, 'id') ? (int)$now_user->id : 0;
781
782 // forced booking reason, status validation and additional data
783 $forced_reason = $this->get('forced_reason', '');
784 $valid_statuses = ['confirmed', 'standby'];
785 $status = in_array($status, $valid_statuses) ? $status : 'confirmed';
786 $paymentmeth = $this->get('id_payment', '');
787 $set_total = (float)$this->get('_total', 0);
788 $set_taxes = (float)$this->get('_total_tax', 0);
789
790 // stay dates
791 $now_ts = time();
792 $checkin_ts = $this->get('checkin');
793 $checkout_ts = $this->get('checkout');
794 $realback_ts = $this->get('checkout_real', $checkout_ts);
795
796 // room
797 $inj_room = $this->getRoom();
798 $cust_cost = !empty($inj_room['cust_cost']) ? (float)$inj_room['cust_cost'] : 0;
799 $room_cost = !empty($inj_room['room_cost']) ? (float)$inj_room['room_cost'] : 0;
800 $id_price = !empty($inj_room['id_price']) ? (int)$inj_room['id_price'] : 0;
801 $id_tax = !empty($inj_room['id_tax']) ? (int)$inj_room['id_tax'] : 0;
802 $id_tariff = $this->get('id_tariff', 0);
803
804 // custom rate modifier per night
805 $totalpnight = !empty($inj_room['total_or_pnight']) ? $inj_room['total_or_pnight'] : 'total';
806 if ($cust_cost > 0.00 && !$set_closed && $totalpnight == 'pnight') {
807 $cust_cost = $cust_cost * $daysdiff;
808 }
809
810 // customer
811 $cpin = VikBooking::getCPinInstance();
812 $inj_customer = $this->getCustomer();
813 $customer_id = !empty($inj_customer['id']) ? $inj_customer['id'] : 0;
814 $customer_pin = !empty($inj_customer['pin']) ? $inj_customer['pin'] : '';
815 $t_first_name = !empty($inj_customer['first_name']) ? $inj_customer['first_name'] : '';
816 $t_last_name = !empty($inj_customer['last_name']) ? $inj_customer['last_name'] : '';
817 $customer_data = !empty($inj_customer['data']) ? $inj_customer['data'] : '';
818 $customer_email = !empty($inj_customer['email']) ? $inj_customer['email'] : '';
819 $country_code = !empty($inj_customer['country']) ? $inj_customer['country'] : '';
820 $phone_number = !empty($inj_customer['phone']) ? $inj_customer['phone'] : '';
821
822 if ($set_closed) {
823 $customer_data = JText::translate('VBDBTEXTROOMCLOSED');
824 }
825
826 // generate booking SID
827 $sid = VikBooking::getSecretLink();
828
829 // assign room specific unit
830 $set_room_indexes = !$set_closed ? VikBooking::autoRoomUnit() : false;
831 $num_rooms = $num_rooms > 0 ? $num_rooms : 1;
832
833 // occupancy and loop limits
834 $forend = 1;
835 $or_forend = 1;
836 $adults_map = [];
837 $children_map = [];
838 if ($set_closed && empty($split_stay_data)) {
839 $forend = $room['units'];
840 } elseif ($num_rooms > 1 && $num_rooms <= $room['units'] && empty($split_stay_data)) {
841 $forend = $num_rooms;
842 $or_forend = $num_rooms;
843 // assign adults/children proportionally
844 if (($adults + $children) < $num_rooms) {
845 // the number of guests does not make much sense but we build the maps anyway
846 for ($r = 1; $r <= $or_forend; $r++) {
847 $adults_map[$r] = $adults;
848 $children_map[$r] = $children;
849 }
850 } else {
851 $adults_per_room = floor(($adults / $num_rooms));
852 $adults_left = ($adults % $num_rooms);
853 $children_per_room = floor(($children / $num_rooms));
854 $children_left = ($children % $num_rooms);
855 for ($r = 1; $r <= $or_forend; $r++) {
856 $adults_map[$r] = $adults_per_room;
857 $children_map[$r] = $children_per_room;
858 if ($r == $or_forend) {
859 $adults_map[$r] += $adults_left;
860 $children_map[$r] += $children_left;
861 }
862 }
863 }
864 }
865
866 // count total rooms booked
867 $totalrooms = ($set_closed && $status == 'confirmed' ? count($rooms_pool) : ($num_rooms > 1 && $num_rooms <= $room['units'] ? $num_rooms : 1));
868 $totalrooms = !empty($split_stay_data) ? count($split_stay_data) : $totalrooms;
869
870 // prepare booking record
871 $booking = new stdClass;
872 $booking->custdata = $customer_data;
873 $booking->ts = $now_ts;
874 $booking->status = $status;
875 $booking->days = $daysdiff;
876 $booking->checkin = $checkin_ts;
877 $booking->checkout = $checkout_ts;
878 $booking->custmail = $customer_email;
879 $booking->sid = $sid;
880 $booking->idpayment = $paymentmeth;
881 $booking->ujid = (int)$store_ujid;
882 $booking->roomsnum = $totalrooms;
883 $booking->total = $set_total > 0 ? $set_total : null;
884 $booking->lang = VikBooking::guessBookingLangFromCountry($country_code);
885 $booking->country = $country_code;
886 $booking->tot_taxes = $set_taxes > 0 ? $set_taxes : null;
887 $booking->phone = $phone_number;
888 $booking->closure = ($status == 'standby' ? 0 : ($set_closed || $units_closed ? 1 : 0));
889 $booking->split_stay = !empty($split_stay_data) ? 1 : 0;
890
891 // store reservation
892 if ($status == 'confirmed') {
893 // occupy the rooms
894 $insertedbusy = [];
895 if (empty($split_stay_data)) {
896 // only when closing other rooms we have an array containing multiple rooms info
897 foreach ($rooms_pool as $nowroom) {
898 $nowforend = $set_closed ? $nowroom['units'] : $forend;
899 for ($b = 1; $b <= $nowforend; $b++) {
900 $busy_record = new stdClass;
901 $busy_record->idroom = (int)$nowroom['id'];
902 $busy_record->checkin = (int)$checkin_ts;
903 $busy_record->checkout = (int)$checkout_ts;
904 $busy_record->realback = (int)$realback_ts;
905
906 // store busy record
907 $dbo->insertObject('#__vikbooking_busy', $busy_record, 'id');
908
909 if (isset($busy_record->id)) {
910 $insertedbusy[] = $busy_record->id;
911 }
912 }
913 }
914 } else {
915 // for split stay bookings we occupy the rooms on the individual stay dates
916 foreach ($split_stay_data as $split_stay) {
917 $busy_record = new stdClass;
918 $busy_record->idroom = (int)$split_stay['idroom'];
919 $busy_record->checkin = (int)$split_stay['checkin_ts'];
920 $busy_record->checkout = (int)$split_stay['checkout_ts'];
921 $busy_record->realback = (int)$split_stay['realback_ts'];
922
923 // store busy record
924 $dbo->insertObject('#__vikbooking_busy', $busy_record, 'id');
925
926 if (isset($busy_record->id)) {
927 $insertedbusy[] = $busy_record->id;
928 }
929 }
930 }
931
932 if (!$insertedbusy) {
933 $this->setError('No records were occupied');
934 return false;
935 }
936
937 // store booking record
938 $dbo->insertObject('#__vikbooking_orders', $booking, 'id');
939
940 if (!isset($booking->id)) {
941 $this->setError('Could not store the reservation record');
942 return false;
943 }
944
945 // get the newly generated booking ID
946 $newoid = $booking->id;
947
948 // set the new booking ID
949 $this->setNewBookingID($newoid);
950
951 if (!empty($split_stay_data)) {
952 // save transient on db for split stay information
953 VBOFactory::getConfig()->set('split_stay_' . $newoid, json_encode($split_stay_data));
954 }
955
956 // check if some of the rooms booked have shared calendars
957 VikBooking::updateSharedCalendars($newoid, [$room['id']], $checkin_ts, $checkout_ts);
958
959 // confirmation number
960 $confirmnumber = VikBooking::generateConfirmNumber($newoid, true);
961
962 // store busy records/booking relations
963 foreach ($insertedbusy as $lid) {
964 $obusy_record = new stdClass;
965 $obusy_record->idorder = (int)$newoid;
966 $obusy_record->idbusy = (int)$lid;
967
968 // store busy relation record
969 $dbo->insertObject('#__vikbooking_ordersbusy', $obusy_record, 'id');
970 }
971
972 // write room records
973 foreach ($rooms_pool as $rind => $nowroom) {
974 $room_indexes_usemap = [];
975 for ($r = 1; $r <= $or_forend; $r++) {
976 // Assign room specific unit
977 $info_room_avail = [
978 'id' => $newoid,
979 'checkin' => (!empty($nowroom['checkin_ts']) ? $nowroom['checkin_ts'] : $checkin_ts),
980 'checkout' => (!empty($nowroom['checkout_ts']) ? $nowroom['checkout_ts'] : $checkout_ts),
981 ];
982 $room_indexes = $set_room_indexes === true ? VikBooking::getRoomUnitNumsAvailable($info_room_avail, $nowroom['id']) : [];
983 $use_ind_key = 0;
984 if ($room_indexes) {
985 if (!array_key_exists($nowroom['id'], $room_indexes_usemap)) {
986 $room_indexes_usemap[$nowroom['id']] = $use_ind_key;
987 } else {
988 $use_ind_key = $room_indexes_usemap[$nowroom['id']];
989 }
990 }
991
992 // room custom cost
993 $or_cust_cost = $cust_cost > 0.00 ? $cust_cost : 0;
994 $or_cust_cost = $or_forend > 1 && $or_cust_cost > 0 ? round(($or_cust_cost / $or_forend), 2) : $or_cust_cost;
995 // room cost from website rate plan is always based on one room
996 $or_room_cost = $room_cost > 0.00 ? $room_cost : 0;
997 if (!empty($split_stay_data) && $cust_cost > 0) {
998 // set the average cost per room in case of split stay
999 $cost_per_room = ($cust_cost / count($split_stay_data));
1000 $or_cust_cost = round($cost_per_room, 2);
1001 if (isset($split_stay_data[$rind]) && isset($split_stay_data[$rind]['nights'])) {
1002 // count the average cost per room depending on the number of nights of stay
1003 $cost_per_room = $cust_cost / $daysdiff * $split_stay_data[$rind]['nights'];
1004 $or_cust_cost = round($cost_per_room, 2);
1005 }
1006 }
1007
1008 // room guests
1009 $room_adults = isset($adults_map[$r]) && empty($split_stay_data) ? $adults_map[$r] : $adults;
1010 $room_children = isset($children_map[$r]) && empty($split_stay_data) ? $children_map[$r] : $children;
1011
1012 // store room record
1013 $room_record = new stdClass;
1014 $room_record->idorder = (int)$newoid;
1015 $room_record->idroom = (int)$nowroom['id'];
1016 $room_record->adults = $room_adults;
1017 $room_record->children = $room_children;
1018 $room_record->idtar = !empty($id_tariff) ? $id_tariff : null;
1019 $room_record->t_first_name = $t_first_name;
1020 $room_record->t_last_name = $t_last_name;
1021 $room_record->roomindex = count($room_indexes) ? (int)$room_indexes[$use_ind_key] : null;
1022 $room_record->cust_cost = $cust_cost > 0.00 ? $or_cust_cost : null;
1023 $room_record->cust_idiva = $cust_cost > 0.00 && !empty($id_tax) ? $id_tax : null;
1024 $room_record->room_cost = $room_cost > 0.00 ? $or_room_cost : null;
1025
1026 $dbo->insertObject('#__vikbooking_ordersrooms', $room_record, 'id');
1027
1028 if (!isset($room_record->id)) {
1029 $this->setError('Could not store room reservation record for booking ID ' . $room_record->idorder);
1030 continue;
1031 }
1032
1033 // Assign room specific unit
1034 if ($room_indexes) {
1035 $room_indexes_usemap[$nowroom['id']]++;
1036 }
1037 }
1038 }
1039 } elseif ($status == 'standby') {
1040 // store booking record
1041 $dbo->insertObject('#__vikbooking_orders', $booking, 'id');
1042
1043 if (!isset($booking->id)) {
1044 $this->setError('Could not store the reservation record');
1045 return false;
1046 }
1047
1048 // get the newly generated booking ID
1049 $newoid = $booking->id;
1050
1051 // set the new booking ID
1052 $this->setNewBookingID($newoid);
1053
1054 if (!empty($split_stay_data)) {
1055 // save transient on db for split stay information
1056 VBOFactory::getConfig()->set('split_stay_' . $newoid, json_encode($split_stay_data));
1057 }
1058
1059 // write room records
1060 foreach ($rooms_pool as $rind => $nowroom) {
1061 for ($r = 1; $r <= $or_forend; $r++) {
1062 // room custom cost
1063 $or_cust_cost = $cust_cost > 0.00 ? $cust_cost : 0;
1064 $or_cust_cost = $or_forend > 1 && $or_cust_cost > 0 ? round(($or_cust_cost / $or_forend), 2) : $or_cust_cost;
1065 // room cost from website rate plan is always based on one room
1066 $or_room_cost = $room_cost > 0.00 ? $room_cost : 0;
1067 if (!empty($split_stay_data) && $cust_cost > 0) {
1068 // set the average cost per room in case of split stay
1069 $cost_per_room = ($cust_cost / count($split_stay_data));
1070 $or_cust_cost = round($cost_per_room, 2);
1071 if (isset($split_stay_data[$rind]) && isset($split_stay_data[$rind]['nights'])) {
1072 // count the average cost per room depending on the number of nights of stay
1073 $cost_per_room = $cust_cost / $daysdiff * $split_stay_data[$rind]['nights'];
1074 $or_cust_cost = round($cost_per_room, 2);
1075 }
1076 }
1077
1078 // room guests
1079 $room_adults = isset($adults_map[$r]) && empty($split_stay_data) ? $adults_map[$r] : $adults;
1080 $room_children = isset($children_map[$r]) && empty($split_stay_data) ? $children_map[$r] : $children;
1081
1082 // store room record
1083 $room_record = new stdClass;
1084 $room_record->idorder = (int)$newoid;
1085 $room_record->idroom = (int)$nowroom['id'];
1086 $room_record->adults = $room_adults;
1087 $room_record->children = $room_children;
1088 $room_record->idtar = !empty($id_tariff) ? $id_tariff : null;
1089 $room_record->t_first_name = $t_first_name;
1090 $room_record->t_last_name = $t_last_name;
1091 $room_record->cust_cost = $cust_cost > 0.00 ? $or_cust_cost : null;
1092 $room_record->cust_idiva = $cust_cost > 0.00 && !empty($id_tax) ? $id_tax : null;
1093 $room_record->room_cost = $room_cost > 0.00 ? $or_room_cost : null;
1094
1095 $dbo->insertObject('#__vikbooking_ordersrooms', $room_record, 'id');
1096
1097 if (!isset($room_record->id)) {
1098 $this->setError('Could not store room reservation record for booking ID ' . $room_record->idorder);
1099 continue;
1100 }
1101
1102 if (empty($split_stay_data)) {
1103 // lock room for pending status
1104 $tmplock_record = new stdClass;
1105 $tmplock_record->idroom = (int)$room['id'];
1106 $tmplock_record->checkin = $checkin_ts;
1107 $tmplock_record->checkout = $checkout_ts;
1108 $tmplock_record->until = VikBooking::getMinutesLock(true);
1109 $tmplock_record->realback = $realback_ts;
1110 $tmplock_record->idorder = (int)$newoid;
1111
1112 $dbo->insertObject('#__vikbooking_tmplock', $tmplock_record, 'id');
1113 }
1114 }
1115 }
1116
1117 if (!empty($split_stay_data)) {
1118 // lock rooms for pending status on proper stay dates
1119 foreach ($split_stay_data as $split_stay) {
1120 $tmplock_record = new stdClass;
1121 $tmplock_record->idroom = (int)$split_stay['idroom'];
1122 $tmplock_record->checkin = (int)$split_stay['checkin_ts'];
1123 $tmplock_record->checkout = (int)$split_stay['checkout_ts'];
1124 $tmplock_record->until = VikBooking::getMinutesLock(true);
1125 $tmplock_record->realback = (int)$split_stay['realback_ts'];
1126 $tmplock_record->idorder = (int)$newoid;
1127
1128 $dbo->insertObject('#__vikbooking_tmplock', $tmplock_record, 'id');
1129 }
1130 }
1131 }
1132
1133 $newoid = $this->getNewBookingID();
1134 if (!$newoid) {
1135 return false;
1136 }
1137
1138 // assign booking to customer
1139 if (!$cpin->getNewCustomerId() && !empty($customer_id)) {
1140 $cpin->setNewPin($customer_pin);
1141 $cpin->setNewCustomerId($customer_id);
1142 }
1143 $cpin->saveCustomerBooking($newoid);
1144
1145 // Booking History
1146 $forced_reason = !empty($forced_reason) ? " {$forced_reason}" : $forced_reason;
1147 VikBooking::getBookingHistoryInstance()->setBid($newoid)->store('NB', "({$now_user->name}){$forced_reason}");
1148
1149 if ($status == 'confirmed' || ($status == 'standby' && class_exists('VCMRequestAvailability'))) {
1150 // Invoke Channel Manager
1151 $vcm_autosync = VikBooking::vcmAutoUpdate();
1152 if ($vcm_autosync > 0) {
1153 $vcm_obj = VikBooking::getVcmInvoker();
1154 $vcm_obj->setOids([$newoid])->setSyncType('new');
1155 $sync_result = $vcm_obj->doSync();
1156 if ($sync_result === false) {
1157 // set error message
1158 $vcm_err = $vcm_obj->getError();
1159 $this->setError(JText::translate('VBCHANNELMANAGERRESULTKO') . (!empty($vcm_err) ? ' - ' . $vcm_err : ''));
1160
1161 // return true because the booking was actually stored
1162 return true;
1163 }
1164 } elseif (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'synch.vikbooking.php')) {
1165 // set the necessary action to invoke VCM
1166 $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']);
1167
1168 $this->setChannelManagerAction(JText::translate('VBCHANNELMANAGERINVOKEASK') . ' <button type="button" class="btn btn-primary" onclick="document.location.href=\'' . $vcm_sync_url . '\';">' . JText::translate('VBCHANNELMANAGERSENDRQ') . '</button>');
1169 }
1170 }
1171
1172 return true;
1173 }
1174 }
1175