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
{
@@ -38,8 +38,10 @@ class departmentSelfserveConditionRulesRoute
$condition_o = new department_selfserve_conditions_o();
$condition_o->select((int)$rules_o->condition_id->value());
$authorized_department_ids = $user->getGroup()->getDepartments();
if ($condition_o->exists() && !in_array((int)$condition_o->department->value(), $authorized_department_ids)) {
$response->error('You do not have access to this department', 403);
if ($condition_o->exists()) {
if (!in_array((int)$condition_o->department->value(), $authorized_department_ids) && !$this->hasPermission('view_all_department_selfserve_condition_rules')) {
$response->error('You do not have access to this department', 403);
}
}
$response->success($rules_o->asArray());
} else {
@@ -56,7 +58,7 @@ class departmentSelfserveConditionRulesRoute
$condition_o->select($filters['condition_id']);
if ($condition_o->exists()) {
$authorized_department_ids = $user->getGroup()->getDepartments();
if (!in_array((int)$condition_o->department->value(), $authorized_department_ids)) {
if (!in_array((int)$condition_o->department->value(), $authorized_department_ids) && !$this->hasPermission('view_all_department_selfserve_condition_rules')) {
$response->error('You do not have access to this department', 403);
}
}
@@ -86,7 +88,8 @@ class departmentSelfserveConditionRulesRoute
$response->error('Invalid session', 400);
}
}, [
'list_department_selfserve_condition_rules' => 'List all department self-serve condition rules'
'list_department_selfserve_condition_rules' => 'List all department self-serve condition rules',
'view_all_department_selfserve_condition_rules' => 'View all department self-serve condition rules'
]);
/**
@@ -35,7 +35,9 @@ class departmentSelfserveConditionsRoute
$conditions_o->select((int)self::getParameter('id'));
if ($conditions_o->exists()) {
if (!in_array((int)$conditions_o->department->value(), $authorized_department_ids)) {
$response->error('You do not have access to this department', 403);
if (!$this->hasPermission('view_all_department_selfserve_conditions')) {
$response->error('You do not have access to this department', 403);
}
}
$response->success($conditions_o->asArray());
} else {
@@ -46,7 +48,7 @@ class departmentSelfserveConditionsRoute
$filters = [];
if (self::isParametersSet(['department'])) {
$requested_department = (int)self::getParameter('department');
if (!in_array($requested_department, $authorized_department_ids)) {
if (!in_array($requested_department, $authorized_department_ids) && !$this->hasPermission('view_all_department_selfserve_conditions')) {
$response->error('You do not have access to this department', 403);
}
$filters['department'] = $requested_department;
@@ -74,7 +76,8 @@ class departmentSelfserveConditionsRoute
$response->error('Invalid session', 400);
}
}, [
'list_department_selfserve_conditions' => 'List all department self-serve conditions'
'list_department_selfserve_conditions' => 'List all department self-serve conditions',
'view_all_department_selfserve_conditions' => 'View all department self-serve conditions'
]);
/**
@@ -48,7 +48,7 @@ class departmentSelfserveQuestionsRoute
$filters = [];
if (self::isParametersSet(['department'])) {
$requested_department = (int)self::getParameter('department');
if (!in_array($requested_department, $authorized_department_ids)) {
if (!in_array($requested_department, $authorized_department_ids) && !$this->hasPermission('view_all_department_selfserve_questions')) {
$response->error('You do not have access to this department', 403);
}
$filters['department'] = $requested_department;
@@ -38,7 +38,9 @@ class departmentSelfserveTasksRoute
$tasks_o->select((int)self::getParameter('id'));
if ($tasks_o->exists()) {
if (!in_array((int)$tasks_o->department->value(), $authorized_department_ids)) {
$response->error('You do not have access to this department', 403);
if (!$this->hasPermission('view_all_department_selfserve_tasks')) {
$response->error('You do not have access to this department', 403);
}
}
$response->success($tasks_o->asArray());
} else {
@@ -49,7 +51,7 @@ class departmentSelfserveTasksRoute
$filters = [];
if (self::isParametersSet(['department'])) {
$requested_department = (int)self::getParameter('department');
if (!in_array($requested_department, $authorized_department_ids)) {
if (!in_array($requested_department, $authorized_department_ids) && !$this->hasPermission('view_all_department_selfserve_tasks')) {
$response->error('You do not have access to this department', 403);
}
$filters['department'] = $requested_department;
@@ -81,7 +83,8 @@ class departmentSelfserveTasksRoute
$response->error('Invalid session', 400);
}
}, [
'list_department_selfserve_tasks' => 'List all department self-serve tasks'
'list_department_selfserve_tasks' => 'List all department self-serve tasks',
'view_all_department_selfserve_tasks' => 'View all department self-serve tasks'
]);
/**
@@ -8,6 +8,7 @@ use classes\response;
use classes\router;
use classes\selfserve;
use classes\stripe;
use modules\selfserve\helpers\selfserve_lane_command;
use objects\departments_o;
use objects\logs_o;
use objects\orders_o;
@@ -69,19 +70,25 @@ class moduleSelfServeRoute
if (self::hasPermission('modules_selfserve_lane_command_bypass_customer_number_validation')) {
$lane->setBypassCustomerNumberValidation(true);
}
$command = \modules\selfserve\helpers\selfserve_lane_command::tryFrom($commandParam);
$command = selfserve_lane_command::tryFrom($commandParam);
if ($command === null) {
$response->error("Invalid command: " . $commandParam);
}
// Require permissions for specific commands
switch ($command) {
case \modules\selfserve\helpers\selfserve_lane_command::START:
case selfserve_lane_command::START:
self::requirePermission('modules_selfserve_lane_command_execute_start');
break;
case \modules\selfserve\helpers\selfserve_lane_command::STOP:
case selfserve_lane_command::STOP:
self::requirePermission('modules_selfserve_lane_command_execute_stop');
break;
case \modules\selfserve\helpers\selfserve_lane_command::RESET:
case selfserve_lane_command::RESERVE:
self::requirePermission('modules_selfserve_lane_command_execute_reserve');
break;
case selfserve_lane_command::RELEASE:
self::requirePermission('modules_selfserve_lane_command_execute_release');
break;
case selfserve_lane_command::RESET:
self::requirePermission('modules_selfserve_lane_command_execute_reset');
break;
}
@@ -111,6 +118,8 @@ class moduleSelfServeRoute
'modules_selfserve_lane_command_execute' => 'Execute self-serve lane command. This is required together with specific command permissions below.',
'modules_selfserve_lane_command_execute_start' => 'Execute self-serve lane START command',
'modules_selfserve_lane_command_execute_stop' => 'Execute self-serve lane STOP command',
'modules_selfserve_lane_command_execute_reserve' => 'Execute self-serve lane RESERVE command',
'modules_selfserve_lane_command_execute_release' => 'Execute self-serve lane RELEASE command (Release the lane reservation and reset its state)',
'modules_selfserve_lane_command_execute_reset' => 'Execute self-serve lane RESET command',
'modules_selfserve_lane_command_bypass_customer_number_validation' => 'Bypass customer number validation when executing commands',
]