PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.9
VikBooking Hotel Booking Engine & PMS v1.8.9
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 / booking / relocator.php

relocator.php in VikBooking Hotel Booking Engine & PMS 1.8.9, at admin/helpers/src/booking/relocator.php

307 lines 11.5 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) 2026 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 * Booking relocator implementation.
16 *
17 * @since 1.18.7 (J) - 1.8.7 (WP)
18 */
19 final class VBOBookingRelocator
20 {
21 /**
22 * @var array
23 */
24 private array $options = [];
25
26 /**
27 * @var ?VBOBookingRegistry
28 */
29 private ?VBOBookingRegistry $registry = null;
30
31 /**
32 * @var int
33 */
34 private int $daysLimBound = 7;
35
36 /**
37 * Proxy to construct the object.
38 *
39 * @param array $options Associative list of booking information to bind.
40 *
41 * @return static
42 */
43 public static function getInstance(array $options)
44 {
45 return new static($options);
46 }
47
48 /**
49 * Class constructor.
50 *
51 * @param array $options Associative list of booking information to bind.
52 *
53 * @throws Exception
54 */
55 public function __construct(array $options)
56 {
57 if (empty($options['id'])) {
58 throw new InvalidArgumentException('Missing booking ID.', 400);
59 }
60
61 // construct full booking registry
62 $this->registry = VBOBookingRegistry::getInstance(['id' => (int) $options['id']]);
63
64 // bind internal options
65 $this->options = $options;
66 }
67
68 /**
69 * Calculates the necessary moves to assign a sub-unit to the current room
70 * reservation by taking into account all bookings around the involved dates.
71 *
72 * @return VBOBookingSubunitMoveset Room booking records moveset object.
73 *
74 * @throws Exception
75 */
76 public function findRelocation()
77 {
78 // identify the room ID to relocate
79 $relocateRoomId = $this->options['id_room'] ?? 0;
80
81 if (!$relocateRoomId) {
82 foreach ($this->registry->getRooms() as $bookedRoom) {
83 // assume we are relocating the first room booked
84 $relocateRoomId = (int) $bookedRoom['idroom'];
85 break;
86 }
87 }
88
89 if (!$relocateRoomId || !in_array($relocateRoomId, $this->registry->getBookedListingIds())) {
90 // unknown room to relocate
91 throw new Exception('Unknown room ID to relocate.', 404);
92 }
93
94 // let the registry load the details of the room to relocate
95 $this->registry->getRoomDetails($relocateRoomId);
96
97 // identify an eligible room booking record to relocate
98 $relocateRoomRecord = [];
99 $relocateRoomIndex = 0;
100 foreach ($this->registry->getRooms() as $bookedIndex => $bookedRoom) {
101 if ($bookedRoom['idroom'] == $relocateRoomId && empty($bookedRoom['roomindex'])) {
102 // room booking record with empty room index identified
103 $relocateRoomRecord = $bookedRoom;
104 $relocateRoomIndex = $bookedIndex;
105 if (($this->options['booking_room_index'] ?? -1) == $bookedIndex) {
106 // exact booking room index match found
107 break;
108 }
109 }
110 }
111
112 if (!$relocateRoomRecord) {
113 // no room records to relocate
114 throw new Exception('No room records to relocate.', 404);
115 }
116
117 // make sure to set the current room index within the booking registry
118 $this->registry->setCurrentRoomIndex($relocateRoomIndex);
119
120 // build the room sub-unit matrix
121 $subunitMatrix = $this->buildSubunitMatrix($relocateRoomRecord);
122
123 // attempt allocate the current record by returning the first fitting moveset
124 return $subunitMatrix->relocateRoomRecords();
125 }
126
127 /**
128 * Builds and returns the room booking records matrix.
129 *
130 * @param ?array $relocateRoomRecord Optional room booking record to relocate.
131 *
132 * @return VBOBookingSubunitMatrix
133 *
134 * @throws Exception
135 */
136 private function buildSubunitMatrix(?array $relocateRoomRecord = null)
137 {
138 $dbo = JFactory::getDbo();
139
140 // access the stay timestamps for the room record to relocate
141 $relocateTimestamps = $this->registry->getStayTimestamps($roomLevel = true);
142
143 // access the room ID to relocate
144 $relocateRoomId = $this->registry->getCurrentRoomID();
145
146 // build stay dates iterable period
147 $stayPeriod = $this->registry->buildStayPeriodInterval('P1D', $relocateTimestamps[0], $relocateTimestamps[1]);
148
149 // build date bounds according to settings or default values
150 $backTimestamp = strtotime('00:00:00', strtotime(sprintf('-%d days', (int) ($this->options['days_bound'] ?? $this->daysLimBound)), $relocateTimestamps[0]));
151 $forthTimestamp = strtotime('23:59:59', strtotime(sprintf('+%d days', (int) ($this->options['days_bound'] ?? $this->daysLimBound)), $relocateTimestamps[1]));
152
153 // booking IDs occupied records
154 $bookingBusyRecords = [];
155
156 // query the database to gather the occupied records
157 $dbo->setQuery(
158 $dbo->getQuery(true)
159 ->select($dbo->qn('b') . '.*')
160 ->select($dbo->qn('ob.idorder'))
161 ->from($dbo->qn('#__vikbooking_busy', 'b'))
162 ->leftJoin($dbo->qn('#__vikbooking_ordersbusy', 'ob') . ' ON ' . $dbo->qn('b.id') . ' = ' . $dbo->qn('ob.idbusy'))
163 ->where($dbo->qn('b.idroom') . ' = ' . $relocateRoomId)
164 ->where($dbo->qn('b.checkin') . ' <= ' . $forthTimestamp)
165 ->where($dbo->qn('b.realback') . ' >= ' . $backTimestamp)
166 ->order($dbo->qn('b.id') . ' ASC')
167 );
168
169 // scan all occupied records
170 foreach ($dbo->loadAssocList() as $busyRecord) {
171 // start container, if needed
172 $bookingBusyRecords[$busyRecord['idorder']] = $bookingBusyRecords[$busyRecord['idorder']] ?? [];
173 // push booking occupied record
174 $bookingBusyRecords[$busyRecord['idorder']][] = $busyRecord;
175 }
176
177 if (!$bookingBusyRecords) {
178 // no records occupied, hence nothing to relocate
179 throw new Exception('No occupied records, nothing to relocate.', 406);
180 }
181
182 // query the database to obtain the involved booking room records
183 $dbo->setQuery(
184 $dbo->getQuery(true)
185 ->select([
186 $dbo->qn('or.id'),
187 $dbo->qn('or.idorder'),
188 $dbo->qn('or.roomindex'),
189 $dbo->qn('b.closure'),
190 ])
191 ->from($dbo->qn('#__vikbooking_ordersrooms', 'or'))
192 ->leftJoin($dbo->qn('#__vikbooking_orders', 'b') . ' ON ' . $dbo->qn('or.idorder') . ' = ' . $dbo->qn('b.id'))
193 ->where($dbo->qn('or.idroom') . ' = ' . $relocateRoomId)
194 ->where($dbo->qn('or.idorder') . ' IN (' . implode(', ', array_map('intval', array_keys($bookingBusyRecords))) . ')')
195 ->order($dbo->qn('or.idorder') . ' ASC')
196 ->order($dbo->qn('or.id') . ' ASC')
197 );
198
199 $bookingRoomRecords = $dbo->loadAssocList();
200 $lastBid = null;
201 $indexes = [];
202 foreach ($bookingRoomRecords as $bookingRoomRecord) {
203 if (!($bookingBusyRecords[$bookingRoomRecord['idorder']] ?? null)) {
204 // we must be dealing with a ghost record
205 continue;
206 }
207
208 // guess booking busy index
209 $bookingBusyIndex = $lastBid == $bookingRoomRecord['idorder'] && isset($indexes[$lastBid]) ? $indexes[$lastBid] : 0;
210
211 // update room index, closure and room-booking record ID on proper booking busy record
212 $updateBookingIndex = isset($bookingBusyRecords[$bookingRoomRecord['idorder']][$bookingBusyIndex]) ? $bookingBusyIndex : 0;
213 $bookingBusyRecords[$bookingRoomRecord['idorder']][$updateBookingIndex] = array_merge(
214 $bookingBusyRecords[$bookingRoomRecord['idorder']][$updateBookingIndex],
215 [
216 'room_booking_id' => $bookingRoomRecord['id'],
217 'roomindex' => $bookingRoomRecord['roomindex'],
218 'closure' => $bookingRoomRecord['closure'],
219 ]
220 );
221
222 // update values
223 $lastBid = $bookingRoomRecord['idorder'];
224 $indexes[$lastBid] = $bookingBusyIndex + 1;
225 }
226
227 // ensure the room booking record to relocate is found
228 $matchFound = false;
229 foreach ($bookingBusyRecords as $bid => $busyRecords) {
230 foreach ($busyRecords as $bidIndex => $busyRecord) {
231 if (!empty($busyRecord['roomindex'])) {
232 // already allocated
233 continue;
234 }
235
236 if ($relocateRoomRecord && ($busyRecord['room_booking_id'] ?? 0) == $relocateRoomRecord['id']) {
237 // match found from the exact given record to relocate
238 $matchFound = true;
239 } elseif (!$relocateRoomRecord) {
240 // first eligible record found
241 $matchFound = true;
242 }
243
244 if ($matchFound === true) {
245 // set flag to identify the room booking record to relocate
246 $bookingBusyRecords[$bid][$bidIndex]['relocate'] = true;
247
248 // abort
249 break 2;
250 }
251 }
252 }
253
254 if (!$matchFound) {
255 throw new Exception('No matching record to relocate against the occupied records.', 404);
256 }
257
258 // build a map of stay dates and related units occupied
259 $daysUnitsMap = [];
260 foreach ($stayPeriod as $stayNight) {
261 // count units booked and involved room booking IDs for the current stay day
262 $dayUnitsBooked = 0;
263 $dayRoomBookingIds = [];
264 $dayStayTs = $stayNight->format('U');
265 foreach ($bookingBusyRecords as $busyRecords) {
266 foreach ($busyRecords as $busyRecord) {
267 if ($busyRecord['checkin'] <= $dayStayTs && $busyRecord['realback'] >= $dayStayTs) {
268 // update values
269 $dayUnitsBooked++;
270 $dayRoomBookingIds[] = $busyRecord['room_booking_id'];
271 }
272 }
273 }
274
275 // make sure the stay night is not overbooked
276 if ($dayUnitsBooked > ($this->registry->getRoomDetails($relocateRoomId)['units'] ?? 0)) {
277 // abort
278 throw new Exception(sprintf('Cannot relocate room record on %s because room is overbooked.', $stayNight->format('Y-m-d')), 403);
279 }
280
281 // set values
282 $daysUnitsMap[$stayNight->format('Y-m-d')] = [
283 'units_booked' => $dayUnitsBooked,
284 'room_booking_ids' => $dayRoomBookingIds,
285 ];
286 }
287
288 // get an instance of a room-booking sub-unit matrix
289 $matrix = new VBOBookingSubunitMatrix($this->registry, $daysUnitsMap);
290
291 // inject relocation options
292 $matrix->setOptions($this->options);
293
294 // iterate all room booking records
295 foreach ($bookingBusyRecords as $bid => $busyRecords) {
296 foreach ($busyRecords as $bidIndex => $busyRecord) {
297 // push room-booking sub-unit record to matrix
298 $matrix->pushRecord(
299 new VBOBookingSubunitRecord($busyRecord, $bidIndex)
300 );
301 }
302 }
303
304 return $matrix;
305 }
306 }
307