PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.16
VikBooking Hotel Booking Engine & PMS v1.8.16
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
← All changes | admin/helpers/src/model/reservation.php +499 -96 1.8.9 → 1.8.16 View file →
@@ -37,8 +37,13 @@
37 37 */
38 38 protected $allRooms = [];
39 39
40 40 /**
41 + * @var ?VBOBookingRegistry
42 + */
43 + protected ?VBOBookingRegistry $prevBookingRegistry = null;
44 +
45 + /**
41 46 * Proxy for immediately getting the object and bind data.
42 47 *
43 48 * @param array|object $data optional data to bind.
44 49 * @param boolean $anew true for forcing a new instance.
@@ -444,20 +449,31 @@
444 449 return $special_requests;
445 450 }
446 451
447 452 /**
448 - * Creates a new reservation record after having constructed the
449 - * object by properly injecting all the necessary booking information.
453 + * Runs before a reservation is created or updated.
450 454 *
455 + * @param ?int $bookingId Optional reservation ID being updated.
456 + *
451 457 * @return bool
458 + *
459 + * @since 1.18.11 (J) - 1.8.11 (WP)
452 460 */
453 - public function create()
461 + protected function preflight(?int $bookingId = null)
454 462 {
455 - if (!$this->canCreate()) {
456 - $this->setError('Forbidden');
463 + // if updating a reservation, fetch the previous details
464 + try {
465 + $prevBookingRegistry = $bookingId ? VBOBookingRegistry::getInstance(['id' => $bookingId]) : null;
466 + } catch (Exception $e) {
467 + $this->setError($e->getMessage());
457 468 return false;
458 469 }
459 470
471 + if ($prevBookingRegistry) {
472 + // set booking registry snapshot prior to update
473 + $this->prevBookingRegistry = $prevBookingRegistry;
474 + }
475 +
460 476 // availability helper
461 477 $av_helper = VikBooking::getAvailabilityInstance(true);
462 478
463 479 // validate mandatory fields
@@ -534,8 +550,33 @@
534 550
535 551 // fetch and apply turnover time before doing anything else
536 552 $this->applyTurnover();
537 553
554 + return true;
555 + }
556 +
557 + /**
558 + * Creates a new reservation record after having constructed the
559 + * object by properly injecting all the necessary booking information.
560 + *
561 + * @return bool
562 + */
563 + public function create()
564 + {
565 + if (!$this->canCreate()) {
566 + $this->setError('Forbidden');
567 + return false;
568 + }
569 +
570 + /**
571 + * Run preflight to ensure data integrity.
572 + *
573 + * @since 1.18.11 (J) - 1.8.11 (WP)
574 + */
575 + if (!$this->preflight()) {
576 + return false;
577 + }
578 +
538 579 // get pool of rooms involved
539 580 $rooms_pool = $this->getRoomsPool();
540 581 if (!$rooms_pool) {
541 582 if ($this->getError() === false) {
@@ -576,8 +617,80 @@
576 617 return true;
577 618 }
578 619
579 620 /**
621 + * Updates a reservation record after having constructed the
622 + * object by properly injecting all the necessary booking information.
623 + * Unlike the modify() method, this method follows the create() pattern.
624 + *
625 + * @return bool
626 + *
627 + * @since 1.18.11 (J) - 1.8.11 (WP)
628 + */
629 + public function update()
630 + {
631 + // access booking ID
632 + $bookingId = (int) $this->get('booking_id', 0);
633 +
634 + if (!$bookingId) {
635 + $this->setError('Missing booking ID to update.');
636 + return false;
637 + }
638 +
639 + /**
640 + * Run preflight to ensure data integrity.
641 + */
642 + if (!$this->preflight($bookingId)) {
643 + return false;
644 + }
645 +
646 + // get pool of rooms involved
647 + $rooms_pool = $this->getRoomsPool();
648 + if (!$rooms_pool) {
649 + if ($this->getError() === false) {
650 + // set generic error if not set already
651 + $this->setError('No rooms involved in the reservation');
652 + }
653 + return false;
654 + }
655 +
656 + try {
657 + // check if the rooms are available for modification
658 + $rooms_available = $this->bookingModifiable($bookingId, $this->get('checkin'), $this->get('checkout'));
659 + if (!$rooms_available && !$this->get('force_booking', 0) && !$this->get('set_closed', 0)) {
660 + // no forcing, no closure and room(s) fully booked means we have an error
661 + if (!$this->getError()) {
662 + $this->setError(JText::translate('VBBOOKNOTMADE'));
663 + }
664 + return false;
665 + }
666 + } catch (Exception $e) {
667 + $this->setError($e->getMessage());
668 + return false;
669 + }
670 +
671 + // detect if we are forcing the reservation
672 + $this->detectForcedReason($rooms_available);
673 +
674 + // store or update the customer information
675 + $this->storeCustomer();
676 +
677 + // calculate total amount and total tax
678 + $this->calculateTotal();
679 +
680 + // update booking and room-booking records
681 + if (!$this->storeReservationRecords($rooms_pool, $bookingId)) {
682 + if ($this->getError() === false) {
683 + // set generic error if not set already
684 + $this->setError('Could not update the reservation');
685 + }
686 + return false;
687 + }
688 +
689 + return true;
690 + }
691 +
692 + /**
580 693 * Searches for bookings according to specified filters.
581 694 *
582 695 * @return array
583 696 *
@@ -642,12 +755,21 @@
642 755 $q->where($dbo->qn('o.custmail') . ' = ' . $dbo->q($filters['email']));
643 756 }
644 757
645 758 if (($filters['phone'] ?? null)) {
646 - $q->where(sprintf('REPLACE(%s, \' \', \'\') LIKE REPLACE(%s, \' \', \'\')',
647 - $dbo->qn('o.phone'),
648 - $dbo->q('%' . $filters['phone'])
649 - ));
759 + $q->where(
760 + sprintf(
761 + '(%s OR %s)',
762 + sprintf('REPLACE(%s, \' \', \'\') LIKE REPLACE(%s, \' \', \'\')',
763 + $dbo->qn('o.phone'),
764 + $dbo->q('%' . $filters['phone'])
765 + ),
766 + sprintf('REPLACE(%s, \' \', \'\') LIKE REPLACE(%s, \' \', \'\')',
767 + $dbo->qn('o.phone_alias'),
768 + $dbo->q('%' . $filters['phone_alias'])
769 + )
770 + )
771 + );
650 772 }
651 773
652 774 if (($filters['date_range']['type'] ?? null) && (($filters['date_range']['start'] ?? null) || ($filters['date_range']['end'] ?? null))) {
653 775 // search by date range
@@ -951,12 +1073,95 @@
951 1073 return (bool) $dbo->getAffectedRows();
952 1074 }
953 1075
954 1076 /**
1077 + * Updates the registration status for the provided booking.
1078 + *
1079 + * @param int $status Registration status to set.
1080 + * @param ?int $bookingId Optional booking ID to update.
1081 + *
1082 + * @return bool
1083 + *
1084 + * @throws Exception
1085 + *
1086 + * @since 1.18.15 (J) - 1.8.15 (WP)
1087 + */
1088 + public function updateRegistration(int $status, ?int $bookingId = null)
1089 + {
1090 + $dbo = JFactory::getDbo();
1091 +
1092 + if (!$bookingId) {
1093 + // get the internal booking record
1094 + $booking = $this->getBooking();
1095 + $bookingId = $booking['id'] ?? null;
1096 + }
1097 +
1098 + if (empty($bookingId)) {
1099 + throw new Exception('Missing booking ID.', 400);
1100 + }
1101 +
1102 + $knownStatuses = [-1, 0, 1, 2];
1103 +
1104 + if (!in_array($status, $knownStatuses, true)) {
1105 + throw new InvalidArgumentException('Invalid registration status.', 400);
1106 + }
1107 +
1108 + // update booking record
1109 + $dbo->setQuery(
1110 + $dbo->getQuery(true)
1111 + ->update($dbo->qn('#__vikbooking_orders'))
1112 + ->set($dbo->qn('checked') . ' = ' . $status)
1113 + ->where($dbo->qn('id') . ' = ' . $bookingId)
1114 + );
1115 + $dbo->execute();
1116 +
1117 + return (bool) $dbo->getAffectedRows();
1118 + }
1119 +
1120 + /**
1121 + * Updates the booking-level phone number or alias phone number (masked phone).
1122 + *
1123 + * @param ?string $phone The phone number value to set, even null to unset.
1124 + * @param bool $isAlias Whether to update the real or alias phone number.
1125 + * @param ?int $bookingId Optional booking ID to update.
1126 + *
1127 + * @return bool
1128 + *
1129 + * @throws Exception
1130 + *
1131 + * @since 1.18.16 (J) - 1.8.16 (WP)
1132 + */
1133 + public function updatePhoneNumber(?string $phone, bool $isAlias = false, ?int $bookingId = null)
1134 + {
1135 + $dbo = JFactory::getDbo();
1136 +
1137 + if (!$bookingId) {
1138 + // get the internal booking record
1139 + $booking = $this->getBooking();
1140 + $bookingId = $booking['id'] ?? null;
1141 + }
1142 +
1143 + if (empty($bookingId)) {
1144 + throw new Exception('Missing booking ID.', 400);
1145 + }
1146 +
1147 + $dbo->setQuery(
1148 + $dbo->getQuery(true)
1149 + ->update('#__vikbooking_orders')
1150 + ->set($dbo->qn($isAlias ? 'phone_alias' : 'phone') . ' = ' . ($phone ? $dbo->q($phone) : 'NULL'))
1151 + ->where($dbo->qn('id') . ' = ' . (int) $bookingId)
1152 + );
1153 + $dbo->execute();
1154 +
1155 + return (bool) $dbo->getAffectedRows();
1156 + }
1157 +
1158 + /**
955 1159 * Modifies the requested booking ID according to the provided options.
956 1160 * This method does not support all rate plan options like for the creation of a
957 1161 * new booking. This is a method for making quick updates concerning a room switch,
958 1162 * a change of stay dates, new booking total amount, guests, add extra services etc..
1163 + * For modifying a reservation with the same data as for the creation, see update().
959 1164 *
960 1165 * @param array $options List of details to perform the modification.
961 1166 *
962 1167 * @return bool
@@ -1400,17 +1605,19 @@
1400 1605 return true;
1401 1606 }
1402 1607
1403 1608 /**
1404 - * Deletes the requested booking ID.
1609 + * Deletes the requested booking ID and related records.
1405 1610 *
1406 - * @param array $options List of details to perform the cancellation.
1611 + * @param array $options List of details to perform the cancellation.
1612 + * @param bool $reCreating True if the booking is being re-created (updated).
1407 1613 *
1408 1614 * @return bool
1409 1615 *
1410 1616 * @since 1.16.10 (J) - 1.6.10 (WP)
1617 + * @since 1.18.11 (J) - 1.8.11 (WP) added argument $reCreating.
1411 1618 */
1412 - public function delete(array $options)
1619 + public function delete(array $options, bool $reCreating = false)
1413 1620 {
1414 1621 $dbo = JFactory::getDbo();
1415 1622
1416 1623 $booking_id = $options['booking_id'] ?? 0;
@@ -1416,9 +1623,13 @@
1416 1623 $booking_id = $options['booking_id'] ?? 0;
1417 1624 $canc_reason = $options['cancellation_reason'] ?? '';
1418 1625 $purge_remove = $options['purge_remove'] ?? false;
1419 1626
1420 - $booking = VikBooking::getBookingInfoFromID($booking_id);
1627 + if ($reCreating && $this->prevBookingRegistry && $this->prevBookingRegistry->getID() == $booking_id) {
1628 + $booking = $this->prevBookingRegistry->getData();
1629 + } else {
1630 + $booking = VikBooking::getBookingInfoFromID($booking_id);
1631 + }
1421 1632
1422 1633 if (!$booking) {
1423 1634 $this->setError('Booking not found.');
1424 1635 return false;
@@ -1423,14 +1634,14 @@
1423 1634 $this->setError('Booking not found.');
1424 1635 return false;
1425 1636 }
1426 1637
1427 - if ($booking['status'] === 'cancelled' && !$purge_remove) {
1638 + if (!$reCreating && $booking['status'] === 'cancelled' && !$purge_remove) {
1428 1639 $this->setError(sprintf('Booking ID %d is already cancelled.', $booking['id']));
1429 1640 return false;
1430 1641 }
1431 1642
1432 - if (class_exists('VCMFeesCancellation')) {
1643 + if (!$reCreating && class_exists('VCMFeesCancellation')) {
1433 1644 // let VCM detect if there are any constraints for the cancellation
1434 1645 $canc_denied = VCMFeesCancellation::getInstance($booking, $anew = true)->isBookingConstrained();
1435 1646 if ($canc_denied) {
1436 1647 // set error message
@@ -1446,19 +1657,21 @@
1446 1657 // whether OTAs should be notified
1447 1658 $notify_otas = false;
1448 1659
1449 1660 if ($booking['status'] != 'cancelled') {
1450 - // update status to cancelled
1451 - $q = $dbo->getQuery(true)
1452 - ->update($dbo->qn('#__vikbooking_orders'))
1453 - ->set($dbo->qn('status') . ' = ' . $dbo->q('cancelled'))
1454 - ->where($dbo->qn('id') . ' = ' . (int) $booking['id']);
1455 - if (!empty($canc_reason)) {
1456 - $set_canc_reason = (!empty($booking['adminnotes']) ? $booking['adminnotes'] . "\n" : '') . $canc_reason;
1457 - $q->set($dbo->qn('adminnotes') . ' = ' . $dbo->q($set_canc_reason));
1661 + if (!$reCreating) {
1662 + // update status to cancelled
1663 + $q = $dbo->getQuery(true)
1664 + ->update($dbo->qn('#__vikbooking_orders'))
1665 + ->set($dbo->qn('status') . ' = ' . $dbo->q('cancelled'))
1666 + ->where($dbo->qn('id') . ' = ' . (int) $booking['id']);
1667 + if (!empty($canc_reason)) {
1668 + $set_canc_reason = (!empty($booking['adminnotes']) ? $booking['adminnotes'] . "\n" : '') . $canc_reason;
1669 + $q->set($dbo->qn('adminnotes') . ' = ' . $dbo->q($set_canc_reason));
1670 + }
1671 + $dbo->setQuery($q);
1672 + $dbo->execute();
1458 1673 }
1459 - $dbo->setQuery($q);
1460 - $dbo->execute();
1461 1674
1462 1675 // delete temporarily locked records, if any
1463 1676 $dbo->setQuery(
1464 1677 $dbo->getQuery(true)
@@ -1466,26 +1679,28 @@
1466 1679 ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
1467 1680 );
1468 1681 $dbo->execute();
1469 1682
1470 - if ($booking['status'] == 'confirmed') {
1683 + if (!$reCreating && $booking['status'] == 'confirmed') {
1471 1684 // turn flag on
1472 1685 $notify_otas = true;
1473 1686 }
1474 1687
1475 - // access history object
1476 - $history_obj = VikBooking::getBookingHistoryInstance($booking['id']);
1688 + if (!$reCreating) {
1689 + // access history object
1690 + $history_obj = VikBooking::getBookingHistoryInstance($booking['id']);
1477 1691
1478 - $caller_id = $now_user->name ? "({$now_user->name})" : '';
1479 - if ($this->getCaller()) {
1480 - $caller_id = '(' . $this->getCaller() . ')';
1481 - if ($this->getHistoryData()) {
1482 - $history_obj->setExtraData($this->getHistoryData());
1692 + $caller_id = $now_user->name ? "({$now_user->name})" : '';
1693 + if ($this->getCaller()) {
1694 + $caller_id = '(' . $this->getCaller() . ')';
1695 + if ($this->getHistoryData()) {
1696 + $history_obj->setExtraData($this->getHistoryData());
1697 + }
1483 1698 }
1699 +
1700 + // update Booking History
1701 + $history_obj->store('CB', $caller_id);
1484 1702 }
1485 -
1486 - // update Booking History
1487 - $history_obj->store('CB', $caller_id);
1488 1703 }
1489 1704
1490 1705 /**
1491 1706 * In case of pending bookings being cancelled, schedule the release through VCM.
@@ -1522,9 +1737,9 @@
1522 1737 );
1523 1738 $dbo->execute();
1524 1739
1525 1740 // check for purge removal
1526 - if ($booking['status'] === 'cancelled' && $purge_remove) {
1741 + if (!$reCreating && $booking['status'] === 'cancelled' && $purge_remove) {
1527 1742 // delete booking-customer relation
1528 1743 $dbo->setQuery(
1529 1744 $dbo->getQuery(true)
1530 1745 ->delete($dbo->qn('#__vikbooking_customers_orders'))
@@ -1561,9 +1776,26 @@
1561 1776 VBOFactory::getConfig()->remove('split_stay_' . $booking['id']);
1562 1777 }
1563 1778 }
1564 1779
1565 - if ($notify_otas) {
1780 + if ($reCreating === true) {
1781 + // when updating a booking record, get rid of all rooms previously assigned
1782 +
1783 + // delete booking-room relations
1784 + $dbo->setQuery(
1785 + $dbo->getQuery(true)
1786 + ->delete($dbo->qn('#__vikbooking_ordersrooms'))
1787 + ->where($dbo->qn('idorder') . ' = ' . (int) $booking['id'])
1788 + );
1789 + $dbo->execute();
1790 +
1791 + // in case of split stay booking, remove the transient
1792 + if ($booking['split_stay']) {
1793 + VBOFactory::getConfig()->remove('split_stay_' . $booking['id']);
1794 + }
1795 + }
1796 +
1797 + if (!$reCreating && $notify_otas) {
1566 1798 $vcm_autosync = VikBooking::vcmAutoUpdate();
1567 1799 if ($vcm_autosync > 0) {
1568 1800 $vcm_obj = VikBooking::getVcmInvoker();
1569 1801 $vcm_obj->setOids([$booking['id']])->setSyncType('cancel');
@@ -1664,8 +1896,11 @@
1664 1896 $units_minus_oper[$or['idroom']] = 0;
1665 1897 }
1666 1898 }
1667 1899
1900 + // list of room-units pool
1901 + $roomUnits = [];
1902 +
1668 1903 // check availability for each room involved
1669 1904 foreach ($roomBooking as $ind => $or) {
1670 1905 // determine proper values for this room
1671 1906 $room_stay_checkin = $booking['checkin'];
@@ -1681,8 +1916,11 @@
1681 1916
1682 1917 // get room record
1683 1918 $room_record = VikBooking::getRoomInfo($or['idroom']);
1684 1919
1920 + // set room (total) units
1921 + $roomUnits[$or['idroom']] = (int) ($room_record['units'] ?? 1);
1922 +
1685 1923 // check if the room is available
1686 1924 if (!VikBooking::roomBookable($or['idroom'], (($room_record['units'] ?? 0) - $units_minus_oper[$or['idroom']]), $room_stay_checkin, $room_stay_checkout)) {
1687 1925 $allbook = false;
1688 1926 $notavail[] = $room_record['name'] ?? '?';
@@ -1706,26 +1944,31 @@
1706 1944 $room_stay_checkout = $room_stay_dates[$ind]['checkout_ts'] ?: $room_stay_dates[$ind]['checkout'];
1707 1945 $room_stay_realback = $turnover_secs + $room_stay_checkout;
1708 1946 }
1709 1947
1710 - // build busy record
1711 - $busy_record = new stdClass;
1712 - $busy_record->idroom = (int) $or['idroom'];
1713 - $busy_record->checkin = (int) $room_stay_checkin;
1714 - $busy_record->checkout = (int) $room_stay_checkout;
1715 - $busy_record->realback = (int) $room_stay_realback;
1948 + // determine the room units to occupy (to support closure re-confirmations)
1949 + $bookUnitsCount = (int) (intval($booking['closure'] ?? 0) ? ($roomUnits[$or['idroom']] ?? 1) : 1);
1716 1950
1717 - // store busy record and obtain the newly created ID
1718 - $dbo->insertObject('#__vikbooking_busy', $busy_record, 'id');
1719 - $lid = $busy_record->id ?? 0;
1951 + for ($bi = 1; $bi <= $bookUnitsCount; $bi++) {
1952 + // build busy record
1953 + $busy_record = new stdClass;
1954 + $busy_record->idroom = (int) $or['idroom'];
1955 + $busy_record->checkin = (int) $room_stay_checkin;
1956 + $busy_record->checkout = (int) $room_stay_checkout;
1957 + $busy_record->realback = (int) $room_stay_realback;
1720 1958
1721 - // build busy relation record
1722 - $obusy_record = new stdClass;
1723 - $obusy_record->idorder = (int) $booking['id'];
1724 - $obusy_record->idbusy = (int) $lid;
1959 + // store busy record and obtain the newly created ID
1960 + $dbo->insertObject('#__vikbooking_busy', $busy_record, 'id');
1961 + $lid = $busy_record->id ?? 0;
1725 1962
1726 - // store busy relation record
1727 - $dbo->insertObject('#__vikbooking_ordersbusy', $obusy_record, 'id');
1963 + // build busy relation record
1964 + $obusy_record = new stdClass;
1965 + $obusy_record->idorder = (int) $booking['id'];
1966 + $obusy_record->idbusy = (int) $lid;
1967 +
1968 + // store busy relation record
1969 + $dbo->insertObject('#__vikbooking_ordersbusy', $obusy_record, 'id');
1970 + }
1728 1971 }
1729 1972
1730 1973 // delete temporarily locked records, if any
1731 1974 $dbo->setQuery(
@@ -1971,10 +2214,15 @@
1971 2214 * @since 1.18.2 (J) - 1.8.2 (WP)
1972 2215 */
1973 2216 public function bookingModifiable(int $booking_id, $new_checkin, $new_checkout)
1974 2217 {
1975 - // load all booking rooms
1976 - $booking_rooms = VikBooking::loadOrdersRoomsData($booking_id);
2218 + // attempt to access previous booking registry to reduce queries
2219 + if ($this->prevBookingRegistry && $this->prevBookingRegistry->getID() == $booking_id) {
2220 + $booking_rooms = $this->prevBookingRegistry->getRooms();
2221 + } else {
2222 + // load all booking rooms
2223 + $booking_rooms = VikBooking::loadOrdersRoomsData($booking_id);
2224 + }
1977 2225
1978 2226 if (!$booking_rooms) {
1979 2227 throw new Exception('Could not find any rooms booked within the reservation.', 500);
1980 2228 }
@@ -2004,8 +2252,36 @@
2004 2252 foreach ($booking_rooms as $booking_room) {
2005 2253 $rooms_units_counter[$booking_room['idroom']] = ($rooms_counter[$booking_room['idroom']] ?? -1) + 1;
2006 2254 }
2007 2255
2256 + // check if the booking modification involves other rooms
2257 + $roomsPool = $this->getRoomsPool();
2258 + $oldRoomIds = array_map('intval', array_column($booking_rooms, 'idroom'));
2259 + $newRoomIds = array_values(array_unique(array_map('intval', array_column($roomsPool, 'id'))));
2260 + $diffIds = array_diff($newRoomIds, $oldRoomIds);
2261 + foreach ($diffIds as $newId) {
2262 + $unitsRequested = 0;
2263 + $totUnits = 0;
2264 + foreach ($roomsPool as $roomData) {
2265 + if ($newId == $roomData['id']) {
2266 + $unitsRequested++;
2267 + $totUnits = (int) ($roomData['units'] ?? 1);
2268 + }
2269 + }
2270 + if (!$unitsRequested) {
2271 + continue;
2272 + }
2273 +
2274 + // update units counter map for multiple units requested
2275 + $rooms_units_counter[$newId] = $unitsRequested - 1;
2276 +
2277 + // push additional room for availability validation
2278 + $booking_rooms[] = [
2279 + 'idroom' => $newId,
2280 + 'tot_units' => $totUnits,
2281 + ];
2282 + }
2283 +
2008 2284 // check the availability for each room booked
2009 2285 $rooms_parsed = [];
2010 2286 foreach ($booking_rooms as $booking_room) {
2011 2287 if (in_array($booking_room['idroom'], $rooms_parsed)) {
@@ -2733,8 +3009,11 @@
2733 3009 $email = !empty($inj_customer['email']) ? $inj_customer['email'] : '';
2734 3010 $country = !empty($inj_customer['country']) ? $inj_customer['country'] : '';
2735 3011 $phone = !empty($inj_customer['phone']) ? $inj_customer['phone'] : '';
2736 3012 $gender = !empty($inj_customer['gender']) ? $inj_customer['gender'] : '';
3013 + $address = !empty($inj_customer['address']) ? $inj_customer['address'] : '';
3014 + $city = !empty($inj_customer['city']) ? $inj_customer['city'] : '';
3015 + $zip = !empty($inj_customer['zip']) ? $inj_customer['zip'] : '';
2737 3016
2738 3017 // custom fields
2739 3018 $q = "SELECT * FROM `#__vikbooking_custfields` ORDER BY `ordering` ASC;";
2740 3019 $dbo->setQuery($q);
@@ -2770,8 +3049,23 @@
2770 3049 // inject the customer gender value
2771 3050 $customer_extrainfo['gender'] = strtoupper($gender);
2772 3051 }
2773 3052
3053 + if (!empty($address)) {
3054 + // inject extra info
3055 + $customer_extrainfo['address'] = strlen((string) $address) > 200 ? substr($address, 0, 200) : $address;
3056 + }
3057 +
3058 + if (!empty($city)) {
3059 + // inject extra info
3060 + $customer_extrainfo['city'] = strlen((string) $city) > 50 ? substr($city, 0, 50) : $city;
3061 + }
3062 +
3063 + if (!empty($zip)) {
3064 + // inject extra info
3065 + $customer_extrainfo['zip'] = strlen((string) $zip) > 10 ? substr($zip, 0, 10) : $zip;
3066 + }
3067 +
2774 3068 $cpin = VikBooking::getCPinInstance();
2775 3069 $cpin->is_admin = true;
2776 3070 $cpin->setCustomerExtraInfo($customer_extrainfo);
2777 3071 $cpin->saveCustomerDetails($first_name, $last_name, $email, $phone, $country, $customer_cfields);
@@ -2920,9 +3214,9 @@
2920 3214 // total and taxes should be multiplied by the number of rooms booked when using a website rate plan
2921 3215 if ($set_closed) {
2922 3216 $roomTotalValue *= $roomDetails['units'];
2923 3217 $roomTotalTax *= $roomDetails['units'];
2924 - } elseif ($num_rooms > 1 && $num_rooms <= $roomDetails['units']) {
3218 + } elseif (!$this->isMultiRoom() && $num_rooms > 1 && $num_rooms <= $roomDetails['units']) {
2925 3219 $roomTotalValue *= $num_rooms;
2926 3220 $roomTotalTax *= $num_rooms;
2927 3221 }
2928 3222 }
@@ -3035,17 +3329,25 @@
3035 3329 ];
3036 3330 }
3037 3331
3038 3332 /**
3039 - * Stores the booking and room-booking records.
3040 - * If no errors, the newly generated booking id is set.
3333 + * Stores (or updates) the booking and room-booking records.
3334 + * If no errors, the newly generated or updated booking id is set.
3041 3335 *
3042 - * @param array $rooms_pool list of rooms involved.
3336 + * @param array $rooms_pool List of rooms involved.
3337 + * @param ?int $bookingId The booking ID being updated, if any.
3043 3338 *
3044 3339 * @return bool
3340 + *
3341 + * @since 1.18.11 (J) - 1.8.11 (WP) added $bookingId argument to support updates.
3045 3342 */
3046 - protected function storeReservationRecords(array $rooms_pool)
3343 + protected function storeReservationRecords(array $rooms_pool, ?int $bookingId = null)
3047 3344 {
3345 + if ($bookingId && !$this->prevBookingRegistry) {
3346 + $this->setError('Missing previous booking registry for update.');
3347 + return false;
3348 + }
3349 +
3048 3350 $dbo = JFactory::getDbo();
3049 3351
3050 3352 // access properties
3051 3353 $set_closed = $this->get('set_closed', 0);
@@ -3080,17 +3382,22 @@
3080 3382 $set_taxes = (float) $this->get('_total_tax', 0);
3081 3383 $set_city_tax = (float) $this->get('_total_city_tax', 0);
3082 3384 $set_fees = (float) $this->get('_total_fees', 0);
3083 3385
3084 - // booking creation date
3085 - $now_ts = time();
3086 - if ($created_on = $this->get('created_on')) {
3087 - if (is_int($created_on)) {
3088 - // timestamp expected
3089 - $now_ts = $created_on;
3090 - } elseif (preg_match('/^[0-9]{4}\-[0-9]{2}\-[0-9]{2}/', (string) $created_on)) {
3091 - // date in military format expected, with or without the time
3092 - $now_ts = strtotime($created_on);
3386 + if ($bookingId) {
3387 + // updating a booking should not change the creation date
3388 + $now_ts = $this->prevBookingRegistry->getProperty('ts', time());
3389 + } else {
3390 + // booking creation date
3391 + $now_ts = time();
3392 + if ($created_on = $this->get('created_on')) {
3393 + if (is_int($created_on)) {
3394 + // timestamp expected
3395 + $now_ts = $created_on;
3396 + } elseif (preg_match('/^[0-9]{4}\-[0-9]{2}\-[0-9]{2}/', (string) $created_on)) {
3397 + // date in military format expected, with or without the time
3398 + $now_ts = strtotime($created_on);
3399 + }
3093 3400 }
3094 3401 }
3095 3402
3096 3403 // stay dates
@@ -3108,8 +3415,12 @@
3108 3415 $customer_data = ($inj_customer['data'] ?? null) ?: '';
3109 3416 $customer_email = ($inj_customer['email'] ?? null) ?: '';
3110 3417 $country_code = ($inj_customer['country'] ?? null) ?: '';
3111 3418 $phone_number = ($inj_customer['phone'] ?? null) ?: '';
3419 + $phone_alias = ($inj_customer['phone_alias'] ?? null) ?: '';
3420 + $address = ($inj_customer['address'] ?? null) ?: '';
3421 + $city = ($inj_customer['city'] ?? null) ?: '';
3422 + $zip = ($inj_customer['zip'] ?? null) ?: '';
3112 3423
3113 3424 if ($set_closed) {
3114 3425 // get the possibly custom "customer notes"
3115 3426 $customer_notes = $customer_data;
@@ -3131,14 +3442,25 @@
3131 3442 }
3132 3443 if ($customer_email) {
3133 3444 $customer_data .= "eMail: {$customer_email}\n";
3134 3445 }
3446 + if ($phone_number) {
3447 + $customer_data .= "Phone: {$phone_number}\n";
3448 + } elseif ($phone_alias) {
3449 + $customer_data .= "Phone-alias: {$phone_alias}\n";
3450 + }
3135 3451 if ($country_code) {
3136 3452 $customer_data .= "Country: {$country_code}\n";
3137 3453 }
3138 - if ($phone_number) {
3139 - $customer_data .= "Phone: {$phone_number}\n";
3454 + if ($address) {
3455 + $customer_data .= "Address: {$address}\n";
3140 3456 }
3457 + if ($city) {
3458 + $customer_data .= "City: {$city}\n";
3459 + }
3460 + if ($zip) {
3461 + $customer_data .= "Zip: {$zip}\n";
3462 + }
3141 3463 $customer_data = rtrim($customer_data, "\n");
3142 3464 }
3143 3465
3144 3466 // ensure the country code is in the right format
@@ -3151,10 +3473,10 @@
3151 3473 // this looks like an unknown country name
3152 3474 $country_code = '';
3153 3475 }
3154 3476
3155 - // generate booking SID
3156 - $sid = VikBooking::getSecretLink();
3477 + // generate booking SID, unless we are updating
3478 + $sid = !$bookingId ? VikBooking::getSecretLink() : null;
3157 3479
3158 3480 // assign room specific unit
3159 3481 $set_room_indexes = !$set_closed ? VikBooking::autoRoomUnit() : false;
3160 3482 $num_rooms = $num_rooms > 0 ? $num_rooms : 1;
@@ -3205,12 +3527,28 @@
3205 3527 // attempt to get the default payment method
3206 3528 if (!$paymentmeth && ($auto_paymeth || $status == 'standby')) {
3207 3529 // get the default payment method, if any
3208 3530 $paymentmeth = $this->getDefaultPaymentMethod($auto_paymeth);
3531 + } elseif ($paymentmeth && is_int($paymentmeth)) {
3532 + // convert the payment integer ID into a string with the actual record name
3533 + $paymentmeth = $this->getPaymentMethodValue($paymentmeth);
3209 3534 }
3210 3535
3536 + if ($bookingId) {
3537 + // when updating, delete all room booking records involved
3538 + // to allow the re-generation of the updated records
3539 + $this->delete(['booking_id' => $bookingId], $reCreating = true);
3540 + }
3541 +
3211 3542 // prepare booking record
3212 3543 $booking = new stdClass;
3544 +
3545 + if ($bookingId) {
3546 + // inject current booking ID for update
3547 + $booking->id = $bookingId;
3548 + }
3549 +
3550 + // set booking record properties
3213 3551 $booking->custdata = $customer_data;
3214 3552 $booking->ts = $now_ts;
3215 3553 $booking->status = $status;
3216 3554 $booking->days = $daysdiff;
@@ -3231,8 +3569,9 @@
3231 3569 $booking->tot_city_taxes = $set_city_tax ?: ($this->get('tot_city_taxes') ? ((float) $this->get('tot_city_taxes')) : null);
3232 3570 $booking->tot_fees = $set_fees ?: ($this->get('tot_fees') ? ((float) $this->get('tot_fees')) : null);
3233 3571 $booking->tot_damage_dep = $this->get('tot_damage_dep') ? ((float) $this->get('tot_damage_dep')) : null;
3234 3572 $booking->phone = $phone_number;
3573 + $booking->phone_alias = $phone_alias ?: null;
3235 3574 $booking->cmms = $this->get('cmms') ? ((float) $this->get('cmms')) : null;
3236 3575 $booking->closure = ($status == 'standby' ? 0 : ($set_closed || $units_closed ? 1 : 0));
3237 3576 $booking->payable = $this->get('payable') ? ((float) $this->get('payable')) : null;
3238 3577 $booking->refund = $this->get('refund') ? ((float) $this->get('refund')) : null;
@@ -3294,17 +3633,22 @@
3294 3633 return false;
3295 3634 }
3296 3635 }
3297 3636
3298 - // store booking record
3299 - $dbo->insertObject('#__vikbooking_orders', $booking, 'id');
3637 + if ($bookingId) {
3638 + // update booking record
3639 + $dbo->updateObject('#__vikbooking_orders', $booking, 'id');
3640 + } else {
3641 + // store booking record
3642 + $dbo->insertObject('#__vikbooking_orders', $booking, 'id');
3643 + }
3300 3644
3301 3645 if (empty($booking->id)) {
3302 - $this->setError('Could not store the reservation record');
3646 + $this->setError('Could not save reservation record.');
3303 3647 return false;
3304 3648 }
3305 3649
3306 - // get the newly generated booking ID
3650 + // get the newly generated (or just updated) booking ID
3307 3651 $newoid = $booking->id;
3308 3652
3309 3653 // set the new booking ID
3310 3654 $this->setNewBookingID($newoid);
@@ -3317,10 +3661,12 @@
3317 3661 if ($status == 'confirmed') {
3318 3662 // check if some of the rooms booked have shared calendars
3319 3663 VikBooking::updateSharedCalendars($newoid, [$room['id']], $checkin_ts, $checkout_ts);
3320 3664
3321 - // generate and set confirmation number
3322 - $confirmnumber = VikBooking::generateConfirmNumber($newoid, true);
3665 + if (!$bookingId) {
3666 + // generate and set confirmation number for the newly created confirmed reservation
3667 + $confirmnumber = VikBooking::generateConfirmNumber($newoid, true);
3668 + }
3323 3669
3324 3670 // store busy record-booking relations
3325 3671 foreach ($insertedbusy as $lid) {
3326 3672 $obusy_record = new stdClass;
@@ -3332,10 +3678,10 @@
3332 3678 }
3333 3679 }
3334 3680
3335 3681 // store room booking records
3682 + $room_indexes_usemap = [];
3336 3683 foreach ($rooms_pool as $rind => $nowroom) {
3337 - $room_indexes_usemap = [];
3338 3684 for ($r = 1; $r <= $or_forend; $r++) {
3339 3685 // determine room-level stay dates
3340 3686 $room_stay_checkin = $nowroom['checkin_ts'] ?? $roomsData[$rind]['checkin'] ?? $checkin_ts;
3341 3687 $room_stay_checkout = $nowroom['checkout_ts'] ?? $roomsData[$rind]['checkout'] ?? $checkout_ts;
@@ -3390,10 +3736,10 @@
3390 3736 }
3391 3737 }
3392 3738
3393 3739 // room guests
3394 - $room_adults = (int) (isset($adults_map[$r]) && empty($split_stay_data) ? $adults_map[$r] : ($roomsData[$rind]['adults'] ?? $adults));
3395 - $room_children = (int) (isset($children_map[$r]) && empty($split_stay_data) ? $children_map[$r] : ($roomsData[$rind]['children'] ?? $children));
3740 + $room_adults = (int) ($roomsData[$rind]['adults'] ?? (isset($adults_map[$r]) && empty($split_stay_data) ? $adults_map[$r] : $adults));
3741 + $room_children = (int) ($roomsData[$rind]['children'] ?? (isset($children_map[$r]) && empty($split_stay_data) ? $children_map[$r] : $children));
3396 3742 $use_children_age = (array) ($roomsData[$rind]['children_age'] ?? $children_age);
3397 3743
3398 3744 // number of pets per room
3399 3745 $room_pets = (int) ($roomsData[$rind]['pets'] ?? 0);
@@ -3544,10 +3890,11 @@
3544 3890 $cpin->setNewCustomerId($customer_id);
3545 3891 }
3546 3892 $cpin->saveCustomerBooking($newoid);
3547 3893
3548 - // Booking History
3894 + // handle booking history
3549 3895 $history_obj = VikBooking::getBookingHistoryInstance($newoid);
3896 +
3550 3897 $forced_reason = !empty($forced_reason) ? " {$forced_reason}" : $forced_reason;
3551 3898 $caller_id = $now_user->name ? "({$now_user->name})" : '';
3552 3899 if ($this->getCaller()) {
3553 3900 $caller_id = '(' . $this->getCaller() . ')';
@@ -3558,27 +3905,62 @@
3558 3905 if ($this->get('idquote')) {
3559 3906 // mention the quote ID in the history description
3560 3907 $forced_reason .= sprintf(' %s #%d', JText::translate('VBO_BTYPE_QUOTE'), (int) $this->get('idquote'));
3561 3908 }
3562 - $history_obj->store('NB', trim($caller_id . $forced_reason));
3563 3909
3910 + // tell whether alterations were detected, in case of booking update
3911 + $alterationsDetected = false;
3912 +
3913 + if ($bookingId) {
3914 + // booking updated event
3915 + $historyPrevBooking = $this->prevBookingRegistry->getData();
3916 + $historyPrevBooking['rooms_info'] = $this->prevBookingRegistry->getRooms();
3917 + $history_obj
3918 + ->setPrevBooking($historyPrevBooking)
3919 + ->store('MB', trim($caller_id . $forced_reason) . ' ' . VikBooking::getLogBookingModification($historyPrevBooking));
3920 +
3921 + // construct a new booking registry, and inject the booking snapshot prior updating
3922 + $currentRegistry = VBOBookingRegistry::getInstance(['id' => $bookingId], [], $historyPrevBooking);
3923 + // detect alterations (include room-level)
3924 + $alterationsDetected = $currentRegistry->detectAlterations(true);
3925 + } else {
3926 + // new booking event
3927 + $history_obj->store('NB', trim($caller_id . $forced_reason));
3928 + }
3929 +
3564 3930 if ($status == 'confirmed' || ($status == 'standby' && class_exists('VCMRequestAvailability'))) {
3565 3931 // Invoke Channel Manager
3566 3932 $vcm_autosync = VikBooking::vcmAutoUpdate();
3567 3933 if ($vcm_autosync > 0) {
3568 3934 $vcm_obj = VikBooking::getVcmInvoker();
3569 - $vcm_obj->setOids([$newoid])->setSyncType('new');
3570 - $sync_result = $vcm_obj->doSync();
3571 - if ($sync_result === false) {
3572 - // set error message
3573 - $vcm_err = $vcm_obj->getError();
3574 - $this->setError(JText::translate('VBCHANNELMANAGERRESULTKO') . (!empty($vcm_err) ? ' - ' . $vcm_err : ''));
3575 3935
3576 - // return true because the booking was actually stored
3577 - return true;
3936 + // tell if sync is actually needed
3937 + $sync_needed = true;
3938 + if ($bookingId && $alterationsDetected === false) {
3939 + // do not waste connections when nothing sensitive was updated
3940 + $sync_needed = false;
3578 3941 }
3579 - } elseif (is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'synch.vikbooking.php')) {
3580 - // set the necessary action to invoke VCM
3942 +
3943 + if ($sync_needed) {
3944 + if ($bookingId) {
3945 + // sync for an updated booking, after something was truly modified
3946 + $vcm_obj->setOids([$bookingId])->setSyncType('modify')->setOriginalBooking($historyPrevBooking ?? []);
3947 + } else {
3948 + // always sync for a new booking
3949 + $vcm_obj->setOids([$newoid])->setSyncType('new');
3950 + }
3951 +
3952 + // run operation
3953 + $sync_result = $vcm_obj->doSync();
3954 +
3955 + if ($sync_result === false) {
3956 + // set error message, but do NOT return false at this point
3957 + $vcm_err = $vcm_obj->getError();
3958 + $this->setError(JText::translate('VBCHANNELMANAGERRESULTKO') . (!empty($vcm_err) ? ' - ' . $vcm_err : ''));
3959 + }
3960 + }
3961 + } elseif (!$bookingId && is_file(VCM_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'synch.vikbooking.php')) {
3962 + // set the necessary action to invoke VCM (only when creating a new booking)
3581 3963 $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']);
3582 3964
3583 3965 $this->setChannelManagerAction(JText::translate('VBCHANNELMANAGERINVOKEASK') . ' <button type="button" class="btn btn-primary" onclick="document.location.href=\'' . $vcm_sync_url . '\';">' . JText::translate('VBCHANNELMANAGERSENDRQ') . '</button>');
3584 3966 }
@@ -3585,13 +3967,13 @@
3585 3967 }
3586 3968
3587 3969 if (VikBooking::isAdmin()) {
3588 3970 /**
3589 - * Trigger event to allow third party plugins to intercept the admin new booking event.
3971 + * Trigger event to allow third party plugins to intercept the admin new/updated booking event.
3590 3972 *
3591 3973 * @since 1.16.8 (J) - 1.6.8 (WP)
3592 3974 */
3593 - VBOFactory::getPlatform()->getDispatcher()->trigger('onAfterCreateNewBookingAdmin', [$newoid]);
3975 + VBOFactory::getPlatform()->getDispatcher()->trigger($bookingId ? 'onAfterUpdateBookingAdmin' : 'onAfterCreateNewBookingAdmin', [$newoid]);
3594 3976 }
3595 3977
3596 3978 return true;
3597 3979 }
@@ -3637,6 +4019,27 @@
3637 4019 }
3638 4020
3639 4021 // nothing was found
3640 4022 return '';
4023 + }
4024 +
4025 + /**
4026 + * Given a payment method record ID, returns the string
4027 + * in the format for saving it along the reservation.
4028 + *
4029 + * @param int $id The record ID.
4030 + *
4031 + * @return ?string ID=Name calculated value, or null.
4032 + *
4033 + * @since 1.18.15 (J) - 1.8.15 (WP)
4034 + */
4035 + protected function getPaymentMethodValue(int $id)
4036 + {
4037 + $record = VBOMvcModel::getInstance('payment')->getItem($id);
4038 +
4039 + if ($record) {
4040 + return sprintf('%s=%s', $record->id, $record->name);
4041 + }
4042 +
4043 + return null;
3641 4044 }
3642 4045 }