diff --git a/services/nginx/app/classes/selfserve.php b/services/nginx/app/classes/selfserve.php new file mode 100644 index 00000000..aac0632e --- /dev/null +++ b/services/nginx/app/classes/selfserve.php @@ -0,0 +1,47 @@ +config = new selfserve_c(); + } + + public function lane(int $lane_id): selfserve_lane + { + return new selfserve_lane($lane_id); + } + + /** + * @inheritDoc + * @throws Exception If the module is not enabled + */ + public function requireModuleEnabled(): void + { + if (!(bool)$this->config->enabled->getVariableValue()) { + throw new Exception('selfserve module is not enabled'); + } + } +} \ No newline at end of file diff --git a/services/nginx/app/index.php b/services/nginx/app/index.php index 39a71197..0cc03299 100644 --- a/services/nginx/app/index.php +++ b/services/nginx/app/index.php @@ -77,6 +77,7 @@ require_once 'classes/attachments.php'; require_once 'classes/attachment_store.php'; require_once 'classes/virkdata.php'; require_once 'classes/shelly.php'; +require_once 'classes/selfserve.php'; /** * Modules diff --git a/services/nginx/app/modules/selfserve/classes/selfserve_lane.php b/services/nginx/app/modules/selfserve/classes/selfserve_lane.php new file mode 100644 index 00000000..1b8df851 --- /dev/null +++ b/services/nginx/app/modules/selfserve/classes/selfserve_lane.php @@ -0,0 +1,49 @@ +id = $lane_id; + $this->department_lane = (new \objects\department_lanes_o())->select($lane_id); + } + + /** + * The lane ID + * @var int $id + * @see \objects\department_lanes_o + */ + public int $id; + /** + * Department lane object + * @var \objects\department_lanes_o|null $department_lane + */ + public ?\objects\department_lanes_o $department_lane = null; +} \ No newline at end of file diff --git a/services/nginx/app/modules/selfserve/config/selfserve_enabled_c.php b/services/nginx/app/modules/selfserve/config/selfserve_enabled_c.php new file mode 100644 index 00000000..3c435dce --- /dev/null +++ b/services/nginx/app/modules/selfserve/config/selfserve_enabled_c.php @@ -0,0 +1,29 @@ +setupConfig('selfserve'); + $this->allowUpdate([ + selfserve_enabled_c::class + ]); + $this->enabled = new selfserve_enabled_c(); + } +} \ No newline at end of file diff --git a/services/nginx/app/modules/selfserve/traits/selfserve_lane_cache_t.php b/services/nginx/app/modules/selfserve/traits/selfserve_lane_cache_t.php new file mode 100644 index 00000000..41780fd3 --- /dev/null +++ b/services/nginx/app/modules/selfserve/traits/selfserve_lane_cache_t.php @@ -0,0 +1,70 @@ +set($this->getLaneCacheKey($laneId, $property), serialize($value)); + return $this; + } + + /** + * Get the cache for a lane property + * @param int $laneId The ID of the lane + * @param string $property The property to get the cache for (self::CACHE_SELFSERVE_LANE_KEY_STATUS, self::CACHE_SELFSERVE_LANE_KEY_STATE, self::CACHE_SELFSERVE_LANE_KEY_MODE, ...) + * @param string|null $class The class name of the expected return type + * @return mixed The value from the cache + */ + public function getLaneCache(int $laneId, string $property, string $class = null): mixed + { + $cachedValue = redis->get($this->getLaneCacheKey($laneId, $property)); + // If the cached value is not found, return null + if ($cachedValue === null) { + return null; + } + $value = unserialize($cachedValue); + // If a class is provided, ensure the value is of that class + if ($class !== null && !($value instanceof $class)) { + throw new \UnexpectedValueException("Cached value for lane ID {$laneId} and property {$property} is not of type {$class}"); + } + return $value; + } + /** + * Clear the cache for a lane property + * @param int $laneId The ID of the lane + * @param string $property The property to clear the cache for (self::CACHE_SELFSERVE_LANE_KEY_STATUS, self::CACHE_SELFSERVE_LANE_KEY_STATE, self::CACHE_SELFSERVE_LANE_KEY_MODE, ...) + * @return $this; + */ + public function clearLaneCache(int $laneId, string $property): self + { + redis->delete($this->getLaneCacheKey($laneId, $property)); + return $this; + } +} \ No newline at end of file diff --git a/services/nginx/app/modules/selfserve/traits/selfserve_lane_command_t.php b/services/nginx/app/modules/selfserve/traits/selfserve_lane_command_t.php new file mode 100644 index 00000000..470137a1 --- /dev/null +++ b/services/nginx/app/modules/selfserve/traits/selfserve_lane_command_t.php @@ -0,0 +1,62 @@ +getLaneStatus()->equals(selfserve_lane_status::AVAILABLE)) throw new \RuntimeException("Cannot start lane: Lane is not available."); + // Set the lane status to OCCUPIED when started + $this->setLaneStatus(selfserve_lane_status::OCCUPIED); + // Open the entrance port + $this->open(selfserve_lane_port::ENTRANCE); + // Set the lane state to IN_WASH + $this->setLaneState(selfserve_lane_state::IN_WASH); + break; + case selfserve_lane_command::STOP: + // Require lane to be occupied before stopping + if (!$this->getLaneStatus()->equals(selfserve_lane_status::OCCUPIED)) throw new \RuntimeException("Cannot stop lane: Lane is not occupied."); + // Set the lane status to AVAILABLE when stopped + $this->setLaneStatus(selfserve_lane_status::AVAILABLE); + // Open the exit port + $this->open(selfserve_lane_port::EXIT); + // Set the lane state to IDLE + $this->setLaneState(selfserve_lane_state::IDLE); + break; + case selfserve_lane_command::RESET: + // Reset the lane to default states + $this->setLaneStatus(selfserve_lane_status::AVAILABLE); + $this->setLaneMode(selfserve_lane_mode::MANUAL); + $this->setLaneState(selfserve_lane_state::IDLE); + break; + default: + throw new \InvalidArgumentException("Unknown command: " . $command->name); + } + return $this; + } +} \ No newline at end of file diff --git a/services/nginx/app/modules/selfserve/traits/selfserve_lane_mode_t.php b/services/nginx/app/modules/selfserve/traits/selfserve_lane_mode_t.php new file mode 100644 index 00000000..0da417a0 --- /dev/null +++ b/services/nginx/app/modules/selfserve/traits/selfserve_lane_mode_t.php @@ -0,0 +1,49 @@ +getLaneCache($this->id, self::CACHE_SELFSERVE_LANE_KEY_MODE, selfserve_lane_mode::class); + if (!empty($mode)) { + $this->mode = $mode; + } + else if (empty($this->mode)) { + // If not found in cache, default to MANUAL + $this->mode = selfserve_lane_mode::MANUAL; + $this->setLaneCache($this->id, self::CACHE_SELFSERVE_LANE_KEY_MODE, $this->mode); + } + return $this->mode; + } + + /** + * Set the current mode of the self-serve lane + * @param selfserve_lane_mode $mode + * @return selfserve_lane_mode_t|selfserve_lane + */ + public function setLaneMode(selfserve_lane_mode $mode): self + { + $this->mode = $mode; + $this->setLaneCache($this->id, self::CACHE_SELFSERVE_LANE_KEY_MODE, $this->mode); + return $this; + } +} \ No newline at end of file diff --git a/services/nginx/app/modules/selfserve/traits/selfserve_lane_port_controller_t.php b/services/nginx/app/modules/selfserve/traits/selfserve_lane_port_controller_t.php new file mode 100644 index 00000000..636c8e64 --- /dev/null +++ b/services/nginx/app/modules/selfserve/traits/selfserve_lane_port_controller_t.php @@ -0,0 +1,50 @@ +department_lane)) throw new \Exception("Department lane object not found for lane ID {$this->id}"); + // Check lane status + if ($this->status->equals(selfserve_lane_status::CLOSED)) throw new \Exception("Cannot open port on CLOSED lane"); + if ($this->status->equals(selfserve_lane_status::MAINTENANCE)) throw new \Exception("Cannot open port on MAINTENANCE lane"); + if ($this->status->equals(selfserve_lane_status::FAULT)) throw new \Exception("Cannot open port on FAULT lane"); + // Get the relay ID based on the port + $relay_id = match ($port) { + selfserve_lane_port::ENTRANCE => $this->department_lane->relay_out_id->value(), + selfserve_lane_port::EXIT => $this->department_lane->relay_in_id->value(), + default => throw new \Exception("Invalid port specified, must be ENTRANCE or EXIT"), + }; + // Make sure relay ID is valid + if (empty($relay_id)) { + throw new \Exception("Invalid relay ID for port {$port->name}"); + } + // Mark the lane state as opening the port + $new_state = match ($port) { + selfserve_lane_port::ENTRANCE => selfserve_lane_state::ENTRANCE_PORT_OPEN_QUEUED, + selfserve_lane_port::EXIT => selfserve_lane_state::EXIT_PORT_OPEN_QUEUED, + default => throw new \Exception("Invalid port specified, must be ENTRANCE or EXIT"), + }; + $this->setLaneState($new_state); + // TODO: Open the relay here + + return true; + } +} \ No newline at end of file diff --git a/services/nginx/app/modules/selfserve/traits/selfserve_lane_state_t.php b/services/nginx/app/modules/selfserve/traits/selfserve_lane_state_t.php new file mode 100644 index 00000000..146a9812 --- /dev/null +++ b/services/nginx/app/modules/selfserve/traits/selfserve_lane_state_t.php @@ -0,0 +1,48 @@ +getLaneCache($this->id, self::CACHE_SELFSERVE_LANE_KEY_STATE, selfserve_lane_state::class); + if (!empty($state)) { + $this->state = $state; + } + else if (empty($this->state)) { + // If not found in cache, default to MANUAL + $this->state = selfserve_lane_state::IDLE; + $this->setLaneCache($this->id, self::CACHE_SELFSERVE_LANE_KEY_STATE, $this->state); + } + return $this->state; + } + + /** + * Set the current operational state of the self-serve lane + * @param selfserve_lane_state $state The new operational state of the self-serve lane + * @return selfserve_lane_state_t + */ + public function setLaneState(selfserve_lane_state $state): self + { + $this->state = $state; + $this->setLaneCache($this->id, self::CACHE_SELFSERVE_LANE_KEY_STATE, $this->state); + return $this; + } +} \ No newline at end of file diff --git a/services/nginx/app/modules/selfserve/traits/selfserve_lane_status_t.php b/services/nginx/app/modules/selfserve/traits/selfserve_lane_status_t.php new file mode 100644 index 00000000..382afa7e --- /dev/null +++ b/services/nginx/app/modules/selfserve/traits/selfserve_lane_status_t.php @@ -0,0 +1,48 @@ +getLaneCache($this->id, self::CACHE_SELFSERVE_LANE_KEY_STATUS, selfserve_lane_status::class); + if (!empty($status)) { + $this->status = $status; + } + else if (empty($this->status)) { + // If not found in cache, default to AVAILABLE + $this->status = selfserve_lane_status::AVAILABLE; + $this->setLaneCache($this->id, self::CACHE_SELFSERVE_LANE_KEY_STATUS, $this->status); + } + return $this->status; + } + + /** + * Set the current status of the self-serve lane + * @param selfserve_lane_status $status The new status of the self-serve lane + * @return selfserve_lane_status_t|selfserve_lane + */ + public function setLaneStatus(selfserve_lane_status $status): self + { + $this->status = $status; + $this->setLaneCache($this->id, self::CACHE_SELFSERVE_LANE_KEY_STATUS, $this->status); + return $this; + } +} \ No newline at end of file diff --git a/services/nginx/app/routes/moduleSelfServeRoute.php b/services/nginx/app/routes/moduleSelfServeRoute.php new file mode 100644 index 00000000..42a187b3 --- /dev/null +++ b/services/nginx/app/routes/moduleSelfServeRoute.php @@ -0,0 +1,45 @@ + Self Serve > Lane > Status */ + $this->get('/modules/self-serve/lane/status', function () { + global $response; + //TODO: self::requirePermission('modules_selfserve_lane_status_view'); + $selfserve = new selfserve(); + $response->success([ + 'id' => $selfserve->lane(1)->id, + 'status' => $selfserve->lane(1)->getLaneStatus()->name, + 'mode' => $selfserve->lane(1)->getLaneMode()->name, + 'state' => $selfserve->lane(1)->getLaneState()->name, + ]); + }, + [ + 'modules_selfserve_lane_status_view' => 'View self-serve lane status', + ] + ); + } +} \ No newline at end of file diff --git a/test/orderBookingsPost.http b/test/orderBookingsPost.http new file mode 100644 index 00000000..d75f6472 --- /dev/null +++ b/test/orderBookingsPost.http @@ -0,0 +1,213 @@ +### POST request to /account/auth/phone +POST https://api.truckwash.dk:4433/account/auth/phone +Accept: application/json +Content-Type: application/json + +{ + "phone": 42331128, + "country_code": 45 +} + +### GET request to order bookings (list all) +GET https://api.truckwash.dk:4433/order-bookings +Accept: application/json +Content-Type: application/json +Authorization: Bearer e9d4359673de64f6a9bc4231bf50ac9f5726ad5b3ee6fcad5f8ebbb20647c3b8 + +### GET request to order bookings (specific ID) +GET https://api.truckwash.dk:4433/order-bookings?id=1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer e9d4359673de64f6a9bc4231bf50ac9f5726ad5b3ee6fcad5f8ebbb20647c3b8 + +### PUT request to order bookings (update specific ID) +PUT https://api.truckwash.dk:4433/order-bookings +Accept: application/json +Content-Type: application/json +Authorization: Bearer e9d4359673de64f6a9bc4231bf50ac9f5726ad5b3ee6fcad5f8ebbb20647c3b8 + +{ + "id": 4, + "note": "Updated note 3", + "reference": "Updated ref 3", + "reg_2": null +} + +### POST request to complete an order booking +POST https://api.truckwash.dk:4433/order-bookings/complete +Accept: application/json +Content-Type: application/json +Authorization: Bearer e9d4359673de64f6a9bc4231bf50ac9f5726ad5b3ee6fcad5f8ebbb20647c3b8 + +{ + "id": 3, + "safety_seal": 123456 +} + +### +### POST request to order bookings +POST https://api.truckwash.dk:4433/order-bookings +Accept: application/json, text/plain, */* +Accept-Language: en-GB,en;q=0.6 +Authorization: Bearer e9d4359673de64f6a9bc4231bf50ac9f5726ad5b3ee6fcad5f8ebbb20647c3b8 +Content-Type: application/json + +{ + "customer_number": 12345679, + "department": 2, + "reg_1": "EC21233", + "reg_2": null, + "reg_3": "Abc", + "datetime": "2025-11-11", + "note": "Note 123", + "reference": "Reference 123", + "po": "PO 123", + "pickup": true, + "items": [ + { + "id": 5, + "name": "Forvogn", + "description": " ", + "price": 649, + "subscription_allowed": true, + "category": 6, + "piktogram": "05", + "economic_product_id": 5, + "apply_category_discount": true, + "requires_note": false, + "created_at": "2024-12-09 14:31:48", + "updated_at": "2025-10-28 13:25:21", + "addons": [], + "is_wash": true, + "display_in_booking_form": true, + "order_priority": 10, + "quantity": 1 + }, + { + "id": 11, + "name": "Indvendig vask Forvogn", + "description": " ", + "price": 399, + "subscription_allowed": 0, + "category": "2", + "piktogram": "", + "economic_product_id": "11", + "apply_category_discount": true, + "requires_note": false, + "is_wash": true, + "display_in_booking_form": true, + "order_priority": 1022, + "created_at": "2024-12-09 14:31:49", + "updated_at": "2025-10-28 13:56:30", + "quantity": 1 + }, + { + "id": 21, + "name": "Undervognskyld pr. Enhed", + "description": " ", + "price": 79, + "subscription_allowed": 1, + "category": "4", + "piktogram": "", + "economic_product_id": "23", + "apply_category_discount": false, + "requires_note": false, + "is_wash": false, + "display_in_booking_form": true, + "order_priority": 1018, + "created_at": "2024-12-09 14:31:49", + "updated_at": "2025-10-28 13:54:41", + "quantity": 1 + }, + { + "id": 25, + "name": "Fælg Flex pr. Enhed", + "description": " ", + "price": 39, + "subscription_allowed": 0, + "category": "4", + "piktogram": "", + "economic_product_id": "40", + "apply_category_discount": false, + "requires_note": false, + "is_wash": false, + "display_in_booking_form": true, + "order_priority": 1019, + "created_at": "2024-12-09 14:31:50", + "updated_at": "2025-10-28 13:55:00", + "quantity": 2 + }, + { + "id": 24, + "name": "Spot Free- Lastbil", + "description": " ", + "price": 39, + "subscription_allowed": 1, + "category": "4", + "piktogram": "", + "economic_product_id": "33", + "apply_category_discount": false, + "requires_note": false, + "is_wash": false, + "display_in_booking_form": true, + "order_priority": 1017, + "created_at": "2024-12-09 14:31:49", + "updated_at": "2025-10-28 13:54:25", + "quantity": 3 + }, + { + "id": 22, + "name": "Double Duty - Kemi", + "description": " ", + "price": 239, + "subscription_allowed": 0, + "category": "4", + "piktogram": "", + "economic_product_id": "24", + "apply_category_discount": false, + "requires_note": true, + "is_wash": false, + "display_in_booking_form": true, + "order_priority": 1021, + "created_at": "2024-12-09 14:31:49", + "updated_at": "2025-10-28 13:55:58", + "quantity": 4 + }, + { + "id": 2, + "name": "Trailer", + "description": "", + "price": 599, + "subscription_allowed": 1, + "category": "6", + "piktogram": "02", + "economic_product_id": "2", + "apply_category_discount": true, + "requires_note": false, + "is_wash": true, + "display_in_booking_form": true, + "order_priority": 50, + "created_at": "2024-12-03 09:07:48", + "updated_at": "2025-10-28 13:25:52", + "quantity": 5 + }, + { + "id": 4, + "name": "Dolly", + "description": "", + "price": 275, + "subscription_allowed": 0, + "category": "6", + "piktogram": "04", + "economic_product_id": "4", + "apply_category_discount": true, + "requires_note": false, + "is_wash": true, + "display_in_booking_form": true, + "order_priority": 90, + "created_at": "2024-12-06 14:36:23", + "updated_at": "2025-10-28 13:26:24", + "quantity": 6 + } + ] +}