Add lane reservation support: implement reservation timer, expiration logic, and new self-serve lane commands (RESERVE and RELEASE); enhance permission checks across routes for global and department-level operations.

This commit is contained in:
Jeppe Bundgaard
2026-01-15 11:22:02 +01:00
parent 90802e9bc7
commit ed7299fc43
12 changed files with 162 additions and 16 deletions
@@ -12,6 +12,7 @@ require_once WD . '/modules/selfserve/traits/selfserve_lane_timer_t.php';
require_once WD . '/modules/selfserve/traits/selfserve_lane_license_plate_t.php';
require_once WD . '/modules/selfserve/traits/selfserve_lane_customer_number_t.php';
require_once WD . '/modules/selfserve/traits/selfserve_lane_invoice_t.php';
require_once WD . '/modules/selfserve/traits/selfserve_lane_reservation_timer_t.php';
require_once WD . '/modules/selfserve/traits/selfserve_lane_log_t.php';
use modules\selfserve\interfaces\selfserve_lane_i;
@@ -23,6 +24,7 @@ use modules\selfserve\traits\selfserve_lane_license_plate_t;
use modules\selfserve\traits\selfserve_lane_log_t;
use modules\selfserve\traits\selfserve_lane_mode_t;
use modules\selfserve\traits\selfserve_lane_port_controller_t;
use modules\selfserve\traits\selfserve_lane_reservation_timer_t;
use modules\selfserve\traits\selfserve_lane_state_t;
use modules\selfserve\traits\selfserve_lane_status_t;
use modules\selfserve\traits\selfserve_lane_timer_t;
@@ -39,6 +41,7 @@ class selfserve_lane implements selfserve_lane_i
selfserve_lane_license_plate_t,
selfserve_lane_customer_number_t,
selfserve_lane_invoice_t,
selfserve_lane_reservation_timer_t,
selfserve_lane_log_t;
/**
@@ -7,6 +7,8 @@ enum selfserve_lane_command
case START; // command to start the lane
case STOP; // command to stop the lane
case RESET; // command to reset the lane
case RESERVE; // command to reserve the lane
case RELEASE; // command to release the lane (from reservation)
public static function tryFrom(string $commandParam): ?selfserve_lane_command
{
@@ -14,6 +16,8 @@ enum selfserve_lane_command
'START' => selfserve_lane_command::START,
'STOP' => selfserve_lane_command::STOP,
'RESET' => selfserve_lane_command::RESET,
'RESERVE' => selfserve_lane_command::RESERVE,
'RELEASE' => selfserve_lane_command::RELEASE,
default => null,
};
}
@@ -12,6 +12,8 @@ trait selfserve_lane_cache_t
const CACHE_SELFSERVE_LANE_KEY_STATE = self::CACHE_SELFSERVE_PREFIX . 'state';
const CACHE_SELFSERVE_LANE_KEY_MODE = self::CACHE_SELFSERVE_PREFIX . 'mode';
const CACHE_SELFSERVE_LANE_KEY_WASH_START_TIME = self::CACHE_SELFSERVE_PREFIX . 'wash_start_time';
const CACHE_SELFSERVE_LANE_KEY_RESERVATION_START_TIME = self::CACHE_SELFSERVE_PREFIX . 'lane_reservation_start_time';
const CACHE_SELFSERVE_LANE_KEY_CUSTOMER_NUMBER = self::CACHE_SELFSERVE_PREFIX . 'customer_number';
const CACHE_SELFSERVE_LANE_KEY_LICENSE_PLATE = self::CACHE_SELFSERVE_PREFIX . 'license_plate';
@@ -33,6 +33,35 @@ trait selfserve_lane_command_t
{
// Handle the command
switch ($command) {
case selfserve_lane_command::RESERVE:
// Require lane to be available before reserving
if (!$this->getLaneStatus()->equals(selfserve_lane_status::AVAILABLE)) throw new \RuntimeException("Cannot reserve lane: Lane is not available.");
// Require the customer number
if (empty($customer_number = $arguments->customer_number)) throw new \InvalidArgumentException("Customer number is required to reserve the lane.");
// Validate customer number
if (!is_numeric($customer_number) || (int)$customer_number <= 0) throw new \InvalidArgumentException("Invalid customer number: " . $customer_number);
if (!(new users_o())->getUserByCustomerNumber((int)$customer_number)->exists()) throw new \InvalidArgumentException("Customer number does not exist: " . $customer_number);
// Set the customer number
$this->setCustomerNumber($customer_number);
// Set the lane status to RESERVED when reserved
$this->setLaneStatus(selfserve_lane_status::RESERVED);
// Set the reservation time
$this->setReservationStartTime(time());
// Log the lane reserve event
$this->logLaneAction(selfserve_lane_log_action::RESERVE_LANE);
break;
case selfserve_lane_command::RELEASE:
// Require lane to be reserved before releasing
if (!$this->getLaneStatus()->equals(selfserve_lane_status::RESERVED)) throw new \RuntimeException("Cannot release lane: Lane is not reserved.");
// Check if the customer number equals the argument customer number
if (($this->getCustomerNumber() !== $arguments->customer_number) && !$this->isBypassCustomerNumberValidation()) {
throw new \InvalidArgumentException("Customer number mismatch: Lane customer number " . $this->getCustomerNumber() . " does not match argument customer number " . $arguments->customer_number);
}
// Reset the lane
self::execute(selfserve_lane_command::RESET, new selfserve_lane_command_arguments());
// Log the lane release event
$this->logLaneAction(selfserve_lane_log_action::RELEASE_LANE);
break;
case selfserve_lane_command::START:
// Require lane to be available before starting
if (!$this->getLaneStatus()->equals(selfserve_lane_status::AVAILABLE)) throw new \RuntimeException("Cannot start lane: Lane is not available.");
@@ -81,6 +110,7 @@ trait selfserve_lane_command_t
$this->setWashStartTime(self::DEFAULT_WASH_START_TIME);
$this->setCustomerNumber(self::DEFAULT_CUSTOMER_NUMBER);
$this->setLicensePlate(self::DEFAULT_LICENSE_PLATE);
$this->setReservationStartTime(null);
break;
default:
throw new \InvalidArgumentException("Unknown command: " . $command->name);
@@ -0,0 +1,80 @@
<?php
namespace modules\selfserve\traits;
require_once WD . '/modules/selfserve/helpers/selfserve_lane_status.php';
require_once WD . '/modules/selfserve/classes/selfserve_lane.php';
use modules\selfserve\classes\selfserve_lane;
use modules\selfserve\classes\selfserve_lane_command_arguments;
use modules\selfserve\helpers\selfserve_lane_command;
use modules\selfserve\helpers\selfserve_lane_status;
trait selfserve_lane_reservation_timer_t
{
const DEFAULT_LANE_RESERVATION_TIME = 300 * 1000; // Default reservation time in milliseconds (5 minutes)
/**
* The timestamp of the reservation time of the lane
* @var int|null $reservation_start_time
*/
public ?int $reservation_start_time = null;
/**
* Get the lane reservation time
* @return int|null The timestamp of the reservation time of the lane, or null if not set
*/
public function getReservationStartTime(): ?int
{
$reservation_start_time = $this->getLaneCache($this->id, self::CACHE_SELFSERVE_LANE_KEY_RESERVATION_START_TIME);
if (!empty($reservation_start_time)) {
$this->reservation_start_time = $reservation_start_time;
}
return $this->reservation_start_time;
}
/**
* Set the lane reservation time
* @param int|null $timestamp The timestamp of the reservation time of the lane, or null to clear
* @return selfserve_lane_reservation_timer_t|selfserve_lane
*/
public function setReservationStartTime(?int $timestamp): self
{
$this->reservation_start_time = $timestamp;
$this->setLaneCache($this->id, self::CACHE_SELFSERVE_LANE_KEY_RESERVATION_START_TIME, $this->reservation_start_time);
return $this;
}
/**
* Get the elapsed time since the reservation started
* @return int|null The elapsed time in seconds since the reservation started, or null if reservation has not started
*/
public function getElapsedReservationTime(): ?int
{
$reservation_start_time = $this->getReservationStartTime();
if ($reservation_start_time === null) {
return null; // Reservation has not started
}
return time() - $reservation_start_time;
}
/**
* Get remaining reservation time
* @return int|null The remaining reservation time in seconds, or null if the reservation has not started
*/
public function getRemainingReservationTime(): ?int
{
$elapsed_time = $this->getElapsedReservationTime();
if ($elapsed_time === null) {
return null; // Reservation has not started
}
$remaining_time = (self::DEFAULT_LANE_RESERVATION_TIME / 1000) - $elapsed_time;
return max($remaining_time, 0);
}
/**
* Check if the reservation has expired
* @return bool True if the reservation has expired, false otherwise
*/
public function isReservationExpired(): bool
{
$remaining_time = $this->getRemainingReservationTime();
if ($remaining_time === null) {
return false; // Reservation has not started
}
return $remaining_time <= 0;
}
}
@@ -3,7 +3,11 @@
namespace modules\selfserve\traits;
require_once WD . '/modules/selfserve/helpers/selfserve_lane_status.php';
require_once WD . '/modules/selfserve/classes/selfserve_lane.php';
use Exception;
use modules\selfserve\classes\selfserve_lane;
use modules\selfserve\classes\selfserve_lane_command_arguments;
use modules\selfserve\helpers\selfserve_lane_command;
use modules\selfserve\helpers\selfserve_lane_log_action;
use modules\selfserve\helpers\selfserve_lane_status;
@@ -20,6 +24,7 @@ trait selfserve_lane_status_t
/**
* Get the current status of the self-serve lane
* @return selfserve_lane_status The current status of the self-serve lane
* @throws Exception If the execution of the release command fails
*/
public function getLaneStatus(): selfserve_lane_status
{
@@ -32,6 +37,11 @@ trait selfserve_lane_status_t
$this->status = selfserve_lane_status::AVAILABLE;
$this->setLaneCache($this->id, self::CACHE_SELFSERVE_LANE_KEY_STATUS, $this->status);
}
// If the status is RESERVED, check if the reservation has expired
if ($this->status->equals(selfserve_lane_status::RESERVED) && $this->isReservationExpired()) {
// Release the lane if the reservation has expired
$this->execute(selfserve_lane_command::RELEASE, new selfserve_lane_command_arguments());
}
return $this->status;
}
@@ -4,7 +4,6 @@ namespace modules\selfserve\traits;
require_once WD . '/modules/selfserve/helpers/selfserve_lane_status.php';
require_once WD . '/modules/selfserve/classes/selfserve_lane.php';
use modules\selfserve\classes\selfserve_lane;
use modules\selfserve\helpers\selfserve_lane_status;
trait selfserve_lane_timer_t
{