PluginProbe
VikBooking Hotel Booking Engine & PMS / trunk
VikBooking Hotel Booking Engine & PMS vtrunk
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 1.7.3 All 36 releases
vikbooking / admin / helpers / src / booking / relocator.php

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

428 lines 15.8 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 * Applies (or undoes) the relocation moveset to complete the room re-assignment operation.
129 *
130 * @param VBOBookingSubunitMoveset $moveset The moveset to process and apply.
131 * @param bool $undo True to undo the applied moveset.
132 *
133 * @return true
134 *
135 * @throws Exception
136 *
137 * @since 1.18.11 (J) - 1.8.11 (WP)
138 */
139 public function applyMoveset(VBOBookingSubunitMoveset $moveset, bool $undo = false)
140 {
141 // obtain moveset signature
142 $signature = $moveset->getSignature();
143
144 return $this->execMovesetSignature($signature, $undo);
145 }
146
147 /**
148 * Executes the relocation moveset signature to complete the room re-assignment operation.
149 *
150 * @param string $signature The moveset signature to execute.
151 * @param bool $undo True to undo the applied moveset.
152 *
153 * @return true
154 *
155 * @throws Exception
156 *
157 * @since 1.18.11 (J) - 1.8.11 (WP)
158 */
159 public function execMovesetSignature(string $signature, bool $undo = false)
160 {
161 $dbo = JFactory::getDbo();
162
163 // room details empty container
164 $roomDetails = [];
165
166 // confirm the integrity of the operations to execute
167 $confirmedOperations = [];
168
169 // scan operation steps
170 $operationSteps = explode('-', $signature);
171 foreach ($operationSteps as $operationStep) {
172 // obtain move details
173 $moveData = explode('.', $operationStep);
174 if (count($moveData) < 4) {
175 throw new Exception('Invalid moveset operation.', 400);
176 }
177
178 // fetch requested room record
179 $dbo->setQuery(
180 $dbo->getQuery(true)
181 ->select('*')
182 ->from($dbo->qn('#__vikbooking_ordersrooms'))
183 ->where($dbo->qn('id') . ' = ' . (int) ($moveData[1] ?? 0))
184 ->where($dbo->qn('idorder') . ' = ' . (int) ($moveData[0] ?? 0))
185 );
186 $roomRecord = $dbo->loadAssoc();
187
188 if (!$roomRecord) {
189 throw new Exception('Could not find moveset operation.', 404);
190 }
191
192 // confirm operation
193 $confirmedOperations[] = $moveData;
194
195 if (!$roomDetails) {
196 // load the first and only room details
197 $roomDetails = VikBooking::getRoomInfo((int) $roomRecord['idroom'], ['id', 'name'], true);
198 }
199 }
200
201 if (!$confirmedOperations) {
202 throw new Exception('No valid moveset operations.', 400);
203 }
204
205 // get current CMS user and name
206 $user = JFactory::getUser();
207 $userName = $user->name;
208
209 // determine action name
210 $actionName = $undo ? JText::translate('VBO_UNDO_CHANGES') : JText::translate('VBO_RESOLVE_ROOM_ASSIGNMENT');
211
212 // apply all moves
213 foreach ($confirmedOperations as $moveData) {
214 // determine the room index to apply
215 $prev_room_index = $undo ? (int) ($moveData[3] ?? 0) : (int) ($moveData[2] ?? 0);
216 $new_room_index = $undo ? (int) ($moveData[2] ?? 0) : (int) ($moveData[3] ?? 0);
217
218 // update current room record
219 $dbo->setQuery(
220 $dbo->getQuery(true)
221 ->update($dbo->qn('#__vikbooking_ordersrooms'))
222 ->set($dbo->qn('roomindex') . ' = ' . ($new_room_index ?: 'NULL'))
223 ->where($dbo->qn('id') . ' = ' . (int) ($moveData[1] ?? 0))
224 ->where($dbo->qn('idorder') . ' = ' . (int) ($moveData[0] ?? 0))
225 );
226 $dbo->execute();
227
228 // Booking History
229 VikBooking::getBookingHistoryInstance((int) ($moveData[0] ?? 0))
230 ->setExtraData([
231 'action' => 'resolve_room_assignment',
232 'type' => $undo ? 'undo' : 'apply',
233 ])
234 ->store(
235 'MB',
236 sprintf(
237 '(%s) %s: %s',
238 (string) $userName,
239 $actionName,
240 JText::sprintf('VBOROOMSUBUNITCHANGEFT', $roomDetails['name'], $prev_room_index, $new_room_index)
241 )
242 );
243 }
244
245 return true;
246 }
247
248 /**
249 * Builds and returns the room booking records matrix.
250 *
251 * @param ?array $relocateRoomRecord Optional room booking record to relocate.
252 *
253 * @return VBOBookingSubunitMatrix
254 *
255 * @throws Exception
256 */
257 private function buildSubunitMatrix(?array $relocateRoomRecord = null)
258 {
259 $dbo = JFactory::getDbo();
260
261 // access the stay timestamps for the room record to relocate
262 $relocateTimestamps = $this->registry->getStayTimestamps($roomLevel = true);
263
264 // access the room ID to relocate
265 $relocateRoomId = $this->registry->getCurrentRoomID();
266
267 // build stay dates iterable period
268 $stayPeriod = $this->registry->buildStayPeriodInterval('P1D', $relocateTimestamps[0], $relocateTimestamps[1]);
269
270 // build date bounds according to settings or default values
271 $backTimestamp = strtotime('00:00:00', strtotime(sprintf('-%d days', (int) ($this->options['days_bound'] ?? $this->daysLimBound)), $relocateTimestamps[0]));
272 $forthTimestamp = strtotime('23:59:59', strtotime(sprintf('+%d days', (int) ($this->options['days_bound'] ?? $this->daysLimBound)), $relocateTimestamps[1]));
273
274 // booking IDs occupied records
275 $bookingBusyRecords = [];
276
277 // query the database to gather the occupied records
278 $dbo->setQuery(
279 $dbo->getQuery(true)
280 ->select($dbo->qn('b') . '.*')
281 ->select($dbo->qn('ob.idorder'))
282 ->from($dbo->qn('#__vikbooking_busy', 'b'))
283 ->leftJoin($dbo->qn('#__vikbooking_ordersbusy', 'ob') . ' ON ' . $dbo->qn('b.id') . ' = ' . $dbo->qn('ob.idbusy'))
284 ->where($dbo->qn('b.idroom') . ' = ' . $relocateRoomId)
285 ->where($dbo->qn('b.checkin') . ' <= ' . $forthTimestamp)
286 ->where($dbo->qn('b.realback') . ' >= ' . $backTimestamp)
287 ->order($dbo->qn('b.id') . ' ASC')
288 );
289
290 // scan all occupied records
291 foreach ($dbo->loadAssocList() as $busyRecord) {
292 // start container, if needed
293 $bookingBusyRecords[$busyRecord['idorder']] = $bookingBusyRecords[$busyRecord['idorder']] ?? [];
294 // push booking occupied record
295 $bookingBusyRecords[$busyRecord['idorder']][] = $busyRecord;
296 }
297
298 if (!$bookingBusyRecords) {
299 // no records occupied, hence nothing to relocate
300 throw new Exception('No occupied records, nothing to relocate.', 406);
301 }
302
303 // query the database to obtain the involved booking room records
304 $dbo->setQuery(
305 $dbo->getQuery(true)
306 ->select([
307 $dbo->qn('or.id'),
308 $dbo->qn('or.idorder'),
309 $dbo->qn('or.roomindex'),
310 $dbo->qn('b.closure'),
311 ])
312 ->from($dbo->qn('#__vikbooking_ordersrooms', 'or'))
313 ->leftJoin($dbo->qn('#__vikbooking_orders', 'b') . ' ON ' . $dbo->qn('or.idorder') . ' = ' . $dbo->qn('b.id'))
314 ->where($dbo->qn('or.idroom') . ' = ' . $relocateRoomId)
315 ->where($dbo->qn('or.idorder') . ' IN (' . implode(', ', array_map('intval', array_keys($bookingBusyRecords))) . ')')
316 ->order($dbo->qn('or.idorder') . ' ASC')
317 ->order($dbo->qn('or.id') . ' ASC')
318 );
319
320 $bookingRoomRecords = $dbo->loadAssocList();
321 $lastBid = null;
322 $indexes = [];
323 foreach ($bookingRoomRecords as $bookingRoomRecord) {
324 if (!($bookingBusyRecords[$bookingRoomRecord['idorder']] ?? null)) {
325 // we must be dealing with a ghost record
326 continue;
327 }
328
329 // guess booking busy index
330 $bookingBusyIndex = $lastBid == $bookingRoomRecord['idorder'] && isset($indexes[$lastBid]) ? $indexes[$lastBid] : 0;
331
332 // update room index, closure and room-booking record ID on proper booking busy record
333 $updateBookingIndex = isset($bookingBusyRecords[$bookingRoomRecord['idorder']][$bookingBusyIndex]) ? $bookingBusyIndex : 0;
334 $bookingBusyRecords[$bookingRoomRecord['idorder']][$updateBookingIndex] = array_merge(
335 $bookingBusyRecords[$bookingRoomRecord['idorder']][$updateBookingIndex],
336 [
337 'room_booking_id' => $bookingRoomRecord['id'],
338 'roomindex' => $bookingRoomRecord['roomindex'],
339 'closure' => $bookingRoomRecord['closure'],
340 ]
341 );
342
343 // update values
344 $lastBid = $bookingRoomRecord['idorder'];
345 $indexes[$lastBid] = $bookingBusyIndex + 1;
346 }
347
348 // ensure the room booking record to relocate is found
349 $matchFound = false;
350 foreach ($bookingBusyRecords as $bid => $busyRecords) {
351 foreach ($busyRecords as $bidIndex => $busyRecord) {
352 if (!empty($busyRecord['roomindex'])) {
353 // already allocated
354 continue;
355 }
356
357 if ($relocateRoomRecord && ($busyRecord['room_booking_id'] ?? 0) == $relocateRoomRecord['id']) {
358 // match found from the exact given record to relocate
359 $matchFound = true;
360 } elseif (!$relocateRoomRecord) {
361 // first eligible record found
362 $matchFound = true;
363 }
364
365 if ($matchFound === true) {
366 // set flag to identify the room booking record to relocate
367 $bookingBusyRecords[$bid][$bidIndex]['relocate'] = true;
368
369 // abort
370 break 2;
371 }
372 }
373 }
374
375 if (!$matchFound) {
376 throw new Exception('No matching record to relocate against the occupied records.', 404);
377 }
378
379 // build a map of stay dates and related units occupied
380 $daysUnitsMap = [];
381 foreach ($stayPeriod as $stayNight) {
382 // count units booked and involved room booking IDs for the current stay day
383 $dayUnitsBooked = 0;
384 $dayRoomBookingIds = [];
385 $dayStayTs = $stayNight->format('U');
386 foreach ($bookingBusyRecords as $busyRecords) {
387 foreach ($busyRecords as $busyRecord) {
388 if ($busyRecord['checkin'] <= $dayStayTs && $busyRecord['realback'] >= $dayStayTs) {
389 // update values
390 $dayUnitsBooked++;
391 $dayRoomBookingIds[] = $busyRecord['room_booking_id'];
392 }
393 }
394 }
395
396 // make sure the stay night is not overbooked
397 if ($dayUnitsBooked > ($this->registry->getRoomDetails($relocateRoomId)['units'] ?? 0)) {
398 // abort
399 throw new Exception(sprintf('Cannot relocate room record on %s because room is overbooked.', $stayNight->format('Y-m-d')), 403);
400 }
401
402 // set values
403 $daysUnitsMap[$stayNight->format('Y-m-d')] = [
404 'units_booked' => $dayUnitsBooked,
405 'room_booking_ids' => $dayRoomBookingIds,
406 ];
407 }
408
409 // get an instance of a room-booking sub-unit matrix
410 $matrix = new VBOBookingSubunitMatrix($this->registry, $daysUnitsMap);
411
412 // inject relocation options
413 $matrix->setOptions($this->options);
414
415 // iterate all room booking records
416 foreach ($bookingBusyRecords as $bid => $busyRecords) {
417 foreach ($busyRecords as $bidIndex => $busyRecord) {
418 // push room-booking sub-unit record to matrix
419 $matrix->pushRecord(
420 new VBOBookingSubunitRecord($busyRecord, $bidIndex)
421 );
422 }
423 }
424
425 return $matrix;
426 }
427 }
428