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

1,174 lines 37.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package VikBooking
4 * @subpackage core
5 * @author E4J s.r.l.
6 * @copyright Copyright (C) 2023 E4J s.r.l. All Rights Reserved.
7 * @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
8 * @link https://vikwp.com
9 */
10
11 // No direct access
12 defined('ABSPATH') or die('No script kiddies please!');
13
14 /**
15 * VikBooking reservation model.
16 *
17 * @since 1.16.0 (J) - 1.6.0 (WP)
18 */
19 class VBOModelReservation extends JObject
20 {
21 /**
22 * The singleton instance of the class.
23 *
24 * @var VBOModelReservation
25 */
26 private static $instance = null;
27
28 /**
29 * 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 $daysdiff = (int)$this->get('nights', 1);
766 $num_rooms = (int)$this->get('num_rooms', 1);
767 $adults = (int)$this->get('adults', 1);
768 $children = (int)$this->get('children', 0);
769 $status = $this->get('status', 'confirmed');
770
771 $split_stay_data = $this->get('split_stay', []);
772 $room = $this->getRoomDetails();
773 if (!$room || !$rooms_pool) {
774 return false;
775 }
776
777 // get current Joomla/WordPress User ID
778 $now_user = JFactory::getUser();
779 $store_ujid = property_exists($now_user, 'id') ? (int)$now_user->id : 0;
780
781 // forced booking reason, status validation and additional data
782 $forced_reason = $this->get('forced_reason', '');
783 $valid_statuses = ['confirmed', 'standby'];
784 $status = in_array($status, $valid_statuses) ? $status : 'confirmed';
785 $paymentmeth = $this->get('id_payment', '');
786 $set_total = (float)$this->get('_total', 0);
787 $set_taxes = (float)$this->get('_total_tax', 0);
788
789 // stay dates
790 $now_ts = time();
791 $checkin_ts = $this->get('checkin');
792 $checkout_ts = $this->get('checkout');
793 $realback_ts = $this->get('checkout_real', $checkout_ts);
794
795 // room
796 $inj_room = $this->getRoom();
797 $cust_cost = !empty($inj_room['cust_cost']) ? (float)$inj_room['cust_cost'] : 0;
798 $room_cost = !empty($inj_room['room_cost']) ? (float)$inj_room['room_cost'] : 0;
799 $id_price = !empty($inj_room['id_price']) ? (int)$inj_room['id_price'] : 0;
800 $id_tax = !empty($inj_room['id_tax']) ? (int)$inj_room['id_tax'] : 0;
801 $id_tariff = $this->get('id_tariff', 0);
802
803 // custom rate modifier per night
804 $totalpnight = !empty($inj_room['total_or_pnight']) ? $inj_room['total_or_pnight'] : 'total';
805 if ($cust_cost > 0.00 && !$set_closed && $totalpnight == 'pnight') {
806 $cust_cost = $cust_cost * $daysdiff;
807 }
808
809 // customer
810 $cpin = VikBooking::getCPinInstance();
811 $inj_customer = $this->getCustomer();
812 $customer_id = !empty($inj_customer['id']) ? $inj_customer['id'] : 0;
813 $customer_pin = !empty($inj_customer['pin']) ? $inj_customer['pin'] : '';
814 $t_first_name = !empty($inj_customer['first_name']) ? $inj_customer['first_name'] : '';
815 $t_last_name = !empty($inj_customer['last_name']) ? $inj_customer['last_name'] : '';
816 $customer_data = !empty($inj_customer['data']) ? $inj_customer['data'] : '';
817 $customer_email = !empty($inj_customer['email']) ? $inj_customer['email'] : '';
818 $country_code = !empty($inj_customer['country']) ? $inj_customer['country'] : '';
819 $phone_number = !empty($inj_customer['phone']) ? $inj_customer['phone'] : '';
820
821 if ($set_closed) {
822 $customer_data = JText::translate('VBDBTEXTROOMCLOSED');
823 }
824
825 // generate booking SID
826 $sid = VikBooking::getSecretLink();
827
828 // assign room specific unit
829 $set_room_indexes = !$set_closed ? VikBooking::autoRoomUnit() : false;
830 $num_rooms = $num_rooms > 0 ? $num_rooms : 1;
831
832 // occupancy and loop limits
833 $forend = 1;
834 $or_forend = 1;
835 $adults_map = [];
836 $children_map = [];
837 if ($set_closed && empty($split_stay_data)) {
838 $forend = $room['units'];
839 } elseif ($num_rooms > 1 && $num_rooms <= $room['units'] && empty($split_stay_data)) {
840 $forend = $num_rooms;
841 $or_forend = $num_rooms;
842 // assign adults/children proportionally
843 if (($adults + $children) < $num_rooms) {
844 // the number of guests does not make much sense but we build the maps anyway
845 for ($r = 1; $r <= $or_forend; $r++) {
846 $adults_map[$r] = $adults;
847 $children_map[$r] = $children;
848 }
849 } else {
850 $adults_per_room = floor(($adults / $num_rooms));
851 $adults_left = ($adults % $num_rooms);
852 $children_per_room = floor(($children / $num_rooms));
853 $children_left = ($children % $num_rooms);
854 for ($r = 1; $r <= $or_forend; $r++) {
855 $adults_map[$r] = $adults_per_room;
856 $children_map[$r] = $children_per_room;
857 if ($r == $or_forend) {
858 $adults_map[$r] += $adults_left;
859 $children_map[$r] += $children_left;
860 }
861 }
862 }
863 }
864
865 // count total rooms booked
866 $totalrooms = ($set_closed && $status == 'confirmed' ? count($rooms_pool) : ($num_rooms > 1 && $num_rooms <= $room['units'] ? $num_rooms : 1));
867 $totalrooms = !empty($split_stay_data) ? count($split_stay_data) : $totalrooms;
868
869 // prepare booking record
870 $booking = new stdClass;
871 $booking->custdata = $customer_data;
872 $booking->ts = $now_ts;
873 $booking->status = $status;
874 $booking->days = $daysdiff;
875 $booking->checkin = $checkin_ts;
876 $booking->checkout = $checkout_ts;
877 $booking->custmail = $customer_email;
878 $booking->sid = $sid;
879 $booking->idpayment = $paymentmeth;
880 $booking->ujid = (int)$store_ujid;
881 $booking->roomsnum = $totalrooms;
882 $booking->total = $set_total > 0 ? $set_total : null;
883 $booking->lang = VikBooking::guessBookingLangFromCountry($country_code);
884 $booking->country = $country_code;
885 $booking->tot_taxes = $set_taxes > 0 ? $set_taxes : null;
886 $booking->phone = $phone_number;
887 $booking->closure = ($status == 'standby' ? 0 : ($set_closed ? 1 : 0));
888 $booking->split_stay = !empty($split_stay_data) ? 1 : 0;
889
890 // store reservation
891 if ($status == 'confirmed') {
892 // occupy the rooms
893 $insertedbusy = [];
894 if (empty($split_stay_data)) {
895 // only when closing other rooms we have an array containing multiple rooms info
896 foreach ($rooms_pool as $nowroom) {
897 $nowforend = $set_closed ? $nowroom['units'] : $forend;
898 for ($b = 1; $b <= $nowforend; $b++) {
899 $busy_record = new stdClass;
900 $busy_record->idroom = (int)$nowroom['id'];
901 $busy_record->checkin = (int)$checkin_ts;
902 $busy_record->checkout = (int)$checkout_ts;
903 $busy_record->realback = (int)$realback_ts;
904
905 // store busy record
906 $dbo->insertObject('#__vikbooking_busy', $busy_record, 'id');
907
908 if (isset($busy_record->id)) {
909 $insertedbusy[] = $busy_record->id;
910 }
911 }
912 }
913 } else {
914 // for split stay bookings we occupy the rooms on the individual stay dates
915 foreach ($split_stay_data as $split_stay) {
916 $busy_record = new stdClass;
917 $busy_record->idroom = (int)$split_stay['idroom'];
918 $busy_record->checkin = (int)$split_stay['checkin_ts'];
919 $busy_record->checkout = (int)$split_stay['checkout_ts'];
920 $busy_record->realback = (int)$split_stay['realback_ts'];
921
922 // store busy record
923 $dbo->insertObject('#__vikbooking_busy', $busy_record, 'id');
924
925 if (isset($busy_record->id)) {
926 $insertedbusy[] = $busy_record->id;
927 }
928 }
929 }
930
931 if (!$insertedbusy) {
932 $this->setError('No records were occupied');
933 return false;
934 }
935
936 // store booking record
937 $dbo->insertObject('#__vikbooking_orders', $booking, 'id');
938
939 if (!isset($booking->id)) {
940 $this->setError('Could not store the reservation record');
941 return false;
942 }
943
944 // get the newly generated booking ID
945 $newoid = $booking->id;
946
947 // set the new booking ID
948 $this->setNewBookingID($newoid);
949
950 if (!empty($split_stay_data)) {
951 // save transient on db for split stay information
952 VBOFactory::getConfig()->set('split_stay_' . $newoid, json_encode($split_stay_data));
953 }
954
955 // check if some of the rooms booked have shared calendars
956 VikBooking::updateSharedCalendars($newoid, [$room['id']], $checkin_ts, $checkout_ts);
957
958 // confirmation number
959 $confirmnumber = VikBooking::generateConfirmNumber($newoid, true);
960
961 // store busy records/booking relations
962 foreach ($insertedbusy as $lid) {
963 $obusy_record = new stdClass;
964 $obusy_record->idorder = (int)$newoid;
965 $obusy_record->idbusy = (int)$lid;
966
967 // store busy relation record
968 $dbo->insertObject('#__vikbooking_ordersbusy', $obusy_record, 'id');
969 }
970
971 // write room records
972 foreach ($rooms_pool as $rind => $nowroom) {
973 $room_indexes_usemap = [];
974 for ($r = 1; $r <= $or_forend; $r++) {
975 // Assign room specific unit
976 $info_room_avail = [
977 'id' => $newoid,
978 'checkin' => (!empty($nowroom['checkin_ts']) ? $nowroom['checkin_ts'] : $checkin_ts),
979 'checkout' => (!empty($nowroom['checkout_ts']) ? $nowroom['checkout_ts'] : $checkout_ts),
980 ];
981 $room_indexes = $set_room_indexes === true ? VikBooking::getRoomUnitNumsAvailable($info_room_avail, $nowroom['id']) : [];
982 $use_ind_key = 0;
983 if ($room_indexes) {
984 if (!array_key_exists($nowroom['id'], $room_indexes_usemap)) {
985 $room_indexes_usemap[$nowroom['id']] = $use_ind_key;
986 } else {
987 $use_ind_key = $room_indexes_usemap[$nowroom['id']];
988 }
989 }
990
991 // room custom cost
992 $or_cust_cost = $cust_cost > 0.00 ? $cust_cost : 0;
993 $or_cust_cost = $or_forend > 1 && $or_cust_cost > 0 ? round(($or_cust_cost / $or_forend), 2) : $or_cust_cost;
994 // room cost from website rate plan is always based on one room
995 $or_room_cost = $room_cost > 0.00 ? $room_cost : 0;
996 if (!empty($split_stay_data) && $cust_cost > 0) {
997 // set the average cost per room in case of split stay
998 $cost_per_room = ($cust_cost / count($split_stay_data));
999 $or_cust_cost = round($cost_per_room, 2);
1000 if (isset($split_stay_data[$rind]) && isset($split_stay_data[$rind]['nights'])) {
1001 // count the average cost per room depending on the number of nights of stay
1002 $cost_per_room = $cust_cost / $daysdiff * $split_stay_data[$rind]['nights'];
1003 $or_cust_cost = round($cost_per_room, 2);
1004 }
1005 }
1006
1007 // room guests
1008 $room_adults = isset($adults_map[$r]) && empty($split_stay_data) ? $adults_map[$r] : $adults;
1009 $room_children = isset($children_map[$r]) && empty($split_stay_data) ? $children_map[$r] : $children;
1010
1011 // store room record
1012 $room_record = new stdClass;
1013 $room_record->idorder = (int)$newoid;
1014 $room_record->idroom = (int)$nowroom['id'];
1015 $room_record->adults = $room_adults;
1016 $room_record->children = $room_children;
1017 $room_record->idtar = !empty($id_tariff) ? $id_tariff : null;
1018 $room_record->t_first_name = $t_first_name;
1019 $room_record->t_last_name = $t_last_name;
1020 $room_record->roomindex = count($room_indexes) ? (int)$room_indexes[$use_ind_key] : null;
1021 $room_record->cust_cost = $cust_cost > 0.00 ? $or_cust_cost : null;
1022 $room_record->cust_idiva = $cust_cost > 0.00 && !empty($id_tax) ? $id_tax : null;
1023 $room_record->room_cost = $room_cost > 0.00 ? $or_room_cost : null;
1024
1025 $dbo->insertObject('#__vikbooking_ordersrooms', $room_record, 'id');
1026
1027 if (!isset($room_record->id)) {
1028 $this->setError('Could not store room reservation record for booking ID ' . $room_record->idorder);
1029 continue;
1030 }
1031
1032 // Assign room specific unit
1033 if ($room_indexes) {
1034 $room_indexes_usemap[$nowroom['id']]++;
1035 }
1036 }
1037 }
1038 } elseif ($status == 'standby') {
1039 // store booking record
1040 $dbo->insertObject('#__vikbooking_orders', $booking, 'id');
1041
1042 if (!isset($booking->id)) {
1043 $this->setError('Could not store the reservation record');
1044 return false;
1045 }
1046
1047 // get the newly generated booking ID
1048 $newoid = $booking->id;
1049
1050 // set the new booking ID
1051 $this->setNewBookingID($newoid);
1052
1053 if (!empty($split_stay_data)) {
1054 // save transient on db for split stay information
1055 VBOFactory::getConfig()->set('split_stay_' . $newoid, json_encode($split_stay_data));
1056 }
1057
1058 // write room records
1059 foreach ($rooms_pool as $rind => $nowroom) {
1060 for ($r = 1; $r <= $or_forend; $r++) {
1061 // room custom cost
1062 $or_cust_cost = $cust_cost > 0.00 ? $cust_cost : 0;
1063 $or_cust_cost = $or_forend > 1 && $or_cust_cost > 0 ? round(($or_cust_cost / $or_forend), 2) : $or_cust_cost;
1064 // room cost from website rate plan is always based on one room
1065 $or_room_cost = $room_cost > 0.00 ? $room_cost : 0;
1066 if (!empty($split_stay_data) && $cust_cost > 0) {
1067 // set the average cost per room in case of split stay
1068 $cost_per_room = ($cust_cost / count($split_stay_data));
1069 $or_cust_cost = round($cost_per_room, 2);
1070 if (isset($split_stay_data[$rind]) && isset($split_stay_data[$rind]['nights'])) {
1071 // count the average cost per room depending on the number of nights of stay
1072 $cost_per_room = $cust_cost / $daysdiff * $split_stay_data[$rind]['nights'];
1073 $or_cust_cost = round($cost_per_room, 2);
1074 }
1075 }
1076
1077 // room guests
1078 $room_adults = isset($adults_map[$r]) && empty($split_stay_data) ? $adults_map[$r] : $adults;
1079 $room_children = isset($children_map[$r]) && empty($split_stay_data) ? $children_map[$r] : $children;
1080
1081 // store room record
1082 $room_record = new stdClass;
1083 $room_record->idorder = (int)$newoid;
1084 $room_record->idroom = (int)$nowroom['id'];
1085 $room_record->adults = $room_adults;
1086 $room_record->children = $room_children;
1087 $room_record->idtar = !empty($id_tariff) ? $id_tariff : null;
1088 $room_record->t_first_name = $t_first_name;
1089 $room_record->t_last_name = $t_last_name;
1090 $room_record->cust_cost = $cust_cost > 0.00 ? $or_cust_cost : null;
1091 $room_record->cust_idiva = $cust_cost > 0.00 && !empty($id_tax) ? $id_tax : null;
1092 $room_record->room_cost = $room_cost > 0.00 ? $or_room_cost : null;
1093
1094 $dbo->insertObject('#__vikbooking_ordersrooms', $room_record, 'id');
1095
1096 if (!isset($room_record->id)) {
1097 $this->setError('Could not store room reservation record for booking ID ' . $room_record->idorder);
1098 continue;
1099 }
1100
1101 if (empty($split_stay_data)) {
1102 // lock room for pending status
1103 $tmplock_record = new stdClass;
1104 $tmplock_record->idroom = (int)$room['id'];
1105 $tmplock_record->checkin = $checkin_ts;
1106 $tmplock_record->checkout = $checkout_ts;
1107 $tmplock_record->until = VikBooking::getMinutesLock(true);
1108 $tmplock_record->realback = $realback_ts;
1109 $tmplock_record->idorder = (int)$newoid;
1110
1111 $dbo->insertObject('#__vikbooking_tmplock', $tmplock_record, 'id');
1112 }
1113 }
1114 }
1115
1116 if (!empty($split_stay_data)) {
1117 // lock rooms for pending status on proper stay dates
1118 foreach ($split_stay_data as $split_stay) {
1119 $tmplock_record = new stdClass;
1120 $tmplock_record->idroom = (int)$split_stay['idroom'];
1121 $tmplock_record->checkin = (int)$split_stay['checkin_ts'];
1122 $tmplock_record->checkout = (int)$split_stay['checkout_ts'];
1123 $tmplock_record->until = VikBooking::getMinutesLock(true);
1124 $tmplock_record->realback = (int)$split_stay['realback_ts'];
1125 $tmplock_record->idorder = (int)$newoid;
1126
1127 $dbo->insertObject('#__vikbooking_tmplock', $tmplock_record, 'id');
1128 }
1129 }
1130 }
1131
1132 $newoid = $this->getNewBookingID();
1133 if (!$newoid) {
1134 return false;
1135 }
1136
1137 // assign booking to customer
1138 if (!$cpin->getNewCustomerId() && !empty($customer_id)) {
1139 $cpin->setNewPin($customer_pin);
1140 $cpin->setNewCustomerId($customer_id);
1141 }
1142 $cpin->saveCustomerBooking($newoid);
1143
1144 // Booking History
1145 $forced_reason = !empty($forced_reason) ? " {$forced_reason}" : $forced_reason;
1146 VikBooking::getBookingHistoryInstance()->setBid($newoid)->store('NB', "({$now_user->name}){$forced_reason}");
1147
1148 if ($status == 'confirmed' || ($status == 'standby' && class_exists('VCMRequestAvailability'))) {
1149 // Invoke Channel Manager
1150 $vcm_autosync = VikBooking::vcmAutoUpdate();
1151 if ($vcm_autosync > 0) {
1152 $vcm_obj = VikBooking::getVcmInvoker();
1153 $vcm_obj->setOids([$newoid])->setSyncType('new');
1154 $sync_result = $vcm_obj->doSync();
1155 if ($sync_result === false) {
1156 // set error message
1157 $vcm_err = $vcm_obj->getError();
1158 $this->setError(JText::translate('VBCHANNELMANAGERRESULTKO') . (!empty($vcm_err) ? ' - ' . $vcm_err : ''));
1159
1160 // return true because the booking was actually stored
1161 return true;
1162 }
1163 } elseif (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'synch.vikbooking.php')) {
1164 // set the necessary action to invoke VCM
1165 $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']);
1166
1167 $this->setChannelManagerAction(JText::translate('VBCHANNELMANAGERINVOKEASK') . ' <button type="button" class="btn btn-primary" onclick="document.location.href=\'' . $vcm_sync_url . '\';">' . JText::translate('VBCHANNELMANAGERSENDRQ') . '</button>');
1168 }
1169 }
1170
1171 return true;
1172 }
1173 }
1174