Merge pull request #92 from copenhagentruckwash/updater-v1

updater-v1
This commit is contained in:
Jeppe B
2026-01-05 09:22:40 +01:00
committed by GitHub
49 changed files with 1941 additions and 23 deletions
+18
View File
@@ -160,6 +160,24 @@ class response implements response_i
return $data[$key] ?? null;
}
/**
* Get all request parameters
* @return array
*/
public function getAllRequestParameters(): array
{
$data = [];
// Get the request data if the method is POST, PUT or PATCH
if ($_SERVER['REQUEST_METHOD'] === 'POST' || $_SERVER['REQUEST_METHOD'] === 'PUT' || $_SERVER['REQUEST_METHOD'] === 'PATCH') {
$data = json_decode(file_get_contents('php://input'), true);
}
// Get the request data if the method is GET, DELETE or OPTIONS
if ($_SERVER['REQUEST_METHOD'] === 'GET' || $_SERVER['REQUEST_METHOD'] === 'DELETE' || $_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
$data = $_GET;
}
return $data;
}
/**
* This function checks if a request parameter is set.
* This is different from getRequestParameter because this function allows for empty & null values.
+52
View File
@@ -0,0 +1,52 @@
<?php
namespace classes;
require_once WD . '/modules/selfserve/selfserve_c.php';
require_once WD . '/modules/selfserve/classes/selfserve_lane.php';
/**
* Actions
*/
use Exception;
use interfaces\universal_module_i;
use modules\selfserve\classes\selfserve_lane;
use modules\selfserve\selfserve_c;
class selfserve implements universal_module_i
{
/**
* The configuration of the module
* @var selfserve_c
*/
public selfserve_c $config;
public function __construct()
{
$this->config = new selfserve_c();
}
public static function getInstance(): self
{
return new self();
}
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');
}
}
}
+1
View File
@@ -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
@@ -4,6 +4,7 @@ namespace attachments\helpers;
class attachment_content
{
const OTHER_TYPE_WASH_CERTIFICATE = 'WASH_CERTIFICATE';
public ?string $image; // Used to store the attachment object name, in the attachment store.
public ?string $document; // Used to store the attachment object name, in the attachment store.
public ?attachment_relation $relation; // Used to store the attachment relation object.
@@ -0,0 +1,80 @@
<?php
namespace modules\selfserve\classes;
require_once WD . '/modules/selfserve/interfaces/selfserve_lane_i.php';
require_once WD . '/modules/selfserve/traits/selfserve_lane_cache_t.php';
require_once WD . '/modules/selfserve/traits/selfserve_lane_mode_t.php';
require_once WD . '/modules/selfserve/traits/selfserve_lane_state_t.php';
require_once WD . '/modules/selfserve/traits/selfserve_lane_status_t.php';
require_once WD . '/modules/selfserve/traits/selfserve_lane_port_controller_t.php';
require_once WD . '/modules/selfserve/traits/selfserve_lane_command_t.php';
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';
use modules\selfserve\interfaces\selfserve_lane_i;
use modules\selfserve\traits\selfserve_lane_cache_t;
use modules\selfserve\traits\selfserve_lane_command_t;
use modules\selfserve\traits\selfserve_lane_customer_number_t;
use modules\selfserve\traits\selfserve_lane_invoice_t;
use modules\selfserve\traits\selfserve_lane_license_plate_t;
use modules\selfserve\traits\selfserve_lane_mode_t;
use modules\selfserve\traits\selfserve_lane_port_controller_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;
class selfserve_lane implements selfserve_lane_i
{
use selfserve_lane_status_t,
selfserve_lane_mode_t,
selfserve_lane_state_t,
selfserve_lane_cache_t,
selfserve_lane_port_controller_t,
selfserve_lane_command_t,
selfserve_lane_timer_t,
selfserve_lane_license_plate_t,
selfserve_lane_customer_number_t,
selfserve_lane_invoice_t;
/**
* @throws \Exception
*/
public function __construct(int $lane_id)
{
$this->id = $lane_id;
$this->department_lane = (new \objects\department_lanes_o())->select($lane_id);
// Raise exception if department lane not found
if (!$this->department_lane->exists()) {
throw new \Exception("Department lane not found for lane ID {$lane_id}");
}
}
/**
* The lane ID
* @var int $id
* @see \objects\department_lanes_o
*/
public int $id;
/**
* Bypass customer number validation flag
* @var bool $bypass_customer_number_validation
*/
public bool $bypass_customer_number_validation = false;
/**
* Department lane object
* @var \objects\department_lanes_o|null $department_lane
*/
public ?\objects\department_lanes_o $department_lane = null;
public function isBypassCustomerNumberValidation(): bool
{
return $this->bypass_customer_number_validation;
}
public function setBypassCustomerNumberValidation(bool $bypass): void
{
$this->bypass_customer_number_validation = $bypass;
}
}
@@ -0,0 +1,39 @@
<?php
namespace modules\selfserve\classes;
class selfserve_lane_command_arguments
{
public ?string $license_plate = null;
public ?int $customer_number = null;
/**
* Set the license plate of the vehicle currently in the lane
* @param string|null $license_plate
* @return $this
*/
public function setLicensePlate(?string $license_plate): self
{
$this->license_plate = $license_plate ? strtoupper(trim($license_plate)) : null;
return $this;
}
public function setCustomerNumber(?int $customer_number): self
{
$this->customer_number = $customer_number;
return $this;
}
public function setParameters($params): self
{
if (is_array($params)) {
if (array_key_exists('license_plate', $params)) {
$this->setLicensePlate($params['license_plate']);
}
if (array_key_exists('customer_number', $params)) {
$this->setCustomerNumber($params['customer_number']);
}
}
return $this;
}
}
@@ -0,0 +1,29 @@
<?php
namespace modules\selfserve\config;
use Exception;
use traits\module_config_variable;
class selfserve_enabled_c
{
use module_config_variable;
/**
* @throws Exception
*/
public function __construct()
{
self::setupConfigVariable(
'selfserve',
'enabled',
'bool',
true,
null,
'Whether the selfserve module is enabled or not',
'1',
false,
'false'
);
}
}
@@ -0,0 +1,29 @@
<?php
namespace modules\selfserve\config;
use Exception;
use traits\module_config_variable;
class selfserve_minute_product_c
{
use module_config_variable;
/**
* @throws Exception
*/
public function __construct()
{
self::setupConfigVariable(
'selfserve',
'minute_product',
'int',
true,
null,
'The product ID used for minute-based self-serve billing',
'1',
false,
0
);
}
}
@@ -0,0 +1,25 @@
<?php
namespace modules\selfserve\helpers;
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
public static function tryFrom(string $commandParam): ?selfserve_lane_command
{
return match (strtoupper($commandParam)) {
'START' => selfserve_lane_command::START,
'STOP' => selfserve_lane_command::STOP,
'RESET' => selfserve_lane_command::RESET,
default => null,
};
}
public function equals(selfserve_lane_command $command): bool
{
return $this === $command;
}
}
@@ -0,0 +1,14 @@
<?php
namespace modules\selfserve\helpers;
enum selfserve_lane_mode
{
case MANUAL; // The wash is in manual mode (Machine is disabled)
case AUTOMATIC; // The wash is in automatic mode (Machine is enabled, provided the lane supports it)
public function equals(selfserve_lane_mode $mode): bool
{
return $this === $mode;
}
}
@@ -0,0 +1,10 @@
<?php
namespace modules\selfserve\helpers;
enum selfserve_lane_port
{
case ENTRANCE;
case EXIT;
}
@@ -0,0 +1,20 @@
<?php
namespace modules\selfserve\helpers;
enum selfserve_lane_state
{
case ENTRANCE_PORT_OPEN_QUEUED; // The wash is queued
case ENTRANCE_PORT_OPEN; // The entrance gate is open
case MACHINE_RELAY_ON_QUEUED; // The machine relay is queued to turn on. (applies to supported lanes)
case MACHINE_RELAY_ON; // The machine relay is on. (applies to supported lanes)
case MACHINE_RELAY_OFF_QUEUED; // The machine relay is queued to turn off. (applies to supported lanes)
case MACHINE_RELAY_OFF; // The machine relay is off. (applies to supported lanes)
case IN_WASH; // The vehicle is in the wash
case EXIT_PORT_OPEN_QUEUED; // The exit gate is queued
case EXIT_PORT_OPEN; // The exit gate is open
case IDLE; // The lane is idle
case FAULT; // The lane is in a fault state
case MAINTENANCE; // The lane is under maintenance
case CLOSED; // The lane is closed
}
@@ -0,0 +1,19 @@
<?php
namespace modules\selfserve\helpers;
enum selfserve_lane_status
{
case OCCUPIED; // the lane is occupied
case FAULT; // the lane is in a fault state
case AVAILABLE; // lane is available
case MAINTENANCE; // lane is under maintenance
case CLOSED; // the lane is closed
case RESERVED; // the lane is reserved
public function equals(selfserve_lane_status $status): bool
{
return $this === $status;
}
}
@@ -0,0 +1,8 @@
<?php
namespace modules\selfserve\interfaces;
interface selfserve_lane_i
{
}
@@ -0,0 +1,36 @@
<?php
namespace modules\selfserve;
require_once WD . '/modules/selfserve/config/selfserve_enabled_c.php';
require_once WD . '/modules/selfserve/config/selfserve_minute_product_c.php';
use modules\selfserve\config\selfserve_enabled_c;
use modules\selfserve\config\selfserve_minute_product_c;
use traits\module_config_t;
class selfserve_c
{
use module_config_t;
/**
* The status of the module
* @var selfserve_enabled_c $enabled
*/
public selfserve_enabled_c $enabled;
/**
* The product ID used for minute-based self-serve billing
* @var selfserve_minute_product_c $minute_product
*/
public selfserve_minute_product_c $minute_product;
public function __construct()
{
$this->setupConfig('selfserve');
$this->allowUpdate([
selfserve_enabled_c::class,
selfserve_minute_product_c::class
]);
$this->enabled = new selfserve_enabled_c();
$this->minute_product = new selfserve_minute_product_c();
}
}
@@ -0,0 +1,74 @@
<?php
namespace modules\selfserve\traits;
use modules\selfserve\classes\selfserve_lane;
use modules\selfserve\helpers\selfserve_lane_status;
trait selfserve_lane_cache_t
{
const CACHE_SELFSERVE_PREFIX = 'selfserve_lane_';
const CACHE_SELFSERVE_LANE_KEY_STATUS = self::CACHE_SELFSERVE_PREFIX . 'status';
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_CUSTOMER_NUMBER = self::CACHE_SELFSERVE_PREFIX . 'customer_number';
const CACHE_SELFSERVE_LANE_KEY_LICENSE_PLATE = self::CACHE_SELFSERVE_PREFIX . 'license_plate';
/**
* Get the cache key for the lane
* @param int $laneId The ID of the lane
* @param string $property The property to get the cache key for (self::CACHE_SELFSERVE_LANE_KEY_STATUS, self::CACHE_SELFSERVE_LANE_KEY_STATE, self::CACHE_SELFSERVE_LANE_KEY_MODE, ...)
* @return string The cache key for the lane
*/
protected function getLaneCacheKey(int $laneId, string $property): string
{
return $property . '_' . $laneId;
}
/**
* Set the cache for a lane property
* @param int $laneId The ID of the lane
* @param string $property The property to set the cache for (self::CACHE_SELFSERVE_LANE_KEY_STATUS, self::CACHE_SELFSERVE_LANE_KEY_STATE, self::CACHE_SELFSERVE_LANE_KEY_MODE, ...)
* @param mixed $value The value to set in the cache
* @return $this;
*/
public function setLaneCache(int $laneId, string $property, mixed $value): self
{
redis->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;
}
}
@@ -0,0 +1,85 @@
<?php
namespace modules\selfserve\traits;
require_once WD . '/modules/selfserve/helpers/selfserve_lane_status.php';
require_once WD . '/modules/selfserve/classes/selfserve_lane.php';
require_once WD . '/modules/selfserve/helpers/selfserve_lane_command.php';
require_once WD . '/modules/selfserve/helpers/selfserve_lane_mode.php';
require_once WD . '/modules/selfserve/helpers/selfserve_lane_port.php';
require_once WD . '/modules/selfserve/helpers/selfserve_lane_state.php';
require_once WD . '/modules/selfserve/classes/selfserve_lane_command_arguments.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_mode;
use modules\selfserve\helpers\selfserve_lane_port;
use modules\selfserve\helpers\selfserve_lane_state;
use modules\selfserve\helpers\selfserve_lane_status;
use objects\users_o;
trait selfserve_lane_command_t
{
/**
* Execute a command on a self-serve lane
* @param selfserve_lane_command $command The command to execute
* @param selfserve_lane_command_arguments $arguments The arguments for the command
* @return selfserve_lane|selfserve_lane_command_t
* @throws Exception If the command cannot be executed
*/
public function execute(selfserve_lane_command $command, selfserve_lane_command_arguments $arguments): self
{
// Handle the command
switch ($command) {
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.");
// Require the customer number
if (empty($customer_number = $arguments->customer_number)) throw new \InvalidArgumentException("Customer number is required to start the lane.");
// Require the license plate
if (empty($license_plate = $arguments->license_plate)) throw new \InvalidArgumentException("License plate is required to start 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 and license plate
$this->setCustomerNumber($customer_number);
$this->setLicensePlate($license_plate);
// Set the lane status to OCCUPIED when started
$this->setLaneStatus(selfserve_lane_status::OCCUPIED);
// Set the lane state to IN_WASH
$this->setLaneState(selfserve_lane_state::IN_WASH);
// Open the entrance port
$this->open(selfserve_lane_port::ENTRANCE);
// Start the wash timer
$this->setWashStartTime(time());
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.");
// 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);
}
// Invoice the customer
$this->invoice();
// Open the exit port
$this->open(selfserve_lane_port::EXIT);
// Reset the lane
self::execute(selfserve_lane_command::RESET, new selfserve_lane_command_arguments());
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);
$this->setWashStartTime(self::DEFAULT_WASH_START_TIME);
$this->setCustomerNumber(self::DEFAULT_CUSTOMER_NUMBER);
$this->setLicensePlate(self::DEFAULT_LICENSE_PLATE);
break;
default:
throw new \InvalidArgumentException("Unknown command: " . $command->name);
}
return $this;
}
}
@@ -0,0 +1,40 @@
<?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\helpers\selfserve_lane_status;
trait selfserve_lane_customer_number_t
{
const DEFAULT_CUSTOMER_NUMBER = null;
/**
* The billable customer number associated with the current wash
* @var int|null $customer_number
*/
public ?int $customer_number = null;
/**
* Get the customer number associated with the current wash
* @return int|null The customer number, or null if not set
*/
public function getCustomerNumber(): ?int
{
$customer_number = $this->getLaneCache($this->id, self::CACHE_SELFSERVE_LANE_KEY_CUSTOMER_NUMBER);
if (!empty($customer_number)) {
$this->customer_number = $customer_number;
}
return $this->customer_number;
}
/**
* Set the customer number associated with the current wash
* @param int|null $customer_number The customer number, or null to clear
* @return selfserve_lane_customer_number_t|selfserve_lane
*/
public function setCustomerNumber(?int $customer_number): self
{
$this->customer_number = $customer_number;
$this->setLaneCache($this->id, self::CACHE_SELFSERVE_LANE_KEY_CUSTOMER_NUMBER, $this->customer_number);
return $this;
}
}
@@ -0,0 +1,73 @@
<?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 classes\selfserve;
use Exception;
use modules\selfserve\classes\selfserve_lane;
use modules\selfserve\helpers\selfserve_lane_status;
use objects\order_items_o;
use objects\orders_o;
trait selfserve_lane_invoice_t
{
/**
* The product ID for minute-based billing
* @var int|null $minute_billing_product_id
*/
public ?int $minute_billing_product_id = null;
/**
* Get the minute billing product ID
* @return int|null The product ID for minute-based billing, or null if not set
*/
public function getMinuteBillingProductId(): ?int
{
$minute_billing_product_id = selfserve::getInstance()
->config
->minute_product
->getVariableValue();
if (!empty($minute_billing_product_id)) {
$this->minute_billing_product_id = $minute_billing_product_id;
}
return $this->minute_billing_product_id;
}
/**
* Invoice for minute-based billing
* @return bool True on success, false on failure
* @throws Exception if lane ID is not set, lane is not occupied, customer number or license plate is not set, or product ID is not set
*/
public function invoice(): bool
{
if (empty($this->id)) throw new \Exception("Lane ID is not set.");
if ($this->getLaneStatus() !== selfserve_lane_status::OCCUPIED) throw new \Exception("Lane ID {$this->id} is not occupied; cannot invoice.");
if (empty($this->getCustomerNumber())) throw new \Exception("Customer number is not set for lane ID {$this->id}.");
if (empty($this->getLicensePlate())) throw new \Exception("License plate is not set for lane ID {$this->id}.");
if (empty($product_id = $this->getMinuteBillingProductId())) throw new \Exception("Minute billing product ID is not set.");
// Calculate minutes used
$minutes = $this->getElapsedWashTime() / 60; // Convert seconds to minutes
$minutes = (int)ceil($minutes); // Round up to nearest whole minute
if ($minutes <= 0) throw new \Exception("No minutes to bill for lane ID {$this->id}.");
$amount = $minutes; // Assuming 1 unit per minute, adjust as needed
// Create invoice order
$order = (new orders_o())->add(
$this->getCustomerNumber(),
2285, // System User ID
'',
'',
(int)$this->department_lane->department->value(),
(string)$this->getLicensePlate()
);
// Add product to order
$order_items = new order_items_o();
$order_items->addItemToOrder(
$order->id,
$product_id,
2285, // System User ID
$amount,
);
return true;
}
}
@@ -0,0 +1,40 @@
<?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\helpers\selfserve_lane_status;
trait selfserve_lane_license_plate_t
{
const DEFAULT_LICENSE_PLATE = null;
/**
* The license plate of the vehicle currently in the lane
* @var string|null $license_plate
*/
public ?string $license_plate = null;
/**
* Get the license plate of the vehicle currently in the lane
* @return string|null The license plate, or null if not set
*/
public function getLicensePlate(): ?string
{
$license_plate = $this->getLaneCache($this->id, self::CACHE_SELFSERVE_LANE_KEY_LICENSE_PLATE);
if (!empty($license_plate)) {
$this->license_plate = $license_plate;
}
return $this->license_plate;
}
/**
* Set the license plate of the vehicle currently in the lane
* @param string|null $license_plate The license plate, or null to clear
* @return selfserve_lane_license_plate_t|selfserve_lane
*/
public function setLicensePlate(?string $license_plate): self
{
$this->license_plate = $license_plate;
$this->setLaneCache($this->id, self::CACHE_SELFSERVE_LANE_KEY_LICENSE_PLATE, $this->license_plate);
return $this;
}
}
@@ -0,0 +1,49 @@
<?php
namespace modules\selfserve\traits;
require_once WD . '/modules/selfserve/helpers/selfserve_lane_mode.php';
require_once WD . '/modules/selfserve/classes/selfserve_lane.php';
use modules\selfserve\classes\selfserve_lane;
use modules\selfserve\helpers\selfserve_lane_mode;
trait selfserve_lane_mode_t
{
/**
* @enum selfserve_lane_mode
* @description The mode of the lane, which can be one of the following values:
* @values MANUAL, AUTOMATIC
* @var selfserve_lane_mode $mode
*/
public selfserve_lane_mode $mode;
/**
* Get the current mode of the self-serve lane
* @return selfserve_lane_mode The current mode of the self-serve lane
*/
public function getLaneMode(): selfserve_lane_mode
{
$mode = $this->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;
}
}
@@ -0,0 +1,89 @@
<?php
namespace modules\selfserve\traits;
require_once WD . '/modules/selfserve/helpers/selfserve_lane_port.php';
require_once WD . '/modules/selfserve/classes/selfserve_lane.php';
use classes\shelly;
use modules\selfserve\helpers\selfserve_lane_port;
use modules\selfserve\helpers\selfserve_lane_state;
use modules\selfserve\helpers\selfserve_lane_status;
use modules\shelly\helpers\shelly_device_switch;
use modules\shelly\helpers\shelly_request_body_get_states;
trait selfserve_lane_port_controller_t
{
/**
* Open the lane port
* @param selfserve_lane_port $port The port to open (ENTRANCE or EXIT)
* @return bool True if the port was successfully opened, false otherwise
* @throws \Exception If an invalid port is specified, or if the lane is not in a state to open the port
*/
public function open(selfserve_lane_port $port): bool
{
// Require department lane object
if (empty($this->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);
// Open the relay
$this->shellyOpenPort($port);
return true;
}
/**
* Open the Shelly relay for the specified port
* @param selfserve_lane_port $port The port to open (ENTRANCE or EXIT)
* @return bool True if the relay was successfully opened
* @throws \Exception If an invalid port is specified or if the relay ID is invalid
*/
public function shellyOpenPort(selfserve_lane_port $port): bool
{
// 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}");
}
$shelly = new shelly();
$shelly->requireModuleEnabled();
$shelly->requireValidSecretKey();
$parameters = new shelly_request_body_get_states();
$parameters->ids = [$relay_id];
$parameters->select = ['status'];
$result = $shelly->sendPostRequest('/v2/devices/api/get', (array)$parameters);
// Wait 1 second
sleep(1);
// Format the result
$result = array_map(function ($device) {
return (new shelly_device_switch())->populate($device);
}, $result);
// Open the switch
foreach ($result as $device) {
$device->switch(true);
}
return true;
}
}
@@ -0,0 +1,48 @@
<?php
namespace modules\selfserve\traits;
require_once WD . '/modules/selfserve/helpers/selfserve_lane_state.php';
use modules\selfserve\helpers\selfserve_lane_state;
trait selfserve_lane_state_t
{
/**
* @enum selfserve_lane_state
* @description The operational state of the lane, which can be one of the following values:
* @values ENTRANCE_PORT_OPEN_QUEUED, ENTRANCE_PORT_OPEN, MACHINE_RELAY_ON_QUEUED, MACHINE_RELAY_ON, MACHINE_RELAY_OFF_QUEUED, MACHINE_RELAY_OFF, IN_WASH, EXIT_PORT_OPEN_QUEUED, EXIT_PORT_OPEN, IDLE, FAULT, MAINTENANCE, CLOSED
* @var selfserve_lane_state $state
*/
public selfserve_lane_state $state;
/**
* Get the current operational state of the self-serve lane
* @return selfserve_lane_state The current operational state of the self-serve lane
*/
public function getLaneState(): selfserve_lane_state
{
$state = $this->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;
}
}
@@ -0,0 +1,48 @@
<?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\helpers\selfserve_lane_status;
trait selfserve_lane_status_t
{
/**
* @enum selfserve_lane_status
* @description The lane availability status, which can be one of the following values:
* @values OCCUPIED, FAULT, AVAILABLE, MAINTENANCE, CLOSED, RESERVED
* @var selfserve_lane_status $status
*/
public selfserve_lane_status $status;
/**
* Get the current status of the self-serve lane
* @return selfserve_lane_status The current status of the self-serve lane
*/
public function getLaneStatus(): selfserve_lane_status
{
$status = $this->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;
}
}
@@ -0,0 +1,53 @@
<?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\helpers\selfserve_lane_status;
trait selfserve_lane_timer_t
{
const DEFAULT_WASH_START_TIME = null;
/**
* The timestamp of the start of the current wash cycle
* @var int|null $wash_start_time
*/
public ?int $wash_start_time = null;
/**
* Get the wash start time
* @return int|null The timestamp of the start of the current wash cycle, or null if not set
*/
public function getWashStartTime(): ?int
{
$wash_start_time = $this->getLaneCache($this->id, self::CACHE_SELFSERVE_LANE_KEY_WASH_START_TIME);
if (!empty($wash_start_time)) {
$this->wash_start_time = $wash_start_time;
}
return $this->wash_start_time;
}
/**
* Set the wash start time
* @param int|null $timestamp The timestamp of the start of the current wash cycle, or null to clear
* @return selfserve_lane_timer_t|selfserve_lane
*/
public function setWashStartTime(?int $timestamp): self
{
$this->wash_start_time = $timestamp;
$this->setLaneCache($this->id, self::CACHE_SELFSERVE_LANE_KEY_WASH_START_TIME, $this->wash_start_time);
return $this;
}
/**
* Get the elapsed time since the wash started
* @return int|null The elapsed time in seconds since the wash started, or null if wash has not started
*/
public function getElapsedWashTime(): ?int
{
$wash_start_time = $this->getWashStartTime();
if ($wash_start_time === null) {
return null; // Wash has not started
}
return time() - $wash_start_time;
}
}
+150
View File
@@ -0,0 +1,150 @@
<?php
namespace objects;
use classes\db;
use classes\object_property;
use Exception;
use traits\db_object_t;
class accounts_o extends db
{
use db_object_t;
/**
* The phone number
* @var object_property $phone The phone number
*/
public object_property $phone;
/**
* The country code (e.g. 31 for +31)
* @var object_property $country_code The country code (e.g. 31 for +31)
*/
public object_property $country_code;
/**
* The account name
* @var object_property $name The account name
*/
public object_property $name;
/**
* The profile picture property
* @var object_property $profile_picture The profile picture property
*/
public object_property $profile_picture;
/**
* The enabled property
* @var object_property $enabled The enabled property
*/
public object_property $enabled;
/**
* The (last) seen at date
* @var object_property $seen_at The seen at date
*/
public object_property $seen_at;
/**
* The created at date
* @var object_property $created_at The created at date
*/
public object_property $created_at;
/**
* The updated at date
* @var object_property $updated_at The updated at date
*/
public object_property $updated_at;
/**
* The deleted at date
* @var object_property $deleted_at The deleted at date
*/
public object_property $deleted_at;
public function structure(): void
{
$this->setTable('accounts');
}
/**
* Add a new account
* @param array $data The account data
* @return int The account id
* @throws Exception If the account was not created successfully
*/
public function add(array $data): int
{
global /** @var db $db */
$db;
// Set the default values
$data_default = [
'phone' => 0,
'country_code' => 45,
'name' => '',
'profile_picture' => null,
];
// Merge the default values with the data
$data = array_merge($data_default, $data);
// Sanitize the input
$data['phone'] = (int)$db->escape_string($data['phone']);
$data['country_code'] = (int)$data['country_code'];
$data['name'] = (string)$db->escape_string($data['name']);
$data['profile_picture'] = $data['profile_picture'] ? (int)$data['profile_picture'] : null;
// Add the object
$tmp_id = self::add_object($data);
if (!$tmp_id) {
throw new Exception('The account was not created successfully.');
}
return $tmp_id;
}
public function getObjectProperties(): void
{
$this->phone = new object_property($this->table, $this->id, 'phone', 'int', false);
$this->country_code = new object_property($this->table, $this->id, 'country_code', 'int', false);
$this->name = new object_property($this->table, $this->id, 'name', 'string', false);
$this->profile_picture = new object_property($this->table, $this->id, 'profile_picture', 'int', false);
$this->enabled = new object_property($this->table, $this->id, 'enabled', 'bool', false);
$this->seen_at = new object_property($this->table, $this->id, 'seen_at', 'datetime', false);
$this->created_at = new object_property($this->table, $this->id, 'created_at', 'datetime', false);
$this->updated_at = new object_property($this->table, $this->id, 'updated_at', 'datetime', false);
$this->deleted_at = new object_property($this->table, $this->id, 'deleted_at', 'datetime', false);
}
/**
* Invalidate the cached object when it is changed
* @throws Exception If no object is selected
*/
public function objectChanged(): void
{
self::requireSelected();
// Invalidate the cache
self::deleteCached(self::$asArrayCacheKey);
}
/**
* Get the object as an array
* @return array The object as an array
* @throws Exception If no object is selected
*/
public function asArray(): array
{
self::requireSelected();
$cached = self::getCached(self::$asArrayCacheKey);
// If the object is cached, return it
if (!empty($cached)) return $cached;
// Convert the object to an array
$array = [
'id' => (int)$this->id,
'phone' => (int)$this->phone->value(),
'country_code' => (int)$this->country_code->value(),
'name' => (string)$this->name->value(),
'profile_picture' => (int)$this->profile_picture->value(),
'enabled' => (bool)$this->enabled->value(),
'seen_at' => $this->seen_at->value(),
'created_at' => $this->created_at->value(),
'updated_at' => $this->updated_at->value(),
//'deleted_at' => $this->deleted_at->value(),
];
// Cache the object
self::cache(self::$asArrayCacheKey, $array);
self::setCachedExpiration(self::$asArrayCacheKey, self::$asArrayCacheExpiration);
// Return the object as an array
return $array;
}
}
@@ -503,6 +503,11 @@ class customer_vehicles_o extends db
// Convert the array of transaction ids to a comma separated string
$orders = '';
foreach ($transaction_ids as $transaction_id) {
// Check if the transaction is included in the invoicing.
$tmp = (new orders_o())->select((int)$transaction_id);
if (!$tmp->isIncludedInInvoicing()) {
continue; // The transaction is not included in the invoicing, skip it
}
$orders .= (int)$transaction_id . ',';
}
// Remove the last comma
@@ -4,6 +4,7 @@ namespace objects;
use classes\db;
use classes\object_property;
use classes\selfserve;
use Exception;
use traits\db_object_t;
@@ -15,6 +16,7 @@ class department_lanes_o extends db
public object_property $name; // The lane name
public object_property $relay_in_id; // The Shelly relay for the entrance port (if applicable)
public object_property $relay_out_id; // The Shelly relay for the exit port (if applicable)
public object_property $relay_machine_id; // The Shelly relay for the machine (if applicable)
public object_property $created_at;
public object_property $updated_at;
public object_property $deleted_at;
@@ -25,16 +27,24 @@ class department_lanes_o extends db
$this->setTable('department_lanes');
}
public function getLaneStatus(): \modules\selfserve\helpers\selfserve_lane_status
{
$selfserve = new selfserve();
$lane = $selfserve->lane((int)$this->id);
return $lane->getLaneStatus();
}
/**
* Add a department lane
* @param int $department The department id
* @param string $name The lane name
* @param string|null $relay_in_id The Shelly relay for the entrance port (if applicable)
* @param string|null $relay_out_id The Shelly relay for the exit port (if applicable)
* @param string|null $relay_machine_id The Shelly relay for the machine (if applicable)
* @return department_lanes_o
* @throws Exception If the object was not created successfully
*/
public function add(int $department, string $name, string $relay_in_id = null, string $relay_out_id = null): self
public function add(int $department, string $name, string $relay_in_id = null, string $relay_out_id = null, string $relay_machine_id = null): department_lanes_o
{
global /** @var db $db */
$db;
@@ -47,12 +57,16 @@ class department_lanes_o extends db
if (!is_null($relay_out_id)) {
$relay_out_id = $db->escape_string($relay_out_id);
}
if (!is_null($relay_machine_id)) {
$relay_machine_id = $db->escape_string($relay_machine_id);
}
// Add the object
$tmp_id = self::add_object([
'department' => $department,
'name' => $name,
...(!is_null($relay_in_id) ? ['relay_in_id' => $relay_in_id] : []), // If the relay_in_id is null, it will be set to null in the database
...(!is_null($relay_out_id) ? ['relay_out_id' => $relay_out_id] : []), // If the relay_out_id is null, it will be set to null in the database
...(!is_null($relay_machine_id) ? ['relay_machine_id' => $relay_machine_id] : []), // If the relay_machine_id is null, it will be set to null in the database
]);
$this->id = $tmp_id;
self::getObjectProperties();
@@ -66,6 +80,7 @@ class department_lanes_o extends db
$this->name = new object_property($this->table, $this->id, 'name', 'string', false);
$this->relay_in_id = new object_property($this->table, $this->id, 'relay_in_id', 'string', false);
$this->relay_out_id = new object_property($this->table, $this->id, 'relay_out_id', 'string', false);
$this->relay_machine_id = new object_property($this->table, $this->id, 'relay_machine_id', 'string', false);
$this->created_at = new object_property($this->table, $this->id, 'created_at', 'timestamp', false);
$this->updated_at = new object_property($this->table, $this->id, 'updated_at', 'timestamp', false);
$this->deleted_at = new object_property($this->table, $this->id, 'deleted_at', 'timestamp', false);
@@ -84,8 +99,31 @@ class department_lanes_o extends db
'name' => (string)$this->name->value(),
'relay_in_id' => (string)$this->relay_in_id->value(),
'relay_out_id' => (string)$this->relay_out_id->value(),
'relay_machine_id' => (string)$this->relay_machine_id->value(),
'created_at' => (string)$this->created_at->value(),
'updated_at' => (string)$this->updated_at->value(),
];
}
/**
* @throws Exception
* @return department_lanes_o[] An array of department lane objects for the specified department
*/
public function getDepartmentLanes(int $department_id): array
{
$lanes = [];
$department_lanes = self::getFieldsWhere(
[
'department' => $department_id,
'deleted_at' => null
],
[
'id',
],
);
foreach ( $department_lanes as $department_lane ) {
$lanes[] = (new department_lanes_o())->select((int)$department_lane['id']);
}
return $lanes;
}
}
@@ -36,6 +36,17 @@ class departments_o extends db
redis->clear_departments();
}
/**
* Get the lanes associated with the department
* @retuns department_lanes_o[] The lanes associated with the department
* @throws Exception If the department is not selected
*/
public function getLanes(): array
{
self::requireSelected();
return (new department_lanes_o())->getDepartmentLanes($this->id);
}
/**
* Add a department
* @param string $name
@@ -119,6 +119,39 @@ class order_bookings_o extends db
$customer = (new users_o())->getUserByCustomerNumber((int)$this->customer_number->value());
$department_array = $department->asArray();
$customer_array = $customer->asArray();
$message = "";
$message .= "Ny booking fra {$customer->getCustomerName($customer_array['customer_number'])} (Kundenr: {$customer_array['customer_number']}, Bookingnr: {$this->id})\n";
$message .= "Dato: " . date('d-m-Y H:i', strtotime($this->datetime->value())) . "\n";
$message .= "Køretøj: " . $this->reg_1->value() . (!empty($this->reg_2->value()) ? ", " . $this->reg_2->value() : "") . (
!empty($this->reg_3->value()) ? ", " . $this->reg_3->value() : ""
) ."\n";
// If the booking has a note, add it to the message
if (!empty($this->note->value())) $message .= "Note: " . $this->note->value() . "\n";
// If the booking has items, add them to the message
if (!empty($this->items->value())) {
$items = array_map(function ($item) {
// Get the product name
$product = (new products_o())->select((int)$item['id']);
if (!$product->exists()) {
return null;
}
$item['name'] = $product->name->value();
if (!isset($item['quantity'])) {
$item['quantity'] = 1;
}
return "- " . $item['quantity'] . " x " . $item['name'];
}, $this->items->value());
$message .= "Ydelser:\n" . implode("\n", array_filter($items)) . "\n";
}
// If the booking has a reference, add it to the message
if (!empty($this->reference->value())) $message .= "Reference: " . $this->reference->value() . "\n";
// If the booking has a PO, add it to the message
if (!empty($this->po->value())) $message .= "PO: " . $this->po->value() . "\n";
if ($this->pickup->value()) $message .= "Afhentning: Ja\n"; else $message .= "Afhentning: Nej\n";
// If the booking has a note, add it to the message
if (!empty($this->note->value())) $message .= "Note: " . $this->note->value() . "\n";
// Add the branding name
$message = "*" . $branding->name->value() . "*\n" . $message;
/**
* Department booking notification
*/
@@ -169,7 +202,8 @@ class order_bookings_o extends db
// Add all the phone numbers from the department
...$department->notificationSmsPhoneNumbers()
],
'New booking from ' . $customer->getCustomerName($customer_array['customer_number']) . ' (' . $this->id . ')',
$message,
true
);
} catch (Exception $e) {
// Log the error
+100
View File
@@ -2,10 +2,14 @@
namespace objects;
use attachments\helpers\attachment_content;
use classes\db;
use classes\email;
use classes\pdf_generator;
use classes\motorapi;
use classes\object_property;
use classes\response;
use DateTime;
use Exception;
use helpers\xlvask_usage_log;
use helpers\xlvask_wash_item;
@@ -1376,6 +1380,86 @@ class orders_o extends db
return false; // No wash certificate product found in the order items
}
/**
* Generate and attach a wash certificate directly on an order (without a booking)
* @param int|null $safety_seal Optional safety seal number
* @param string|null $operator Optional operator/employee name who carried out the wash
* @param string|DateTime|null $date Optional date of the wash (defaults to current date)
* @throws Exception If the order is not selected or required related objects are missing
*/
public function generateWashCertificate(int|null $safety_seal = null, string|null $operator = null, $date = null): void
{
self::requireSelected();
// Avoid generating duplicate certificates
if ($this->hasWashCertificateAttached()) {
return;
}
// Get the department for the order
$department = (new departments_o())->select((int)$this->department_id->value());
if (!$department->exists()) {
throw new Exception('Department not found');
}
// Get branding for the department
$branding = (new branding_o())->select((int)$department->branding->value());
if (!$branding->exists()) {
throw new Exception('Branding not found');
}
// Get the customer for the order
$customer = (new users_o())->getUserByCustomerNumber((int)$this->customer_id->value());
$department_array = $department->asArray();
$department_array['branding'] = $branding->asArray();
$customer_array = $customer->asArray();
$order_array = $this->asArray();
// Format the date as 17:35 02-12-2025
$date = ($date instanceof DateTime ? $date : ($date !== null ? new DateTime($date) : new DateTime()));
$date_formatted = ($date instanceof DateTime ? $date->format('d-m-Y') : date('d-m-Y'));
$time_formatted = ($date instanceof DateTime ? $date->format('H:i') : date('H:i'));
// Generate PDF
$pdf_generator = new pdf_generator();
$pdf_generator->add_html(
$pdf_generator->templates->getTemplate('wash_certificate')
->setCompany([
'name' => 'Truck Wash',
'address' => 'Letland Allé 2',
'zip' => 2630,
'city' => 'Taastrup',
'phone_prefix' => 45,
'phone' => 43717886,
'email' => 'cph@truckwash.dk',
'website' => 'www.truckwash.dk',
'images' => [
'logo' => '/truckwash-banner-png.png',
'banner' => '/truckwash-banner-png.png',
'signature' => '/truckwash-underskrift.png',
],
])
->addData([
'booking_number' => $this->id, // Used as document number on the template
'seal_number' => ($safety_seal ?? null),
'reg_1' => $order_array['reg_1'],
'reg_2' => $order_array['reg_2'],
'date' => $date_formatted,
'time' => $time_formatted,
'carried_out_by' => ($operator ?? null),
'department_id' => $department->id,
'department_name' => $department_array['branding']['name'] ?: $department_array['name'],
'department_address' => $department_array['branding']['address'] ?: $department_array['description'],
'customer_name' => $customer->getCustomerName($customer_array['customer_number']),
'customer_address' => $customer->getCustomerEcocomicData($customer_array['customer_number'])->economic_customer->address ?: '-',
'wash_type' => 'ORDER_WASH'
])
->getHtml()
);
$pdf_path = $pdf_generator->generate_pdf();
// Attach PDF to the order
$this->addAttachment(new attachment_content((object)[
'document' => $pdf_path,
'other' => attachment_content::OTHER_TYPE_WASH_CERTIFICATE,
]));
}
/**
* @throws Exception
*/
@@ -1414,4 +1498,20 @@ class orders_o extends db
self::requireSelected();
return !(new departments_o())->select((int)$this->department_id->value())->isExcludedFromInvoicing();
}
/**
* If the order is linked to a booking, get the booking object
* @return order_bookings_o|null The booking object if linked, null otherwise
* @throws Exception
*/
public function getOrderBooking(): order_bookings_o|null
{
self::requireSelected();
if ((int)$this->booking_id->value() <= 0) {
return null;
}
$booking = new order_bookings_o();
$booking->select((int)$this->booking_id->value());
return $booking->exists() ? $booking : null;
}
}
+11 -2
View File
@@ -12,6 +12,7 @@ class plate_scans_o extends db
public object_property $plate_scanner_id;
public object_property $plate;
public object_property $bay_id;
public function structure(): void
{
@@ -40,14 +41,19 @@ class plate_scans_o extends db
{
$this->plate_scanner_id = new object_property($this->table, $this->id, 'plate_scanner_id', 'int');
$this->plate = new object_property($this->table, $this->id, 'plate', 'string');
$this->bay_id = new object_property($this->table, $this->id, 'bay_id', 'string');
}
public function add(int $plate_scanner_id, string $plate): void
public function add(int $plate_scanner_id, string $plate, string $bayId = null): void
{
global $db, $response;
try {
// Avoid SQL injection
$plate = $db->escape_string($plate);
// If the bayId is set, validate it
if ($bayId !== null) {
$bayId = $db->escape_string($bayId);
}
// Get the department id from the plate scanner id
$sql = "SELECT department_id FROM plate_scanners WHERE id = $plate_scanner_id";
$result = $db->query($sql);
@@ -59,9 +65,12 @@ class plate_scans_o extends db
// Get the id of the new record
$this->id = $db->insert_id();
// Set the values of the object properties
$this->getObjectProperties();
// Set the bayId if provided
if ($bayId !== null) {
$this->bay_id->set((string)$bayId);
}
} catch (\Exception $e) {
$response->error($e->getMessage());
}
@@ -78,7 +78,7 @@ class InvoicingPeriodRoute
$this->get('/superuser/invoicing/period/distribution/fixed-pricing', function () {
// Require the user to be logged in
global $response;
//$this->requirePermission('superuser_invoicing_period');
$this->requirePermission('superuser_invoicing_period');
self::requireParameters([
'dateFrom',
'dateTo',
@@ -100,7 +100,7 @@ class InvoicingPeriodRoute
$this->get('/superuser/invoicing/period/distribution/wash-subscriptions', function () {
// Require the user to be logged in
global $response;
//$this->requirePermission('superuser_invoicing_period');
$this->requirePermission('superuser_invoicing_period');
self::requireParameters([
'dateFrom',
'dateTo',
@@ -291,6 +291,13 @@ class InvoicingPeriodRoute
}
$collective_results['subscription_price_department_distribution'][$department_id] += $price;
}
// Verify that the sum of the department distribution equals the total subscription price for the customer
$sum_of_distribution = array_sum($customer['meta']['subscription']['subscription_price_department_distribution']);
$difference = $customer['meta']['subscription']['subscription_total'] - $sum_of_distribution;
if (abs($difference) > 0.01) {
// Throw an error
throw new Exception('Subscription price distribution does not equal total subscription price for customer ' . $customer['customer_number'] . '. Difference: ' . $difference);
}
}
}
// Parse the department ids to department names
@@ -498,6 +505,10 @@ class InvoicingPeriodRoute
$department_totals = []; // Array to hold totals per department
foreach ( $customer['transactions'] as $transaction ) {
$transaction_obj = (new orders_o())->select((int)$transaction['id']);
// Check if the transaction is included in invoicing
if (!$transaction_obj->isIncludedInInvoicing()) {
continue; // Skip transactions not included in invoicing
}
$transaction['original_price'] = $transaction_obj->getNetAmountForOrderItemsOriginal();
$original_price += $transaction['original_price'];
// Add the transaction department id to the transaction
@@ -35,6 +35,7 @@ class departmentLanesRoute
'department',
'relay_in_id',
'relay_out_id',
'relay_machine_id',
])
->listObjectsWithPaginationIfSet(
function ($department_lane) use ($user) {
@@ -45,6 +46,7 @@ class departmentLanesRoute
'name' => (string)$department_lane['name'],
'relay_in_id' => (string)$department_lane['relay_in_id'],
'relay_out_id' => (string)$department_lane['relay_out_id'],
'relay_machine_id' => (string)$department_lane['relay_machine_id'],
'created_at' => (string)$department_lane['created_at'],
'updated_at' => (string)$department_lane['updated_at'],
];
@@ -79,11 +81,12 @@ class departmentLanesRoute
$department = $response->getRequestParameter('department') ?? null;
$relay_in_id = $response->getRequestParameter('relay_in_id') ?? null;
$relay_out_id = $response->getRequestParameter('relay_out_id') ?? null;
$relay_machine_id = $response->getRequestParameter('relay_machine_id') ?? null;
// Remove spaces from the relay_in_id and relay_out_id
// Check if the required fields are set
if ($name && $department) {
// Add the department lane
(new department_lanes_o())->add((int)$department, (string)$name, $relay_in_id, $relay_out_id);
(new department_lanes_o())->add((int)$department, (string)$name, $relay_in_id, $relay_out_id, $relay_machine_id);
// Return a success message
$response->success('Department lane added');
} else {
@@ -120,6 +123,7 @@ class departmentLanesRoute
$department = $response->getRequestParameter('department') ?? null;
$relay_in_id = $response->getRequestParameter('relay_in_id') ?? null;
$relay_out_id = $response->getRequestParameter('relay_out_id') ?? null;
$relay_machine_id = $response->getRequestParameter('relay_machine_id') ?? null;
// Check what fields are set
self::requireParameters(['id']);
@@ -143,6 +147,9 @@ class departmentLanesRoute
if (self::isParametersSet(['relay_out_id'])) {
$department_lane->relay_out_id->set((string)$relay_out_id);
}
if (self::isParametersSet(['relay_machine_id'])) {
$department_lane->relay_machine_id->set((string)$relay_machine_id);
}
// Return a success message
$response->success('Department lane updated');
} else {
+33 -3
View File
@@ -5,6 +5,7 @@ namespace routes;
use classes\authentication;
use classes\recaptcha;
use classes\virkdata;
use objects\department_lanes_o;
use objects\departments_o;
use objects\logs_o;
use objects\tokens_o;
@@ -52,13 +53,42 @@ class guestRoute
$this->get('/guest/departments', function () {
global $response;
$departments = (new departments_o());
$response->success($departments->listObjectsWithPaginationIfSet(function ($department_array) {
// Check if the department lane status is requested to be included
$include_lane_status = false;
if (self::isParametersSet(['include_lanes']) && self::getParameter('include_lanes')) {
$include_lane_status = true;
}
$response->success($departments->listObjectsWithPaginationIfSet(function ($department_array) use ($departments, $include_lane_status) {
$additional_data = [];
// If including lane status, fetch it
if ($include_lane_status) {
$department = $departments->select((int)$department_array['id']);
$additional_data['lanes'] = array_map(
/**
* @param department_lanes_o $lane
* @return array
*/
function (department_lanes_o $lane) {
return [
'id' => (int)$lane->id,
'name' => (string)$lane->name->value(),
'status' => (string)$lane->getLaneStatus()->name,
];
}, $department->getLanes());
}
// Only return id and name
return [
'id' => $department_array['id'],
'name' => $department_array['name']
'name' => $department_array['name'],
'longitude' => $department_array['longitude'],
'latitude' => $department_array['latitude'],
'address' => $department_array['description'],
...$additional_data
];
}), 200);
},
$departments->forceRestrictFilters([
'visible' => 1, // Only show visible departments, this is to prevent showing internal system departments to the end-user.
])), 200);
});
}
}
+15 -7
View File
@@ -5,6 +5,7 @@ namespace routes;
use classes\authentication;
use classes\economic;
use classes\invoice_store;
use objects\collected_order_invoices_o;
use objects\economic_module_orders;
use objects\logs_o;
use objects\orders_o;
@@ -104,18 +105,25 @@ class invoicesRoute
if (!$this->fromRequest('id')) {
$response->error('id parameter is required', 400);
}
$order_id = (new orders_o())->getOrderByInvoiceId($this->fromRequest('id'))->id;
// Check if the user has permission to view the invoice
if (!$user->hasAccessToOrder($order_id)) {
$response->error('User does not have access to this invoice', 403);
// Get the collected order invoice object
$collected_order_invoice = (new collected_order_invoices_o())->select((int)$this->fromRequest('id'));
// Make sure the user owns the invoice
if ((int)$collected_order_invoice->customer_number->value() !== (int)$user->customer_number->value()) {
$response->error('You do not have permission to access this invoice', 403);
}
// Check if the invoice is booked
$is_booked = $collected_order_invoice->isBooked();
if (!$is_booked) {
$response->error('Invoice is not booked yet', 400);
}
$booked_invoice_id = (int)$collected_order_invoice->booked_invoice_id->value();
// Create economic object
$economic = new economic();
// Get the draft invoice
$invoicePathFile = $economic->invoices->pdf->get($this->fromRequest('id'));
$invoicePathFile = $economic->invoices->pdf->get($booked_invoice_id);
// Add the pdf to the invoice store
$invoice_store = new invoice_store();
$invoice_store->uploadFile('invoice_' . $this->fromRequest('id') . '.pdf', $invoicePathFile);
$invoice_store->uploadFile('invoice_' . $booked_invoice_id . '.pdf', $invoicePathFile);
// Log the incident
(new logs_o())->add('invoices', 'global', 1, $user->id, 'GET_INVOICE_PDF', 'Successfully retrieved invoice pdf');
@@ -123,7 +131,7 @@ class invoicesRoute
unlink($invoicePathFile);
// Return the download link
$response->success(
['message' => 'Invoice PDF retrieved', 'url' => $invoice_store->getInvoiceDownloadUrl($this->fromRequest('id'))]
['message' => 'Invoice PDF retrieved', 'url' => $invoice_store->getInvoiceDownloadUrl($booked_invoice_id)]
);
} else {
// Log the incident
@@ -669,5 +669,36 @@ class moduleConfigRoute
(new logs_o())->add('shelly_config', 'global', 1, 0, 'SHELLY_CONFIG', 'No user found, or invalid session');
}
});
/** Self-Serve config -> GET */
$this->get('/selfserve/config', function () {
global $response;
$this->requirePermission('modules_selfserve_config');
$user = (new authentication())->get_user();
if ($user) {
(new logs_o())->add('selfserve_config', 'global', 1, $user->id, 'SELFSERVE_CONFIG', 'Successfully fetched self-serve config');
$response->success(
(new \classes\selfserve())->config->getConfigRequest()
);
} else {
(new logs_o())->add('selfserve_config', 'global', 1, 0, 'SELFSERVE_CONFIG', 'No user found, or invalid session');
}
},
[
'modules_selfserve_config' => 'Get self-serve config'
]
);
$this->post('/selfserve/config', function () {
global $response;
$this->requirePermission('modules_selfserve_config');
$user = (new authentication())->get_user();
if ($user) {
(new logs_o())->add('selfserve_config', 'global', 1, $user->id, 'SELFSERVE_CONFIG', 'Successfully updated self-serve config');
$response->success(
(new \classes\selfserve())->config->postConfigRequest()
);
} else {
(new logs_o())->add('selfserve_config', 'global', 1, 0, 'SELFSERVE_CONFIG', 'No user found, or invalid session');
}
});
}
}
@@ -0,0 +1,119 @@
<?php
namespace routes;
use classes\authentication;
use classes\email;
use classes\response;
use classes\router;
use classes\selfserve;
use classes\stripe;
use objects\departments_o;
use objects\logs_o;
use objects\orders_o;
use objects\stripe_module_customers_o;
use traits\route_t;
class moduleSelfServeRoute
{
use route_t;
public function run(): void
{
global /** @var response $response */
/** @var router $router */
$router, $response;
/** Modules > Self Serve > Lane > Status */
$this->get('/modules/self-serve/lane/status', function () {
global $response;
self::requirePermission('modules_selfserve_lane_status_view');
$selfserve = new selfserve();
$lane = $selfserve->lane(1);
$response->success([
'id' => $lane->id,
'status' => $lane->getLaneStatus()->name,
'mode' => $lane->getLaneMode()->name,
'state' => $lane->getLaneState()->name,
'wash_start_time' => $lane->getWashStartTime(),
'elapsed_wash_time' => $lane->getElapsedWashTime(),
'license_plate' => $lane->getLicensePlate(),
'customer_number' => $lane->getCustomerNumber(),
]);
},
[
'modules_selfserve_lane_status_view' => 'View self-serve lane status',
]
);
/** Modules > Self Serve > Lane > Command */
$this->post('/modules/self-serve/lane/command', function () {
global $response;
self::requirePermission('modules_selfserve_lane_command_execute');
$selfserve = new selfserve();
// Get the request user
$user = (new authentication())->get_user();
$param_lane_id = 'lane_id';
$param_command = 'command';
// Validate parameters
self::requireParameters([$param_lane_id, $param_command]);
$lane_id = (int)$this->getParameter($param_lane_id);
self::requireType($lane_id, self::type_int());
self::requireMinValue($lane_id, 1);
$commandParam = (string)$this->getParameter($param_command);
self::requireType($commandParam, self::type_string());
// Get the lane and command
$lane = $selfserve->lane($lane_id);
// If the user has the bypass permission, set the lane to bypass customer number validation
if (self::hasPermission('modules_selfserve_lane_command_bypass_customer_number_validation')) {
$lane->setBypassCustomerNumberValidation(true);
}
$command = \modules\selfserve\helpers\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:
self::requirePermission('modules_selfserve_lane_command_execute_start');
break;
case \modules\selfserve\helpers\selfserve_lane_command::STOP:
self::requirePermission('modules_selfserve_lane_command_execute_stop');
break;
case \modules\selfserve\helpers\selfserve_lane_command::RESET:
self::requirePermission('modules_selfserve_lane_command_execute_reset');
break;
}
// Execute the command
try {
$args = new \modules\selfserve\classes\selfserve_lane_command_arguments();
$args->setParameters([
...$this->getParametersAsArray(), // Pass all parameters
'customer_number' => (int)$user->customer_number->value(), // Get customer number from request user
]);
$lane->execute($command, $args);
$response->success([
'id' => $lane->id,
'status' => $lane->getLaneStatus()->name,
'mode' => $lane->getLaneMode()->name,
'state' => $lane->getLaneState()->name,
'wash_start_time' => $lane->getWashStartTime(),
'elapsed_wash_time' => $lane->getElapsedWashTime(),
'license_plate' => $lane->getLicensePlate(),
'customer_number' => $lane->getCustomerNumber(),
]);
} catch (\Exception $e) {
$response->error("Failed to execute command: " . $e->getMessage());
}
},
[
'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_reset' => 'Execute self-serve lane RESET command',
'modules_selfserve_lane_command_bypass_customer_number_validation' => 'Bypass customer number validation when executing commands',
]
);
}
}
+62
View File
@@ -54,6 +54,68 @@ class orderRoute
]
);
$this->post('/order/wash-certificate', function () {
// Require the user to be logged in and have permission
global /** @var response $response */
$response;
$this->requirePermission('complete_bookings');
// Get the user
$user = (new authentication())->get_user();
if (!$user) {
(new logs_o())->add('orders', 'global', 1, 0, 'GENERATE_ORDER_WASH_CERTIFICATE', 'No user found, or invalid session');
$response->error('Invalid session', 400);
}
// Parse request body
$data = json_decode(file_get_contents('php://input'), true) ?? [];
if (!isset($data['id']) || !(int)$data['id']) {
$response->error('Order id is required', 400);
}
$orders_o = new orders_o();
$orders_o->select((int)$data['id']);
if (!$orders_o->exists()) {
$response->error('Order not found', 400);
}
// Check department access
self::requireDepartmentAccess($orders_o->department_id->value());
// Collect optional params
$safety_seal = null;
if (isset($data['safety_seal']) && $data['safety_seal'] !== '') {
$safety_seal = (int)$data['safety_seal'];
}
$operator = isset($data['operator']) && $data['operator'] !== '' ? (string)$data['operator'] : (string)$user->display_name->value();
// Set the date to the creation date of the order if not provided
if (isset($data['date']) && $data['date'] !== '') {
$date = date('Y-m-d H:i:s', strtotime($data['date']));
} else {
$date = date('Y-m-d H:i:s', strtotime($orders_o->created_at->value()));
}
// Determine if already exists
$already_exists = $orders_o->hasWashCertificateAttached();
// Generate (no-op if already exists)
$orders_o->generateWashCertificate($safety_seal, $operator, $date);
$now_exists = $orders_o->hasWashCertificateAttached();
// If a booking is associated, send the wash certificate email to the customer
if ($orders_o->booking_id->value()) {
$order_booking = $orders_o->getOrderBooking();
$order_booking?->sendWashCertificateToCustomer();
}
(new logs_o())->add('orders', $orders_o->department_id->value(), 1, $user->id, 'GENERATE_ORDER_WASH_CERTIFICATE', 'Wash certificate ' . ($already_exists ? 'already existed' : 'generated'));
$response->success([
'order_id' => (int)$orders_o->id,
'created' => !$already_exists && $now_exists,
'already_existed' => $already_exists,
]);
}, [
'complete_bookings' => 'Generate and attach a wash certificate PDF to an order',
'department_access_:id' => 'Access to the department the order is in'
]);
/**
* $this->put('/order', function () {
*
+16 -2
View File
@@ -23,6 +23,20 @@ class plateScansRoute
$this->requirePlateScannerAuth();
// Get the plate scanner object
$plate_scanner = (new authentication())->get_plate_scanner();
/**
* Define the optional request body parameters
* - plate: The license plate to scan (string, required)
* - bayId: The wash bay id (int, optional) - XLVASK lanes only
*/
$bayId = null;
// Validate the bayId if set
$bayIdParameterKey = 'bay_id';
if (self::isParametersSet([$bayIdParameterKey])) {
$bayId = self::getParameter($bayIdParameterKey);
self::requireType($bayId, self::type_string());
self::requireMinLength($bayIdParameterKey, 1);
self::requireMaxLength($bayIdParameterKey, 255);
}
// Get the post data
$data = json_decode(file_get_contents('php://input'), true);
// Check if the required fields are set
@@ -30,11 +44,11 @@ class plateScansRoute
$response->error('Missing required body parameter plate', 400);
}
// Add the number plate scanner
(new plate_scans_o())->add($plate_scanner->id, $data['plate']);
(new plate_scans_o())->add($plate_scanner->id, $data['plate'], $bayId);
// Log the incident
(new logs_o())->add('numberplatescans', $plate_scanner->department_id->value(), 1, 0, 'ADD_NUMBER_PLATE_SCANS', 'Successfully added a number plate scan: ' . $data['plate']);
// Return a success message
$response->success(['message' => 'License plate scan recorded.', 'plate' => $data['plate'], 'scanner' => $plate_scanner->name->value()], 201);
$response->success(['message' => 'License plate scan recorded.', 'plate' => $data['plate'], 'scanner' => $plate_scanner->name->value(), 'bay_id' => $bayId], 201);
});
$this->post('/numberplatescans/department', function () {
+15
View File
@@ -5,6 +5,7 @@ namespace routes;
use classes\economic;
use classes\router;
use classes\shelly;
use classes\slack;
use classes\virkdata;
use modules\shelly\helpers\shelly_device_switch;
use modules\shelly\helpers\shelly_request_body_get_states;
@@ -18,6 +19,20 @@ class workerRoute
public function run(): void
{
$this->get('/worker/version', function () {
global /** @var router $router */
$response, $router;
$response->success(['version' => redis->get('worker_target_version') ?? 'unknown'] );
});
$this->get('/worker/update-version', function () {
global /** @var router $router */
$response, $router;
self::requirePermission('worker_update_version');
self::requireParameters(['version']);
$version = (string)self::getParameter('version');
redis->set('worker_target_version', $version);
$response->success(['message' => 'Version update functionality is not yet implemented.']);
});
$this->get('/worker/status', function () {
global /** @var router $router */
$response, $router;
@@ -0,0 +1,12 @@
{
"dev": {
"api_url": "https://api.truckwash.dk:4433",
"registration_number_1": "AB12345",
"registration_number_2": "EC21233",
"registration_number_3": "EC21234",
"customer_number": "12345679",
"customer_id": "1952",
"bay_guid_1": "df640a07-918a-465e-abf7-c27f983d8a37",
"department_id": "12"
}
}
@@ -0,0 +1,10 @@
### GET Invoice PDF
@order_id = 12345
GET {{api_url}}/invoices/pdf
Accept: application/json
Authorization: Bearer {{auth_token_superuser}}
Content-Type: application/json
{
"id": {{order_id}}
}
@@ -1,9 +1,12 @@
### POST /numberplatescans
POST /numberplatescans HTTP/1.1
Host: truckwashdev.maintenancemode.cloud
Authorization: Bearer 2d9cc49436b716c69abb39e152f0024e6a64734d131d77eb4f87b5b5aeb542fd
@plate = {{registration_number_1}}
@bay_id = {{bay_guid_1}}
POST {{api_url}}/numberplatescans
Accept: application/json
Authorization: Bearer {{auth_token_license_plate_scanner}}
Content-Type: application/json
{
"plate": "AB123"
"plate": "{{plate}}",
"bay_id": "{{bay_id}}"
}
@@ -0,0 +1,10 @@
### GET Invoice PDF
@order_id = 9
GET {{api_url}}/orders/potential-duplicates
Accept: application/json
Authorization: Bearer {{auth_token_superuser}}
Content-Type: application/json
{
"id": {{order_id}}
}
@@ -0,0 +1,35 @@
### POST /modules/self-serve/lane/command START
@lane_id = 1
POST {{api_url}}/modules/self-serve/lane/command
Accept: application/json
Content-Type: application/json
{
"command": "START",
"customer_number": {{customer_number}},
"license_plate": "{{registration_number_1}}",
"lane_id": {{lane_id}}
}
### POST /modules/self-serve/lane/command STOP
POST {{api_url}}/modules/self-serve/lane/command
Accept: application/json
Content-Type: application/json
Authorization: Bearer {{auth_token_superuser}}
{
"command": "STOP",
"lane_id": {{lane_id}}
}
### POST /modules/self-serve/lane/command RESET
POST {{api_url}}/modules/self-serve/lane/command
Accept: application/json
Content-Type: application/json
Authorization: Bearer {{auth_token_superuser}}
{
"command": "RESET",
"lane_id": {{lane_id}}
}
@@ -49,6 +49,7 @@ use objects\users_o;
trait db_object_t
{
public static string $asArrayCacheKey = 'as_array'; // The cache key for the asArray function
public static int $cashierNameCacheExpiration = 300; // The cashier name cache expiration time, in seconds. Default is 1 day (86400 seconds).
public static int $economicCustomerNameCacheExpiration = 86400; // The economic customer name cache expiration time, in seconds. Default is 1 day (86400 seconds).
public static int $asArrayCacheExpiration = 600; // The id of the object in the database
+10
View File
@@ -249,6 +249,16 @@ trait route_t
return self::hasPermission('department_access_' . $department . ($permission ? '_' . $permission : ''));
}
/**
* Get parameters as an array from the request
* @return array
*/
public function getParametersAsArray(): array
{
global $response;
return $response->getAllRequestParameters();
}
/**
* Require permission
* @param string $permission
+213
View File
@@ -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
}
]
}