2083 lines
93 KiB
PHP
2083 lines
93 KiB
PHP
<?php
|
|
|
|
namespace routes;
|
|
|
|
use classes\authentication;
|
|
use classes\email;
|
|
use classes\response;
|
|
use classes\router;
|
|
use classes\selfserve;
|
|
use classes\stripe;
|
|
use modules\selfserve\classes\selfserve_config_versioning;
|
|
use modules\selfserve\classes\selfserve_lane;
|
|
use modules\selfserve\classes\selfserve_wash_flow;
|
|
use modules\selfserve\helpers\selfserve_lane_command;
|
|
use modules\selfserve\helpers\selfserve_lane_port;
|
|
use modules\selfserve\helpers\selfserve_lane_relay;
|
|
use modules\selfserve\helpers\selfserve_lane_state;
|
|
use modules\selfserve\helpers\selfserve_lane_status;
|
|
use modules\selfserve\helpers\selfserve_wash_session_status;
|
|
use objects\department_lanes_o;
|
|
use objects\departments_o;
|
|
use objects\logs_o;
|
|
use objects\orders_o;
|
|
use objects\customer_vehicles_o;
|
|
use objects\selfserve_wash_sessions_o;
|
|
use objects\selfserve_wash_session_tasks_o;
|
|
use objects\stripe_module_customers_o;
|
|
use objects\users_o;
|
|
use traits\route_t;
|
|
|
|
class moduleSelfServeRoute
|
|
{
|
|
use route_t;
|
|
|
|
private const MAX_GATE_OPEN_TOGGLE_AFTER_SECONDS = 5;
|
|
private const CUSTOMER_SELFSERVE_PERMISSION = 'list_own_department_selfserve_vehicle_conditions';
|
|
|
|
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_id = self::isParametersSet(['lane_id']) ? (int)self::getParameter('lane_id') : 1;
|
|
$lane = $selfserve->lane($lane_id);
|
|
$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',
|
|
]
|
|
);
|
|
|
|
$this->put('/modules/self-serve/lane/status', function () {
|
|
global $response;
|
|
|
|
self::requirePermission('modules_selfserve_lane_status_set');
|
|
self::requireParameters(['lane_id', 'enabled']);
|
|
$lane_id = (int)self::getParameter('lane_id');
|
|
self::requireType($lane_id, self::type_int());
|
|
self::requireMinValue($lane_id, 1);
|
|
|
|
$department_lane = (new department_lanes_o())->select($lane_id);
|
|
if (!$department_lane->exists()) {
|
|
$response->error('Department lane not found', 404);
|
|
}
|
|
self::requireDepartmentAccess((int)$department_lane->department->value());
|
|
|
|
$enabled = $this->requestedBoolean('enabled');
|
|
$target_status = $enabled ? selfserve_lane_status::AVAILABLE : selfserve_lane_status::MAINTENANCE;
|
|
$lane = (new selfserve())->lane($lane_id);
|
|
$lane->setLaneStatus($target_status);
|
|
|
|
$user = (new authentication())->get_user();
|
|
$machine_status_audit = [
|
|
'modified_at' => date(DATE_ATOM),
|
|
'modified_by_user_id' => $user ? (int)$user->id : null,
|
|
'modified_by_name' => $this->machineStatusAuditUserName($user),
|
|
];
|
|
$lane->setLaneStatusAudit($machine_status_audit);
|
|
|
|
(new logs_o())->add(
|
|
'selfserve',
|
|
'global',
|
|
1,
|
|
$user ? $user->id : 0,
|
|
'SET_LANE_MACHINE_STATUS',
|
|
'User set self-serve lane ' . $lane_id . ' machine status to ' . $target_status->name
|
|
);
|
|
|
|
$status = (string)$lane->getLaneStatus()->name;
|
|
$response->success([
|
|
'id' => $lane->id,
|
|
'status' => $status,
|
|
'machine_status_enabled' => department_lanes_o::isOperationalStatusName($status),
|
|
'machine_status_audit' => $machine_status_audit,
|
|
'machine_status_modified_at' => $machine_status_audit['modified_at'],
|
|
'machine_status_modified_by' => $machine_status_audit['modified_by_name'],
|
|
'machine_status_modified_by_user_id' => $machine_status_audit['modified_by_user_id'],
|
|
'lane' => $department_lane->asArray(),
|
|
]);
|
|
},
|
|
[
|
|
'modules_selfserve_lane_status_set' => 'Set self-serve lane machine status',
|
|
]
|
|
);
|
|
|
|
/** Modules > Self Serve > Lane > Wash > In-progress details */
|
|
$this->get('/modules/self-serve/lane/wash/in-progress', function () {
|
|
global $response;
|
|
self::requireParameters(['lane_id']);
|
|
$lane_id = (int)$this->getParameter('lane_id');
|
|
self::requireType($lane_id, self::type_int());
|
|
self::requireMinValue($lane_id, 1);
|
|
$customer_scope = $this->requireInProgressWashDetailsAccess($lane_id);
|
|
|
|
$build_customer = static function (?int $customer_number): ?array {
|
|
if ($customer_number === null || $customer_number <= 0) {
|
|
return null;
|
|
}
|
|
|
|
$customer_obj = (new users_o())->getUserByCustomerNumber($customer_number);
|
|
if ($customer_obj->exists()) {
|
|
return [
|
|
'id' => (int)$customer_obj->id,
|
|
'customer_number' => $customer_number,
|
|
'display_name' => $customer_obj->display_name->value() === null ? null : (string)$customer_obj->display_name->value(),
|
|
'email' => $customer_obj->email->value() === null ? null : (string)$customer_obj->email->value(),
|
|
'phone_country_code' => $customer_obj->phone_country_code->value() === null ? null : (int)$customer_obj->phone_country_code->value(),
|
|
'phone' => $customer_obj->phone->value() === null ? null : (string)$customer_obj->phone->value(),
|
|
];
|
|
}
|
|
|
|
return [
|
|
'id' => null,
|
|
'customer_number' => $customer_number,
|
|
'display_name' => null,
|
|
'email' => null,
|
|
'phone_country_code' => null,
|
|
'phone' => null,
|
|
];
|
|
};
|
|
|
|
$build_vehicle = static function (?int $vehicle_id, ?string $reg): ?array {
|
|
$vehicle_obj = null;
|
|
if ($vehicle_id !== null && $vehicle_id > 0) {
|
|
$tmp_vehicle = (new customer_vehicles_o())->select($vehicle_id);
|
|
if ($tmp_vehicle->exists()) {
|
|
$vehicle_obj = $tmp_vehicle;
|
|
}
|
|
}
|
|
if ($vehicle_obj === null && $reg !== null && trim($reg) !== '') {
|
|
$tmp_vehicle = (new customer_vehicles_o())->selectByPlate(trim($reg));
|
|
if ($tmp_vehicle->exists()) {
|
|
$vehicle_obj = $tmp_vehicle;
|
|
}
|
|
}
|
|
if ($vehicle_obj === null) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'id' => (int)$vehicle_obj->id,
|
|
'customer_id' => (int)$vehicle_obj->customer_id->value(),
|
|
'type' => (int)$vehicle_obj->type->value(),
|
|
'reg' => (string)$vehicle_obj->reg->value(),
|
|
'reference' => $vehicle_obj->reference->value() === null ? null : (string)$vehicle_obj->reference->value(),
|
|
];
|
|
};
|
|
|
|
$format_wash_started_at = static function (?int $wash_start_time): ?string {
|
|
if ($wash_start_time === null || $wash_start_time <= 0) {
|
|
return null;
|
|
}
|
|
|
|
return date('Y-m-d H:i:s', $wash_start_time);
|
|
};
|
|
$selfserve = new selfserve();
|
|
$included_minutes_when_machine_enabled = (int)$selfserve
|
|
->config
|
|
->machine_wash_minutes_included
|
|
->getVariableValue();
|
|
if ($included_minutes_when_machine_enabled < 0) {
|
|
$included_minutes_when_machine_enabled = 0;
|
|
}
|
|
|
|
$rows = (new selfserve_wash_sessions_o())->getFieldsWhere([
|
|
'lane_id' => $lane_id,
|
|
'completed_at' => null,
|
|
'deleted_at' => null,
|
|
], ['id']);
|
|
$session = null;
|
|
if ($rows !== []) {
|
|
usort($rows, static fn(array $a, array $b): int => (int)$b['id'] <=> (int)$a['id']);
|
|
$tmp_session = (new selfserve_wash_sessions_o())->select((int)$rows[0]['id']);
|
|
if ($tmp_session->exists()) {
|
|
$session = $tmp_session;
|
|
}
|
|
}
|
|
|
|
if ($session === null) {
|
|
$lane = $selfserve->lane($lane_id);
|
|
$lane_status = $lane->getLaneStatus();
|
|
$lane_state = $lane->getLaneState();
|
|
$wash_started_at = $format_wash_started_at($lane->getWashStartTime());
|
|
$machine_start_triggered = $wash_started_at !== null;
|
|
$machine_relay_enabled = $machine_start_triggered
|
|
|| $lane_state->equals(\modules\selfserve\helpers\selfserve_lane_state::MACHINE_RELAY_ON_QUEUED)
|
|
|| $lane_state->equals(\modules\selfserve\helpers\selfserve_lane_state::MACHINE_RELAY_ON)
|
|
|| $lane_state->equals(\modules\selfserve\helpers\selfserve_lane_state::IN_WASH)
|
|
|| $lane_state->equals(\modules\selfserve\helpers\selfserve_lane_state::EXIT_PORT_OPEN_QUEUED)
|
|
|| $lane_state->equals(\modules\selfserve\helpers\selfserve_lane_state::EXIT_PORT_OPEN);
|
|
$included_minutes = $machine_relay_enabled ? $included_minutes_when_machine_enabled : null;
|
|
$in_progress = $lane_status->equals(\modules\selfserve\helpers\selfserve_lane_status::OCCUPIED)
|
|
|| $lane_state->equals(\modules\selfserve\helpers\selfserve_lane_state::IN_WASH);
|
|
|
|
if (!$in_progress) {
|
|
$response->success($this->scopeInProgressWashResponseForCustomer([
|
|
'lane_id' => $lane_id,
|
|
'in_progress' => false,
|
|
'session' => null,
|
|
'customer' => null,
|
|
'vehicle' => null,
|
|
], $customer_scope));
|
|
return;
|
|
}
|
|
|
|
$runtime_customer_number = (int)$lane->getCustomerNumber();
|
|
if ($runtime_customer_number <= 0) {
|
|
$runtime_customer_number = null;
|
|
}
|
|
$runtime_reg = trim((string)$lane->getLicensePlate());
|
|
if ($runtime_reg === '') {
|
|
$runtime_reg = null;
|
|
}
|
|
|
|
$customer = $build_customer($runtime_customer_number);
|
|
$vehicle = $build_vehicle(null, $runtime_reg);
|
|
$response->success($this->scopeInProgressWashResponseForCustomer([
|
|
'lane_id' => $lane_id,
|
|
'in_progress' => true,
|
|
'session' => [
|
|
'id' => null,
|
|
'status' => $lane_state->name,
|
|
'reg' => $runtime_reg,
|
|
'customer_number' => $runtime_customer_number,
|
|
'vehicle_id' => $vehicle['id'] ?? null,
|
|
'vehicle_type_id' => $vehicle['type'] ?? null,
|
|
'included_minutes' => $included_minutes,
|
|
'machine_type_id' => null,
|
|
'machine_relay_enabled' => $machine_relay_enabled,
|
|
'machine_relay_enabled_at' => $machine_relay_enabled ? $wash_started_at : null,
|
|
'machine_start_triggered' => $machine_start_triggered,
|
|
'machine_start_triggered_at' => $wash_started_at,
|
|
'wash_started_at' => $wash_started_at,
|
|
'created_at' => null,
|
|
'updated_at' => null,
|
|
],
|
|
'customer' => $customer,
|
|
'vehicle' => $vehicle,
|
|
], $customer_scope));
|
|
return;
|
|
}
|
|
|
|
$customer_number = $session->customer_number->value() === null ? null : (int)$session->customer_number->value();
|
|
$vehicle_id = $session->vehicle_id->value() === null ? null : (int)$session->vehicle_id->value();
|
|
$session_reg = trim((string)$session->reg->value());
|
|
if ($session_reg === '') {
|
|
$session_reg = null;
|
|
}
|
|
$customer = $build_customer($customer_number);
|
|
$vehicle = $build_vehicle($vehicle_id, $session_reg);
|
|
$machine_start_triggered_at = $session->machine_start_triggered_at->value() === null ? null : (string)$session->machine_start_triggered_at->value();
|
|
$wash_started_at = $session->wash_started_at->value() === null ? null : (string)$session->wash_started_at->value();
|
|
if ($wash_started_at === null) {
|
|
$wash_started_at = $machine_start_triggered_at;
|
|
}
|
|
if ($wash_started_at === null) {
|
|
$wash_started_at = $format_wash_started_at($selfserve->lane($lane_id)->getWashStartTime());
|
|
}
|
|
$machine_relay_enabled = (bool)$session->machine_relay_enabled->value();
|
|
$included_minutes = $machine_relay_enabled ? $included_minutes_when_machine_enabled : null;
|
|
$status = $session->status->value();
|
|
$in_progress_statusses = [
|
|
selfserve_wash_session_status::MACHINE_RELAY_ENABLED->value,
|
|
selfserve_wash_session_status::READY_FOR_MACHINE_START->value,
|
|
selfserve_wash_session_status::MACHINE_STARTED->value,
|
|
selfserve_wash_session_status::PENDING_QUESTIONS->value,
|
|
selfserve_wash_session_status::MACHINE_NOT_ALLOWED->value
|
|
];
|
|
$in_progress = in_array($status, $in_progress_statusses);
|
|
|
|
$response->success($this->scopeInProgressWashResponseForCustomer([
|
|
'lane_id' => $lane_id,
|
|
'status' => (string)$session->status->value(),
|
|
'in_progress' => $in_progress,
|
|
'elapsed_minutes' => $session->getElapsedMinutes(),
|
|
'session' => [
|
|
'id' => (int)$session->id,
|
|
'status' => (string)$session->status->value(),
|
|
'reg' => $session_reg,
|
|
'customer_number' => $customer_number,
|
|
'vehicle_id' => $vehicle_id,
|
|
'vehicle_type_id' => $session->vehicle_type_id->value() === null ? null : (int)$session->vehicle_type_id->value(),
|
|
'included_minutes' => $included_minutes ?? 0,
|
|
'machine_type_id' => $session->machine_type_id->value() === null ? null : (int)$session->machine_type_id->value(),
|
|
'machine_relay_enabled' => $machine_relay_enabled,
|
|
'machine_relay_enabled_at' => $session->machine_relay_enabled_at->value() === null ? null : (string)$session->machine_relay_enabled_at->value(),
|
|
'machine_start_triggered' => (bool)$session->machine_start_triggered->value(),
|
|
'machine_start_triggered_at' => $machine_start_triggered_at,
|
|
'wash_started_at' => $wash_started_at,
|
|
'created_at' => (string)$session->created_at->value(),
|
|
'updated_at' => $session->updated_at->value() === null ? null : (string)$session->updated_at->value(),
|
|
],
|
|
'customer' => $customer,
|
|
'vehicle' => $vehicle,
|
|
], $customer_scope));
|
|
},
|
|
[
|
|
'modules_selfserve_lane_wash_in_progress_view' => 'View customer and vehicle details for an in-progress self-serve wash on a lane',
|
|
'list_own_department_selfserve_vehicle_conditions' => 'View in-progress self-serve wash details for the authenticated customer',
|
|
]
|
|
);
|
|
|
|
/** Modules > Self Serve > Lane > Wash > My active wash */
|
|
$this->get('/modules/self-serve/lane/wash/my-active-wash', function () {
|
|
global $response;
|
|
|
|
$customer_number = $this->requireMyActiveWashCustomerNumber();
|
|
$session = $this->findLatestActiveSelfServeSessionForCustomer($customer_number);
|
|
if (!$session->exists()) {
|
|
$response->error('No active self-serve wash found.', 404);
|
|
}
|
|
|
|
$response->success($this->buildActiveSelfServeSessionResponse($session));
|
|
},
|
|
[
|
|
'list_own_department_selfserve_vehicle_conditions' => 'View the authenticated customer\'s active self-serve wash',
|
|
]
|
|
);
|
|
|
|
/** Modules > Self Serve > Sessions */
|
|
$this->get('/modules/self-serve/sessions', function () {
|
|
global $response;
|
|
self::requirePermission('modules_selfserve_sessions_view');
|
|
|
|
$sessions = new selfserve_wash_sessions_o();
|
|
$sessions->setSearchableFields([
|
|
'id',
|
|
'lane_id',
|
|
'department_id',
|
|
'machine_type_id',
|
|
'customer_number',
|
|
'vehicle_id',
|
|
'vehicle_type_id',
|
|
'reg',
|
|
'status',
|
|
'order_id',
|
|
'created_at',
|
|
'completed_at',
|
|
]);
|
|
|
|
$additional_where = null;
|
|
if ($this->requestedBoolean('open_only', false) || $this->requestedBoolean('active_only', false)) {
|
|
$additional_where = '`completed_at` IS NULL AND `status` NOT IN (' . selfserve_wash_sessions_o::terminalStatusSqlList() . ')';
|
|
}
|
|
|
|
$response->success($sessions->listObjectsWithPaginationIfSet(
|
|
function (array $row): array {
|
|
$session = (new selfserve_wash_sessions_o())->select((int)$row['id']);
|
|
if (!$session->exists()) {
|
|
return $row;
|
|
}
|
|
|
|
return [
|
|
...$session->asArray(),
|
|
'elapsed_minutes' => $session->getElapsedMinutes(),
|
|
'open' => $session->isOpen(),
|
|
];
|
|
},
|
|
null,
|
|
[],
|
|
$additional_where
|
|
));
|
|
},
|
|
[
|
|
'modules_selfserve_sessions_view' => 'View self-serve wash sessions',
|
|
]
|
|
);
|
|
|
|
/** Modules > Self Serve > Session detail */
|
|
$this->get('/modules/self-serve/sessions/{id}', function () {
|
|
global $response;
|
|
self::requirePermission('modules_selfserve_sessions_view');
|
|
$session_id = (int)$this->fromRoute('id');
|
|
self::requireType($session_id, self::type_int());
|
|
self::requireMinValue($session_id, 1);
|
|
|
|
try {
|
|
$response->success((new selfserve_wash_flow())->getSessionSummary($session_id));
|
|
} catch (\Throwable $e) {
|
|
$response->error($e->getMessage(), 404);
|
|
}
|
|
},
|
|
[
|
|
'modules_selfserve_sessions_view' => 'View self-serve wash session details',
|
|
]
|
|
);
|
|
|
|
/** Modules > Self Serve > Lane > Force > Stop */
|
|
$this->post('/modules/self-serve/lane/force/stop', function () {
|
|
global $response;
|
|
self::requirePermission('modules_selfserve_sessions_force_stop');
|
|
self::requireParameters(['lane_id', 'bill']);
|
|
|
|
$lane_id = (int)$this->getParameter('lane_id');
|
|
self::requireType($lane_id, self::type_int());
|
|
self::requireMinValue($lane_id, 1);
|
|
|
|
$session_id = null;
|
|
if ($this->isParametersSet(['session_id']) && $this->getParameter('session_id') !== null && $this->getParameter('session_id') !== '') {
|
|
$session_id = (int)$this->getParameter('session_id');
|
|
self::requireType($session_id, self::type_int());
|
|
self::requireMinValue($session_id, 1);
|
|
}
|
|
|
|
$bill = $this->requestedBoolean('bill');
|
|
if ($bill) {
|
|
self::requirePermission('modules_selfserve_sessions_force_stop_bill');
|
|
}
|
|
$reason = null;
|
|
if ($this->isParametersSet(['reason'])) {
|
|
$reason = trim((string)$this->getParameter('reason'));
|
|
$reason = $reason === '' ? null : $reason;
|
|
}
|
|
$user = (new authentication())->get_user();
|
|
$user_id = $user instanceof users_o ? (int)$user->id : null;
|
|
|
|
try {
|
|
$response->success((new selfserve_wash_flow())->forceStopLane($lane_id, $session_id, $bill, $reason, $user_id));
|
|
} catch (\Throwable $e) {
|
|
$response->error($e->getMessage(), 409);
|
|
}
|
|
},
|
|
[
|
|
'modules_selfserve_sessions_force_stop' => 'Force stop a self-serve wash session without relay or gate signaling',
|
|
'modules_selfserve_sessions_force_stop_bill' => 'Bill elapsed minutes when force stopping a self-serve wash session',
|
|
]
|
|
);
|
|
|
|
/** Modules > Self Serve > Lane > Command */
|
|
$this->post('/modules/self-serve/lane/command', function () {
|
|
global $response;
|
|
$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());
|
|
$command = selfserve_lane_command::tryFrom($commandParam);
|
|
if ($command === null) {
|
|
$response->error("Invalid command: " . $commandParam);
|
|
}
|
|
// Get the lane and command
|
|
$lane = $selfserve->lane($lane_id);
|
|
// Preserve not-found behavior before evaluating elevated/customer alternatives.
|
|
if (empty($lane->department_lane) || empty($lane->department_lane->department)) {
|
|
$response->error('Lane department not found', 404);
|
|
}
|
|
$customer_number = $this->resolveEffectiveCustomerNumber();
|
|
$customer_number = $customer_number === null ? 0 : (int)$customer_number;
|
|
[
|
|
$allow_customer_self_serve,
|
|
$requires_active_wash,
|
|
$allow_department_active_wash
|
|
] = $this->customerSelfServeCommandAccessRequirements($command);
|
|
$this->requireSelfServeLaneDepartmentOrCustomerAccess(
|
|
$lane,
|
|
$customer_number,
|
|
$allow_customer_self_serve,
|
|
$requires_active_wash,
|
|
$allow_department_active_wash
|
|
);
|
|
// 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);
|
|
}
|
|
// Require permissions for specific commands
|
|
switch ($command) {
|
|
case selfserve_lane_command::START:
|
|
$this->requireSelfServeLaneCommandPermission(
|
|
$lane,
|
|
$customer_number,
|
|
'modules_selfserve_lane_command_execute_start',
|
|
true
|
|
);
|
|
break;
|
|
case selfserve_lane_command::STOP:
|
|
$this->requireSelfServeLaneCommandPermission(
|
|
$lane,
|
|
$customer_number,
|
|
'modules_selfserve_lane_command_execute_stop',
|
|
true,
|
|
true,
|
|
false
|
|
);
|
|
break;
|
|
case selfserve_lane_command::RESERVE:
|
|
$this->requireOperatorLaneCommandPermission(
|
|
$lane,
|
|
'modules_selfserve_lane_command_execute_reserve'
|
|
);
|
|
break;
|
|
case selfserve_lane_command::RELEASE:
|
|
$this->requireOperatorLaneCommandPermission(
|
|
$lane,
|
|
'modules_selfserve_lane_command_execute_release'
|
|
);
|
|
break;
|
|
case selfserve_lane_command::RESET:
|
|
$this->requireOperatorLaneCommandPermission(
|
|
$lane,
|
|
'modules_selfserve_lane_command_execute_reset'
|
|
);
|
|
break;
|
|
case selfserve_lane_command::OPEN_PROPERTY_ACCESS_GATE:
|
|
$this->requirePropertyGateCommandPermission(
|
|
'modules_selfserve_lane_command_execute_open_property_access_gate',
|
|
$lane,
|
|
$customer_number
|
|
);
|
|
break;
|
|
case selfserve_lane_command::OPEN_PROPERTY_EXIT_GATE:
|
|
$this->requirePropertyGateCommandPermission(
|
|
'modules_selfserve_lane_command_execute_open_property_exit_gate',
|
|
$lane,
|
|
$customer_number
|
|
);
|
|
break;
|
|
}
|
|
// Execute the command
|
|
try {
|
|
$this->applyShellyTransportOverride($lane);
|
|
$subuser = (new authentication())->get_subuser();
|
|
$args = new \modules\selfserve\classes\selfserve_lane_command_arguments();
|
|
$args->setParameters([
|
|
...$this->getParametersAsArray(), // Pass all parameters
|
|
'customer_number' => $customer_number, // Get customer number from request user
|
|
'subuser_id' => $subuser === false ? null : (int)$subuser->id,
|
|
]);
|
|
$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(),
|
|
'transport' => $this->requestedShellyTransportOverride(),
|
|
]);
|
|
} catch (\Exception $e) {
|
|
if (
|
|
$command === selfserve_lane_command::OPEN_PROPERTY_ACCESS_GATE ||
|
|
$command === selfserve_lane_command::OPEN_PROPERTY_EXIT_GATE
|
|
) {
|
|
error_log(
|
|
'Failed to execute self-serve property gate command ' . $command->name .
|
|
' for lane ' . $lane_id . ': ' . $e->getMessage()
|
|
);
|
|
}
|
|
$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_reserve' => 'Execute self-serve lane RESERVE command',
|
|
'modules_selfserve_lane_command_execute_release' => 'Execute self-serve lane RELEASE command (Release the lane reservation and reset its state)',
|
|
'modules_selfserve_lane_command_execute_reset' => 'Execute self-serve lane RESET command',
|
|
'modules_selfserve_lane_command_execute_open_property_access_gate' => 'Execute self-serve lane OPEN_PROPERTY_ACCESS_GATE command',
|
|
'modules_selfserve_lane_command_execute_open_property_exit_gate' => 'Execute self-serve lane OPEN_PROPERTY_EXIT_GATE command',
|
|
'modules_selfserve_lane_command_bypass_customer_number_validation' => 'Bypass customer number validation when executing commands',
|
|
]
|
|
);
|
|
|
|
/** Modules > Self Serve > Lane > Allowed services (derived from shown tasks) */
|
|
$this->post('/modules/self-serve/lane/services/allowed', function () {
|
|
global $response;
|
|
$selfserve = new selfserve();
|
|
// Validate parameters
|
|
self::requireParameters(['lane_id']);
|
|
$lane_id = (int)$this->getParameter('lane_id');
|
|
self::requireType($lane_id, self::type_int());
|
|
self::requireMinValue($lane_id, 1);
|
|
// Parse task_ids from request (optional, may be empty to clear)
|
|
$task_ids_param = $this->isParametersSet(['task_ids']) ? $this->getParameter('task_ids') : [];
|
|
if (is_string($task_ids_param)) {
|
|
$decoded = json_decode($task_ids_param, true);
|
|
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
|
$task_ids_param = $decoded;
|
|
} else {
|
|
$task_ids_param = array_filter(array_map(fn($s) => (int)trim($s), explode(',', $task_ids_param)), fn($v) => $v > 0);
|
|
}
|
|
}
|
|
if (!is_array($task_ids_param)) {
|
|
$response->error('Invalid format for task_ids. Expected array, JSON array, or comma-separated string of IDs.', 400);
|
|
}
|
|
$task_ids = array_values(array_unique(array_map(fn($v) => (int)$v, $task_ids_param)));
|
|
// Build allowed services from provided tasks
|
|
$lane = $selfserve->lane($lane_id);
|
|
$customer_number = $this->resolveEffectiveCustomerNumber();
|
|
$this->requireSelfServeLaneAccess(
|
|
$lane,
|
|
$customer_number === null ? 0 : (int)$customer_number,
|
|
['modules_selfserve_lane_services_set_allowed'],
|
|
false
|
|
);
|
|
$session_task_services = [];
|
|
$latest_session = (new selfserve_wash_sessions_o())->selectLatestOpenByLane(
|
|
$lane_id,
|
|
$customer_number === null || (int)$customer_number <= 0 ? null : (int)$customer_number
|
|
);
|
|
if (!$latest_session->exists() && $customer_number !== null && (int)$customer_number > 0) {
|
|
$latest_session = (new selfserve_wash_sessions_o())->selectLatestOpenByLane($lane_id);
|
|
}
|
|
if ($latest_session->exists()) {
|
|
foreach ((new selfserve_wash_session_tasks_o())->listBySession((int)$latest_session->id) as $session_task) {
|
|
$task_id = (int)($session_task['task_id'] ?? 0);
|
|
if ($task_id > 0) {
|
|
$services = $session_task['services'] ?? [];
|
|
if (is_string($services)) {
|
|
$decoded_services = json_decode($services, true);
|
|
$services = json_last_error() === JSON_ERROR_NONE && is_array($decoded_services)
|
|
? $decoded_services
|
|
: [];
|
|
}
|
|
$session_task_services[$task_id] = is_array($services) ? $services : [];
|
|
}
|
|
}
|
|
}
|
|
|
|
$allowed_services = [];
|
|
$published_config_task_services = $this->publishedConfigTaskServicesForLane($lane, $task_ids);
|
|
$merge_services = static function (array $services) use (&$allowed_services): void {
|
|
foreach ($services as $srv) {
|
|
$name = strtoupper((string)$srv);
|
|
if (!in_array($name, $allowed_services, true)) {
|
|
$allowed_services[] = $name;
|
|
}
|
|
}
|
|
};
|
|
foreach ($task_ids as $tid) {
|
|
if ($tid <= 0) continue;
|
|
if (array_key_exists($tid, $session_task_services)) {
|
|
$merge_services($session_task_services[$tid]);
|
|
continue;
|
|
}
|
|
if (array_key_exists($tid, $published_config_task_services)) {
|
|
$merge_services($published_config_task_services[$tid]);
|
|
continue;
|
|
}
|
|
|
|
$t = new \objects\department_selfserve_tasks_o();
|
|
$t->select($tid);
|
|
if (!$t->exists()) continue; // ignore unknown ids
|
|
// Validate the task belongs to the same lane
|
|
if ((int)$t->lane->value() !== $lane_id) continue;
|
|
// Merge services (if any)
|
|
$merge_services((array)$t->services->value());
|
|
}
|
|
// Persist on lane cache (overwrites previous allowed services)
|
|
try {
|
|
$relay_sync = $lane->setAllowedServicesFromVisibleTasks($allowed_services);
|
|
$response->success([
|
|
'lane_id' => $lane_id,
|
|
'allowed_services' => $allowed_services,
|
|
'relay_sync' => $relay_sync,
|
|
'transport' => $this->requestedShellyTransportOverride(),
|
|
]);
|
|
} catch (\Exception $e) {
|
|
$response->error('Failed to set allowed services: ' . $e->getMessage(), 400);
|
|
}
|
|
}, [
|
|
'modules_selfserve_lane_services_set_allowed' => 'Set allowed services for a lane based on currently shown tasks (post-Q&A)'
|
|
]);
|
|
|
|
/** Modules > Self Serve > Lane > Gate > Open (ENTRANCE/EXIT) */
|
|
$this->post('/modules/self-serve/lane/gate/open', function () {
|
|
global $response;
|
|
self::requirePermission('modules_selfserve_lane_gate_open');
|
|
$selfserve = new selfserve();
|
|
self::requireParameters(['lane_id', 'gate']);
|
|
$lane_id = (int)$this->getParameter('lane_id');
|
|
self::requireType($lane_id, self::type_int());
|
|
self::requireMinValue($lane_id, 1);
|
|
|
|
$gate_name = strtoupper(trim((string)$this->getParameter('gate')));
|
|
$gate = match ($gate_name) {
|
|
'ENTRANCE' => selfserve_lane_port::ENTRANCE,
|
|
'EXIT' => selfserve_lane_port::EXIT,
|
|
default => null,
|
|
};
|
|
if ($gate === null) {
|
|
$response->error('Invalid gate value. Expected ENTRANCE or EXIT.', 400);
|
|
}
|
|
$toggle_after = $this->requestedRelayToggleAfter(1);
|
|
|
|
$lane = $selfserve->lane($lane_id);
|
|
try {
|
|
$this->applyShellyTransportOverride($lane);
|
|
$lane->open($gate, $toggle_after);
|
|
$response->success([
|
|
'lane_id' => $lane_id,
|
|
'gate' => $gate->name,
|
|
'opened' => true,
|
|
'toggle_after' => $toggle_after,
|
|
'state' => $lane->getLaneState()->name,
|
|
'transport' => $this->requestedShellyTransportOverride(),
|
|
]);
|
|
} catch (\Exception $e) {
|
|
error_log('Failed to open self-serve lane gate ' . $gate->name . ' for lane ' . $lane_id . ': ' . $e->getMessage());
|
|
$response->error('Failed to open ' . strtolower($gate->name) . ' gate.', 400);
|
|
}
|
|
}, [
|
|
'modules_selfserve_lane_gate_open' => 'Open ENTRANCE or EXIT gate for a self-serve lane'
|
|
]);
|
|
|
|
/** Modules > Self Serve > Lane > Relay > MACHINE_PROGRAM_PICKER status */
|
|
$this->get('/modules/self-serve/lane/relay/machine_program_picker/status', function () {
|
|
global $response;
|
|
self::requirePermission('modules_selfserve_lane_relay_machine_program_picker_status_view');
|
|
$selfserve = new selfserve();
|
|
self::requireParameters(['lane_id']);
|
|
$lane_id = (int)$this->getParameter('lane_id');
|
|
self::requireType($lane_id, self::type_int());
|
|
self::requireMinValue($lane_id, 1);
|
|
$lane = $selfserve->lane($lane_id);
|
|
|
|
try {
|
|
$this->applyShellyTransportOverride($lane);
|
|
$status = $lane->getMachineProgramPickerRelayStatus();
|
|
$response->success($this->buildRelayStatusResponse($lane_id, 'MACHINE_PROGRAM_PICKER', $status));
|
|
} catch (\Exception $e) {
|
|
$response->error('Failed to get MACHINE_PROGRAM_PICKER relay status: ' . $e->getMessage(), 400);
|
|
}
|
|
}, [
|
|
'modules_selfserve_lane_relay_machine_program_picker_status_view' => 'Get MACHINE_PROGRAM_PICKER relay status for a lane'
|
|
]);
|
|
|
|
/** Modules > Self Serve > Lane > Relay > MACHINE_PROGRAM_PICKER set on/off */
|
|
$this->post('/modules/self-serve/lane/relay/machine_program_picker/set', function () {
|
|
global $response;
|
|
self::requirePermission('modules_selfserve_lane_relay_machine_program_picker_status_set');
|
|
$selfserve = new selfserve();
|
|
self::requireParameters(['lane_id', 'on']);
|
|
$lane_id = (int)$this->getParameter('lane_id');
|
|
self::requireType($lane_id, self::type_int());
|
|
self::requireMinValue($lane_id, 1);
|
|
|
|
$raw_on = $this->getParameter('on');
|
|
$on = is_bool($raw_on) ? $raw_on : filter_var($raw_on, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
|
if ($on === null) {
|
|
$response->error('Invalid on value. Expected boolean true/false.', 400);
|
|
}
|
|
|
|
$lane = $selfserve->lane($lane_id);
|
|
try {
|
|
$this->applyShellyTransportOverride($lane);
|
|
$lane->setMachineProgramPickerRelayStatus((bool)$on);
|
|
$status = $lane->getMachineProgramPickerRelayStatus();
|
|
$response->success($this->buildRelayStatusResponse($lane_id, 'MACHINE_PROGRAM_PICKER', $status, [
|
|
'requested_on' => (bool)$on,
|
|
]));
|
|
} catch (\Exception $e) {
|
|
$response->error('Failed to set MACHINE_PROGRAM_PICKER relay status: ' . $e->getMessage(), 400);
|
|
}
|
|
}, [
|
|
'modules_selfserve_lane_relay_machine_program_picker_status_set' => 'Set MACHINE_PROGRAM_PICKER relay status (on/off) for a lane'
|
|
]);
|
|
|
|
/** Modules > Self Serve > Lane > Relay > MACHINE_CLEANER status */
|
|
$this->get('/modules/self-serve/lane/relay/machine_cleaner/status', function () {
|
|
global $response;
|
|
self::requirePermission('modules_selfserve_lane_relay_machine_cleaner_status_view');
|
|
$selfserve = new selfserve();
|
|
self::requireParameters(['lane_id']);
|
|
$lane_id = (int)$this->getParameter('lane_id');
|
|
self::requireType($lane_id, self::type_int());
|
|
self::requireMinValue($lane_id, 1);
|
|
$lane = $selfserve->lane($lane_id);
|
|
|
|
try {
|
|
$this->applyShellyTransportOverride($lane);
|
|
$status = $lane->getMachineCleanerRelayStatus();
|
|
$response->success($this->buildRelayStatusResponse($lane_id, 'MACHINE_CLEANER', $status));
|
|
} catch (\Exception $e) {
|
|
$response->error('Failed to get MACHINE_CLEANER relay status: ' . $e->getMessage(), 400);
|
|
}
|
|
}, [
|
|
'modules_selfserve_lane_relay_machine_cleaner_status_view' => 'Get MACHINE_CLEANER relay status for a lane'
|
|
]);
|
|
|
|
/** Modules > Self Serve > Lane > Relay > MACHINE_CLEANER set on/off */
|
|
$this->post('/modules/self-serve/lane/relay/machine_cleaner/set', function () {
|
|
global $response;
|
|
self::requirePermission('modules_selfserve_lane_relay_machine_cleaner_status_set');
|
|
$selfserve = new selfserve();
|
|
self::requireParameters(['lane_id', 'on']);
|
|
$lane_id = (int)$this->getParameter('lane_id');
|
|
self::requireType($lane_id, self::type_int());
|
|
self::requireMinValue($lane_id, 1);
|
|
|
|
$raw_on = $this->getParameter('on');
|
|
$on = is_bool($raw_on) ? $raw_on : filter_var($raw_on, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
|
if ($on === null) {
|
|
$response->error('Invalid on value. Expected boolean true/false.', 400);
|
|
}
|
|
|
|
$lane = $selfserve->lane($lane_id);
|
|
try {
|
|
$this->applyShellyTransportOverride($lane);
|
|
$lane->setMachineCleanerRelayStatus((bool)$on);
|
|
$status = $lane->getMachineCleanerRelayStatus();
|
|
$response->success($this->buildRelayStatusResponse($lane_id, 'MACHINE_CLEANER', $status, [
|
|
'requested_on' => (bool)$on,
|
|
]));
|
|
} catch (\Exception $e) {
|
|
$response->error('Failed to set MACHINE_CLEANER relay status: ' . $e->getMessage(), 400);
|
|
}
|
|
}, [
|
|
'modules_selfserve_lane_relay_machine_cleaner_status_set' => 'Set MACHINE_CLEANER relay status (on/off) for a lane'
|
|
]);
|
|
|
|
/** Modules > Self Serve > Lane > Relay > MACHINE status */
|
|
$this->get('/modules/self-serve/lane/relay/machine/status', function () {
|
|
global $response;
|
|
self::requirePermission('modules_selfserve_lane_relay_machine_status_view');
|
|
$selfserve = new selfserve();
|
|
self::requireParameters(['lane_id']);
|
|
$lane_id = (int)$this->getParameter('lane_id');
|
|
self::requireType($lane_id, self::type_int());
|
|
self::requireMinValue($lane_id, 1);
|
|
$lane = $selfserve->lane($lane_id);
|
|
|
|
try {
|
|
$this->applyShellyTransportOverride($lane);
|
|
$status = $lane->getMachineRelayStatus();
|
|
$response->success($this->buildRelayStatusResponse($lane_id, 'MACHINE', $status));
|
|
} catch (\Exception $e) {
|
|
$response->error('Failed to get MACHINE relay status: ' . $e->getMessage(), 400);
|
|
}
|
|
}, [
|
|
'modules_selfserve_lane_relay_machine_status_view' => 'Get MACHINE relay status for a lane'
|
|
]);
|
|
|
|
/** Modules > Self Serve > Lane > Relay > MACHINE set on/off */
|
|
$this->post('/modules/self-serve/lane/relay/machine/set', function () {
|
|
global $response;
|
|
self::requirePermission('modules_selfserve_lane_relay_machine_status_set');
|
|
$selfserve = new selfserve();
|
|
self::requireParameters(['lane_id', 'on']);
|
|
$lane_id = (int)$this->getParameter('lane_id');
|
|
self::requireType($lane_id, self::type_int());
|
|
self::requireMinValue($lane_id, 1);
|
|
|
|
$raw_on = $this->getParameter('on');
|
|
$on = is_bool($raw_on) ? $raw_on : filter_var($raw_on, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
|
if ($on === null) {
|
|
$response->error('Invalid on value. Expected boolean true/false.', 400);
|
|
}
|
|
|
|
$lane = $selfserve->lane($lane_id);
|
|
try {
|
|
$this->applyShellyTransportOverride($lane);
|
|
$lane->setMachineRelayStatus((bool)$on);
|
|
// Keep lane cache state aligned with the latest explicit relay action
|
|
try {
|
|
$machine_relay_id = '';
|
|
if (!empty($lane->department_lane) && !empty($lane->department_lane->relay_machine_id)) {
|
|
$machine_relay_id = trim((string)$lane->department_lane->relay_machine_id->value());
|
|
}
|
|
$is_demo_machine_relay = str_starts_with(strtolower($machine_relay_id), 'demo-');
|
|
$lane->setLaneState((bool)$on
|
|
? ($is_demo_machine_relay
|
|
? \modules\selfserve\helpers\selfserve_lane_state::MACHINE_RELAY_ON_QUEUED
|
|
: \modules\selfserve\helpers\selfserve_lane_state::MACHINE_RELAY_ON)
|
|
: ($is_demo_machine_relay
|
|
? \modules\selfserve\helpers\selfserve_lane_state::MACHINE_RELAY_OFF_QUEUED
|
|
: \modules\selfserve\helpers\selfserve_lane_state::MACHINE_RELAY_OFF)
|
|
);
|
|
} catch (\Throwable $ignored) {}
|
|
|
|
$status = $lane->getMachineRelayStatus();
|
|
$response->success($this->buildRelayStatusResponse($lane_id, 'MACHINE', $status, [
|
|
'requested_on' => (bool)$on,
|
|
]));
|
|
} catch (\Exception $e) {
|
|
$response->error('Failed to set MACHINE relay status: ' . $e->getMessage(), 400);
|
|
}
|
|
}, [
|
|
'modules_selfserve_lane_relay_machine_status_set' => 'Set MACHINE relay status (on/off) for a lane'
|
|
]);
|
|
|
|
/** Modules > Self Serve > Lane > Relay > Enable MACHINE_PROGRAM_PICKER (manual) */
|
|
$this->post('/modules/self-serve/lane/relay/machine_program_picker/enable', function () {
|
|
global $response;
|
|
self::requirePermission('modules_selfserve_lane_relay_enable_machine_program_picker');
|
|
$selfserve = new selfserve();
|
|
// Validate parameters
|
|
self::requireParameters(['lane_id']);
|
|
$lane_id = (int)$this->getParameter('lane_id');
|
|
self::requireType($lane_id, self::type_int());
|
|
self::requireMinValue($lane_id, 1);
|
|
$duration = null;
|
|
if ($this->isParametersSet(['duration'])) {
|
|
$duration = (int)$this->getParameter('duration');
|
|
self::requireMinValue($duration, 1);
|
|
}
|
|
$lane = $selfserve->lane($lane_id);
|
|
try {
|
|
$this->applyShellyTransportOverride($lane);
|
|
$lane->turnOnRelay(selfserve_lane_relay::MACHINE_PROGRAM_PICKER, $duration);
|
|
$response->success([
|
|
'lane_id' => $lane_id,
|
|
'relay' => 'MACHINE_PROGRAM_PICKER',
|
|
'enabled' => true,
|
|
'duration' => $duration,
|
|
'transport' => $this->requestedShellyTransportOverride(),
|
|
]);
|
|
} catch (\Exception $e) {
|
|
$response->error('Failed to enable MACHINE_PROGRAM_PICKER relay: ' . $e->getMessage(), 400);
|
|
}
|
|
}, [
|
|
'modules_selfserve_lane_relay_enable_machine_program_picker' => 'Manually enable MACHINE_PROGRAM_PICKER relay for a lane'
|
|
]);
|
|
|
|
/** Modules > Self Serve > Lane > Relay > Enable MACHINE_CLEANER (manual) */
|
|
$this->post('/modules/self-serve/lane/relay/machine_cleaner/enable', function () {
|
|
global $response;
|
|
self::requirePermission('modules_selfserve_lane_relay_enable_machine_cleaner');
|
|
$selfserve = new selfserve();
|
|
// Validate parameters
|
|
self::requireParameters(['lane_id']);
|
|
$lane_id = (int)$this->getParameter('lane_id');
|
|
self::requireType($lane_id, self::type_int());
|
|
self::requireMinValue($lane_id, 1);
|
|
$duration = null;
|
|
if ($this->isParametersSet(['duration'])) {
|
|
$duration = (int)$this->getParameter('duration');
|
|
self::requireMinValue($duration, 1);
|
|
}
|
|
$lane = $selfserve->lane($lane_id);
|
|
try {
|
|
$this->applyShellyTransportOverride($lane);
|
|
$lane->turnOnRelay(selfserve_lane_relay::MACHINE_CLEANER, $duration);
|
|
$response->success([
|
|
'lane_id' => $lane_id,
|
|
'relay' => 'MACHINE_CLEANER',
|
|
'enabled' => true,
|
|
'duration' => $duration,
|
|
'transport' => $this->requestedShellyTransportOverride(),
|
|
]);
|
|
} catch (\Exception $e) {
|
|
$response->error('Failed to enable MACHINE_CLEANER relay: ' . $e->getMessage(), 400);
|
|
}
|
|
}, [
|
|
'modules_selfserve_lane_relay_enable_machine_cleaner' => 'Manually enable MACHINE_CLEANER relay for a lane'
|
|
]);
|
|
|
|
/** Modules > Self Serve > Lane > Relay > Enable MACHINE (manual, gated by allowed services) */
|
|
$this->post('/modules/self-serve/lane/relay/machine/enable', function () {
|
|
global $response;
|
|
$selfserve = new selfserve();
|
|
// Validate parameters
|
|
self::requireParameters(['lane_id']);
|
|
$lane_id = (int)$this->getParameter('lane_id');
|
|
self::requireType($lane_id, self::type_int());
|
|
self::requireMinValue($lane_id, 1);
|
|
$duration = null;
|
|
if ($this->isParametersSet(['duration'])) {
|
|
$duration = (int)$this->getParameter('duration');
|
|
self::requireMinValue($duration, 1);
|
|
}
|
|
$lane = $selfserve->lane($lane_id);
|
|
$customer_number = $this->resolveEffectiveCustomerNumber();
|
|
self::requirePermission('modules_selfserve_lane_relay_enable_machine');
|
|
try {
|
|
$this->applyShellyTransportOverride($lane);
|
|
$lane->turnOnRelay(selfserve_lane_relay::MACHINE, $duration);
|
|
// A started wash should also turn on cleaner when configured.
|
|
try {
|
|
if (
|
|
!empty($lane->department_lane)
|
|
&& !empty($lane->department_lane->relay_machine_cleaner_id)
|
|
&& trim((string)$lane->department_lane->relay_machine_cleaner_id->value()) !== ''
|
|
) {
|
|
$lane->setMachineCleanerRelayStatusHard(true);
|
|
}
|
|
} catch (\Throwable $ignored) {}
|
|
$response->success([
|
|
'lane_id' => $lane_id,
|
|
'relay' => 'MACHINE',
|
|
'enabled' => true,
|
|
'duration' => $duration,
|
|
'transport' => $this->requestedShellyTransportOverride(),
|
|
]);
|
|
} catch (\Exception $e) {
|
|
$msg = $e->getMessage();
|
|
// If gating prevented activation, respond with 403
|
|
if (str_contains(strtoupper($msg), 'NOT ALLOWED')) {
|
|
$response->error($msg, 403);
|
|
}
|
|
$response->error('Failed to enable MACHINE relay: ' . $msg, 400);
|
|
}
|
|
}, [
|
|
'modules_selfserve_lane_relay_enable_machine' => 'Manually enable MACHINE relay for a lane (requires allowed task to be present)'
|
|
]);
|
|
|
|
/** Modules > Self Serve > Lane > Force > MACHINE_PROGRAM_PICKER enable */
|
|
$this->post('/modules/self-serve/lane/force/machine_program_picker/enable', function () {
|
|
global $response;
|
|
self::requirePermission('modules_selfserve_lane_force_machine_program_picker_enable');
|
|
$selfserve = new selfserve();
|
|
// Validate parameters
|
|
self::requireParameters(['lane_id']);
|
|
$lane_id = (int)$this->getParameter('lane_id');
|
|
self::requireType($lane_id, self::type_int());
|
|
self::requireMinValue($lane_id, 1);
|
|
$duration = null;
|
|
if ($this->isParametersSet(['duration'])) {
|
|
$duration = (int)$this->getParameter('duration');
|
|
self::requireMinValue($duration, 1);
|
|
}
|
|
$lane = $selfserve->lane($lane_id);
|
|
try {
|
|
$this->applyShellyTransportOverride($lane);
|
|
$lane->forceTurnOnRelay(selfserve_lane_relay::MACHINE_PROGRAM_PICKER, $duration);
|
|
$response->success([
|
|
'lane_id' => $lane_id,
|
|
'relay' => 'MACHINE_PROGRAM_PICKER',
|
|
'enabled' => true,
|
|
'duration' => $duration,
|
|
'transport' => $this->requestedShellyTransportOverride(),
|
|
]);
|
|
} catch (\Exception $e) {
|
|
$response->error('Failed to force enable MACHINE_PROGRAM_PICKER relay: ' . $e->getMessage(), 400);
|
|
}
|
|
}, [
|
|
'modules_selfserve_lane_force_machine_program_picker_enable' => 'Force enable MACHINE_PROGRAM_PICKER relay for a lane'
|
|
]);
|
|
|
|
/** Modules > Self Serve > Lane > Force > MACHINE_PROGRAM_PICKER disable */
|
|
$this->post('/modules/self-serve/lane/force/machine_program_picker/disable', function () {
|
|
global $response;
|
|
self::requirePermission('modules_selfserve_lane_force_machine_program_picker_disable');
|
|
$selfserve = new selfserve();
|
|
// Validate parameters
|
|
self::requireParameters(['lane_id']);
|
|
$lane_id = (int)$this->getParameter('lane_id');
|
|
self::requireType($lane_id, self::type_int());
|
|
self::requireMinValue($lane_id, 1);
|
|
$lane = $selfserve->lane($lane_id);
|
|
try {
|
|
$this->applyShellyTransportOverride($lane);
|
|
$lane->forceTurnOffRelay(selfserve_lane_relay::MACHINE_PROGRAM_PICKER);
|
|
$response->success([
|
|
'lane_id' => $lane_id,
|
|
'relay' => 'MACHINE_PROGRAM_PICKER',
|
|
'disabled' => true,
|
|
'transport' => $this->requestedShellyTransportOverride(),
|
|
]);
|
|
} catch (\Exception $e) {
|
|
$response->error('Failed to force disable MACHINE_PROGRAM_PICKER relay: ' . $e->getMessage(), 400);
|
|
}
|
|
}, [
|
|
'modules_selfserve_lane_force_machine_program_picker_disable' => 'Force disable MACHINE_PROGRAM_PICKER relay for a lane'
|
|
]);
|
|
|
|
/** Modules > Self Serve > Lane > Force > MACHINE_CLEANER enable */
|
|
$this->post('/modules/self-serve/lane/force/machine_cleaner/enable', function () {
|
|
global $response;
|
|
self::requirePermission('modules_selfserve_lane_force_machine_cleaner_enable');
|
|
$selfserve = new selfserve();
|
|
// Validate parameters
|
|
self::requireParameters(['lane_id']);
|
|
$lane_id = (int)$this->getParameter('lane_id');
|
|
self::requireType($lane_id, self::type_int());
|
|
self::requireMinValue($lane_id, 1);
|
|
$duration = null;
|
|
if ($this->isParametersSet(['duration'])) {
|
|
$duration = (int)$this->getParameter('duration');
|
|
self::requireMinValue($duration, 1);
|
|
}
|
|
$lane = $selfserve->lane($lane_id);
|
|
try {
|
|
$this->applyShellyTransportOverride($lane);
|
|
$lane->forceTurnOnRelay(selfserve_lane_relay::MACHINE_CLEANER, $duration);
|
|
$response->success([
|
|
'lane_id' => $lane_id,
|
|
'relay' => 'MACHINE_CLEANER',
|
|
'enabled' => true,
|
|
'duration' => $duration,
|
|
'transport' => $this->requestedShellyTransportOverride(),
|
|
]);
|
|
} catch (\Exception $e) {
|
|
$response->error('Failed to force enable MACHINE_CLEANER relay: ' . $e->getMessage(), 400);
|
|
}
|
|
}, [
|
|
'modules_selfserve_lane_force_machine_cleaner_enable' => 'Force enable MACHINE_CLEANER relay for a lane'
|
|
]);
|
|
|
|
/** Modules > Self Serve > Lane > Force > MACHINE_CLEANER disable */
|
|
$this->post('/modules/self-serve/lane/force/machine_cleaner/disable', function () {
|
|
global $response;
|
|
self::requirePermission('modules_selfserve_lane_force_machine_cleaner_disable');
|
|
$selfserve = new selfserve();
|
|
// Validate parameters
|
|
self::requireParameters(['lane_id']);
|
|
$lane_id = (int)$this->getParameter('lane_id');
|
|
self::requireType($lane_id, self::type_int());
|
|
self::requireMinValue($lane_id, 1);
|
|
$lane = $selfserve->lane($lane_id);
|
|
try {
|
|
$this->applyShellyTransportOverride($lane);
|
|
$lane->forceTurnOffRelay(selfserve_lane_relay::MACHINE_CLEANER);
|
|
$response->success([
|
|
'lane_id' => $lane_id,
|
|
'relay' => 'MACHINE_CLEANER',
|
|
'disabled' => true,
|
|
'transport' => $this->requestedShellyTransportOverride(),
|
|
]);
|
|
} catch (\Exception $e) {
|
|
$response->error('Failed to force disable MACHINE_CLEANER relay: ' . $e->getMessage(), 400);
|
|
}
|
|
}, [
|
|
'modules_selfserve_lane_force_machine_cleaner_disable' => 'Force disable MACHINE_CLEANER relay for a lane'
|
|
]);
|
|
|
|
/** Modules > Self Serve > Lane > Force > MACHINE enable (simulate started wash) */
|
|
$this->post('/modules/self-serve/lane/force/machine/enable', function () {
|
|
global $response;
|
|
self::requirePermission('modules_selfserve_lane_force_machine_enable');
|
|
$selfserve = new selfserve();
|
|
// Validate parameters
|
|
self::requireParameters(['lane_id']);
|
|
$lane_id = (int)$this->getParameter('lane_id');
|
|
self::requireType($lane_id, self::type_int());
|
|
self::requireMinValue($lane_id, 1);
|
|
$duration = null;
|
|
if ($this->isParametersSet(['duration'])) {
|
|
$duration = (int)$this->getParameter('duration');
|
|
self::requireMinValue($duration, 1);
|
|
}
|
|
$license_plate = $this->isParametersSet(['license_plate']) ? (string)$this->getParameter('license_plate') : null;
|
|
$lane = $selfserve->lane($lane_id);
|
|
try {
|
|
$this->applyShellyTransportOverride($lane);
|
|
// If lane looks idle/available, mark as occupied and in wash, set timers
|
|
if ($lane->getLaneStatus()->equals(\modules\selfserve\helpers\selfserve_lane_status::AVAILABLE)) {
|
|
$lane->setLaneStatus(\modules\selfserve\helpers\selfserve_lane_status::OCCUPIED);
|
|
}
|
|
if (!$lane->getLaneState()->equals(\modules\selfserve\helpers\selfserve_lane_state::IN_WASH)) {
|
|
$lane->setLaneState(\modules\selfserve\helpers\selfserve_lane_state::IN_WASH);
|
|
}
|
|
if ($license_plate !== null && $license_plate !== '') {
|
|
$lane->setLicensePlate($license_plate);
|
|
}
|
|
if ((int)$lane->getWashStartTime() <= 0) {
|
|
$lane->setWashStartTime(time());
|
|
}
|
|
// A started wash should always have cleaner relay enabled when configured.
|
|
try {
|
|
if (
|
|
!empty($lane->department_lane)
|
|
&& !empty($lane->department_lane->relay_machine_cleaner_id)
|
|
&& trim((string)$lane->department_lane->relay_machine_cleaner_id->value()) !== ''
|
|
) {
|
|
$lane->setMachineCleanerRelayStatusHard(true);
|
|
}
|
|
} catch (\Throwable $ignored) {}
|
|
// Force enable the machine relay (bypass gating)
|
|
$lane->forceTurnOnMachineRelay($duration);
|
|
// Reflect relay state explicitly
|
|
try {
|
|
$machine_relay_id = '';
|
|
if (!empty($lane->department_lane) && !empty($lane->department_lane->relay_machine_id)) {
|
|
$machine_relay_id = trim((string)$lane->department_lane->relay_machine_id->value());
|
|
}
|
|
$is_demo_machine_relay = str_starts_with(strtolower($machine_relay_id), 'demo-');
|
|
$lane->setLaneState(
|
|
$is_demo_machine_relay
|
|
? \modules\selfserve\helpers\selfserve_lane_state::MACHINE_RELAY_ON_QUEUED
|
|
: \modules\selfserve\helpers\selfserve_lane_state::MACHINE_RELAY_ON
|
|
);
|
|
} catch (\Throwable $ignored) {}
|
|
$response->success([
|
|
'lane_id' => $lane_id,
|
|
'forced' => true,
|
|
'machine' => 'ENABLED',
|
|
'duration' => $duration,
|
|
'status' => $lane->getLaneStatus()->name,
|
|
'state' => $lane->getLaneState()->name,
|
|
'wash_start_time' => $lane->getWashStartTime(),
|
|
'transport' => $this->requestedShellyTransportOverride(),
|
|
]);
|
|
} catch (\Exception $e) {
|
|
$response->error('Failed to force-enable MACHINE: ' . $e->getMessage(), 400);
|
|
}
|
|
}, [
|
|
'modules_selfserve_lane_force_machine_enable' => 'Force enable MACHINE relay and mark lane as in-wash (superusers only)'
|
|
]);
|
|
|
|
/** Modules > Self Serve > Lane > Force > MACHINE disable (simulate started wash without machine) */
|
|
$this->post('/modules/self-serve/lane/force/machine/disable', function () {
|
|
global $response;
|
|
self::requirePermission('modules_selfserve_lane_force_machine_disable');
|
|
$selfserve = new selfserve();
|
|
// Validate parameters
|
|
self::requireParameters(['lane_id']);
|
|
$lane_id = (int)$this->getParameter('lane_id');
|
|
self::requireType($lane_id, self::type_int());
|
|
self::requireMinValue($lane_id, 1);
|
|
$license_plate = $this->isParametersSet(['license_plate']) ? (string)$this->getParameter('license_plate') : null;
|
|
$lane = $selfserve->lane($lane_id);
|
|
try {
|
|
$this->applyShellyTransportOverride($lane);
|
|
// Ensure lane reflects an ongoing wash, but with machine off
|
|
if ($lane->getLaneStatus()->equals(\modules\selfserve\helpers\selfserve_lane_status::AVAILABLE)) {
|
|
$lane->setLaneStatus(\modules\selfserve\helpers\selfserve_lane_status::OCCUPIED);
|
|
}
|
|
if (!$lane->getLaneState()->equals(\modules\selfserve\helpers\selfserve_lane_state::IN_WASH)) {
|
|
$lane->setLaneState(\modules\selfserve\helpers\selfserve_lane_state::IN_WASH);
|
|
}
|
|
if ($license_plate !== null && $license_plate !== '') {
|
|
$lane->setLicensePlate($license_plate);
|
|
}
|
|
if ((int)$lane->getWashStartTime() <= 0) {
|
|
$lane->setWashStartTime(time());
|
|
}
|
|
// A started wash should always have cleaner relay enabled when configured.
|
|
try {
|
|
if (
|
|
!empty($lane->department_lane)
|
|
&& !empty($lane->department_lane->relay_machine_cleaner_id)
|
|
&& trim((string)$lane->department_lane->relay_machine_cleaner_id->value()) !== ''
|
|
) {
|
|
$lane->setMachineCleanerRelayStatusHard(true);
|
|
}
|
|
} catch (\Throwable $ignored) {}
|
|
// Turn off the machine relay (do not swallow errors)
|
|
$lane->forceTurnOffMachineRelay();
|
|
// Reflect relay state explicitly
|
|
try {
|
|
$machine_relay_id = '';
|
|
if (!empty($lane->department_lane) && !empty($lane->department_lane->relay_machine_id)) {
|
|
$machine_relay_id = trim((string)$lane->department_lane->relay_machine_id->value());
|
|
}
|
|
$is_demo_machine_relay = str_starts_with(strtolower($machine_relay_id), 'demo-');
|
|
$lane->setLaneState(
|
|
$is_demo_machine_relay
|
|
? \modules\selfserve\helpers\selfserve_lane_state::MACHINE_RELAY_OFF_QUEUED
|
|
: \modules\selfserve\helpers\selfserve_lane_state::MACHINE_RELAY_OFF
|
|
);
|
|
} catch (\Throwable $ignored) {}
|
|
$response->success([
|
|
'lane_id' => $lane_id,
|
|
'forced' => true,
|
|
'machine' => 'DISABLED',
|
|
'status' => $lane->getLaneStatus()->name,
|
|
'state' => $lane->getLaneState()->name,
|
|
'wash_start_time' => $lane->getWashStartTime(),
|
|
'transport' => $this->requestedShellyTransportOverride(),
|
|
]);
|
|
} catch (\Exception $e) {
|
|
$response->error('Failed to force-disable MACHINE: ' . $e->getMessage(), 400);
|
|
}
|
|
}, [
|
|
'modules_selfserve_lane_force_machine_disable' => 'Force disable MACHINE relay but keep lane as in-wash (superusers only)'
|
|
]);
|
|
}
|
|
|
|
private function requireInProgressWashDetailsAccess(int $lane_id): ?int
|
|
{
|
|
global $response;
|
|
|
|
$user = (new authentication())->get_user();
|
|
if (!$user) {
|
|
$response->error('Authentication failed. Invalid or missing token.', 401);
|
|
}
|
|
|
|
if (self::hasPermission('modules_selfserve_lane_wash_in_progress_view')) {
|
|
$department_lane = (new department_lanes_o())->select($lane_id);
|
|
if (!$department_lane->exists()) {
|
|
$response->error('Department lane not found', 404);
|
|
}
|
|
self::requireDepartmentAccess((string)$department_lane->department->value());
|
|
return null;
|
|
}
|
|
|
|
if (self::hasPermission('list_own_department_selfserve_vehicle_conditions')) {
|
|
$customer_number = $this->resolveEffectiveCustomerNumber();
|
|
if ($customer_number !== null && $customer_number > 0) {
|
|
return (int)$customer_number;
|
|
}
|
|
}
|
|
|
|
$this->emitForbidden([
|
|
'modules_selfserve_lane_wash_in_progress_view',
|
|
'list_own_department_selfserve_vehicle_conditions',
|
|
]);
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Customers poll all visible lanes to restore their own active wash. Keep that
|
|
* poll successful without exposing another customer's session details.
|
|
*
|
|
* @param array<string,mixed> $payload
|
|
* @return array<string,mixed>
|
|
*/
|
|
protected function scopeInProgressWashResponseForCustomer(array $payload, ?int $customer_number): array
|
|
{
|
|
if ($customer_number === null || $customer_number <= 0 || ($payload['in_progress'] ?? false) !== true) {
|
|
return $payload;
|
|
}
|
|
|
|
$session_customer_number = $this->extractInProgressWashCustomerNumber($payload);
|
|
if ($session_customer_number === $customer_number) {
|
|
return $payload;
|
|
}
|
|
|
|
return [
|
|
'lane_id' => (int)($payload['lane_id'] ?? 0),
|
|
'in_progress' => true,
|
|
'session' => null,
|
|
'customer' => null,
|
|
'vehicle' => null,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string,mixed> $payload
|
|
*/
|
|
private function extractInProgressWashCustomerNumber(array $payload): ?int
|
|
{
|
|
$candidates = [
|
|
$payload['session']['customer_number'] ?? null,
|
|
$payload['customer']['customer_number'] ?? null,
|
|
];
|
|
|
|
foreach ($candidates as $candidate) {
|
|
if ($candidate === null || $candidate === '') {
|
|
continue;
|
|
}
|
|
$customer_number = (int)$candidate;
|
|
if ($customer_number > 0) {
|
|
return $customer_number;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function requireMyActiveWashCustomerNumber(): int
|
|
{
|
|
global $response;
|
|
|
|
$user = (new authentication())->get_user();
|
|
if (!$user) {
|
|
$response->error('Authentication failed. Invalid or missing token.', 401);
|
|
}
|
|
|
|
if (!self::hasPermission(self::CUSTOMER_SELFSERVE_PERMISSION)) {
|
|
$this->emitForbidden([self::CUSTOMER_SELFSERVE_PERMISSION]);
|
|
}
|
|
|
|
$customer_number = $this->resolveEffectiveCustomerNumber();
|
|
if ($customer_number === null || $customer_number <= 0) {
|
|
$response->error('No customer number found for authenticated user.', 404);
|
|
}
|
|
|
|
return (int)$customer_number;
|
|
}
|
|
|
|
private function findLatestActiveSelfServeSessionForCustomer(int $customer_number): selfserve_wash_sessions_o
|
|
{
|
|
if ($customer_number <= 0) {
|
|
return new selfserve_wash_sessions_o();
|
|
}
|
|
|
|
$rows = (new selfserve_wash_sessions_o())->getFieldsWhere([
|
|
'customer_number' => $customer_number,
|
|
'completed_at' => null,
|
|
'deleted_at' => null,
|
|
], ['id', 'status']);
|
|
|
|
$active_statuses = $this->activeSelfServeWashSessionStatusValues();
|
|
$rows = array_values(array_filter(
|
|
$rows,
|
|
static fn(array $row): bool => in_array((string)($row['status'] ?? ''), $active_statuses, true)
|
|
));
|
|
if ($rows === []) {
|
|
return new selfserve_wash_sessions_o();
|
|
}
|
|
|
|
usort($rows, static fn(array $a, array $b): int => (int)$b['id'] <=> (int)$a['id']);
|
|
return (new selfserve_wash_sessions_o())->select((int)$rows[0]['id']);
|
|
}
|
|
|
|
/**
|
|
* @return array<int,string>
|
|
*/
|
|
private function activeSelfServeWashSessionStatusValues(): array
|
|
{
|
|
return array_map(
|
|
static fn(selfserve_wash_session_status $status): string => $status->value,
|
|
[
|
|
selfserve_wash_session_status::MACHINE_RELAY_ENABLED,
|
|
selfserve_wash_session_status::READY_FOR_MACHINE_START,
|
|
selfserve_wash_session_status::MACHINE_STARTED,
|
|
selfserve_wash_session_status::PENDING_QUESTIONS,
|
|
selfserve_wash_session_status::MACHINE_NOT_ALLOWED,
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array<string,mixed>
|
|
*/
|
|
private function buildActiveSelfServeSessionResponse(selfserve_wash_sessions_o $session): array
|
|
{
|
|
$lane_id = (int)$session->lane_id->value();
|
|
$customer_number = $session->customer_number->value() === null ? null : (int)$session->customer_number->value();
|
|
$vehicle_id = $session->vehicle_id->value() === null ? null : (int)$session->vehicle_id->value();
|
|
$session_reg = trim((string)$session->reg->value());
|
|
if ($session_reg === '') {
|
|
$session_reg = null;
|
|
}
|
|
|
|
$machine_start_triggered_at = $session->machine_start_triggered_at->value() === null ? null : (string)$session->machine_start_triggered_at->value();
|
|
$wash_started_at = $session->wash_started_at->value() === null ? null : (string)$session->wash_started_at->value();
|
|
if ($wash_started_at === null) {
|
|
$wash_started_at = $machine_start_triggered_at;
|
|
}
|
|
|
|
$machine_relay_enabled = (bool)$session->machine_relay_enabled->value();
|
|
$included_minutes = null;
|
|
if ($machine_relay_enabled) {
|
|
$included_minutes = (int)(new selfserve())->config->machine_wash_minutes_included->getVariableValue();
|
|
if ($included_minutes < 0) {
|
|
$included_minutes = 0;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'lane_id' => $lane_id,
|
|
'status' => (string)$session->status->value(),
|
|
'in_progress' => true,
|
|
'elapsed_minutes' => $session->getElapsedMinutes(),
|
|
'session' => [
|
|
'id' => (int)$session->id,
|
|
'status' => (string)$session->status->value(),
|
|
'department_id' => $session->department_id->value() === null ? null : (int)$session->department_id->value(),
|
|
'lane_id' => $lane_id,
|
|
'reg' => $session_reg,
|
|
'customer_number' => $customer_number,
|
|
'vehicle_id' => $vehicle_id,
|
|
'vehicle_type_id' => $session->vehicle_type_id->value() === null ? null : (int)$session->vehicle_type_id->value(),
|
|
'included_minutes' => $included_minutes ?? 0,
|
|
'machine_type_id' => $session->machine_type_id->value() === null ? null : (int)$session->machine_type_id->value(),
|
|
'machine_relay_enabled' => $machine_relay_enabled,
|
|
'machine_relay_enabled_at' => $session->machine_relay_enabled_at->value() === null ? null : (string)$session->machine_relay_enabled_at->value(),
|
|
'machine_start_triggered' => (bool)$session->machine_start_triggered->value(),
|
|
'machine_start_triggered_at' => $machine_start_triggered_at,
|
|
'wash_started_at' => $wash_started_at,
|
|
'created_at' => (string)$session->created_at->value(),
|
|
'updated_at' => $session->updated_at->value() === null ? null : (string)$session->updated_at->value(),
|
|
],
|
|
'customer' => $this->buildInProgressWashCustomerPayload($customer_number),
|
|
'vehicle' => $this->buildInProgressWashVehiclePayload($vehicle_id, $session_reg),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string,mixed>|null
|
|
*/
|
|
private function buildInProgressWashCustomerPayload(?int $customer_number): ?array
|
|
{
|
|
if ($customer_number === null || $customer_number <= 0) {
|
|
return null;
|
|
}
|
|
|
|
$customer_obj = (new users_o())->getUserByCustomerNumber($customer_number);
|
|
if ($customer_obj->exists()) {
|
|
return [
|
|
'id' => (int)$customer_obj->id,
|
|
'customer_number' => $customer_number,
|
|
'display_name' => $customer_obj->display_name->value() === null ? null : (string)$customer_obj->display_name->value(),
|
|
'email' => $customer_obj->email->value() === null ? null : (string)$customer_obj->email->value(),
|
|
'phone_country_code' => $customer_obj->phone_country_code->value() === null ? null : (int)$customer_obj->phone_country_code->value(),
|
|
'phone' => $customer_obj->phone->value() === null ? null : (string)$customer_obj->phone->value(),
|
|
];
|
|
}
|
|
|
|
return [
|
|
'id' => null,
|
|
'customer_number' => $customer_number,
|
|
'display_name' => null,
|
|
'email' => null,
|
|
'phone_country_code' => null,
|
|
'phone' => null,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string,mixed>|null
|
|
*/
|
|
private function buildInProgressWashVehiclePayload(?int $vehicle_id, ?string $reg): ?array
|
|
{
|
|
$vehicle_obj = null;
|
|
if ($vehicle_id !== null && $vehicle_id > 0) {
|
|
$tmp_vehicle = (new customer_vehicles_o())->select($vehicle_id);
|
|
if ($tmp_vehicle->exists()) {
|
|
$vehicle_obj = $tmp_vehicle;
|
|
}
|
|
}
|
|
if ($vehicle_obj === null && $reg !== null && trim($reg) !== '') {
|
|
$tmp_vehicle = (new customer_vehicles_o())->selectByPlate(trim($reg));
|
|
if ($tmp_vehicle->exists()) {
|
|
$vehicle_obj = $tmp_vehicle;
|
|
}
|
|
}
|
|
if ($vehicle_obj === null) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'id' => (int)$vehicle_obj->id,
|
|
'customer_id' => (int)$vehicle_obj->customer_id->value(),
|
|
'type' => (int)$vehicle_obj->type->value(),
|
|
'reg' => (string)$vehicle_obj->reg->value(),
|
|
'reference' => $vehicle_obj->reference->value() === null ? null : (string)$vehicle_obj->reference->value(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string,mixed> $status
|
|
* @param array<string,mixed> $extra
|
|
* @return array<string,mixed>
|
|
*/
|
|
private function buildRelayStatusResponse(int $lane_id, string $relay, array $status, array $extra = []): array
|
|
{
|
|
$on = (bool)($status['on'] ?? false);
|
|
$switch_status = is_array($status['status'] ?? null) ? $status['status'] : [];
|
|
if (!isset($switch_status['switch:0']) || !is_array($switch_status['switch:0'])) {
|
|
$switch_status['switch:0'] = [];
|
|
}
|
|
$switch_status['switch:0']['output'] = (bool)($switch_status['switch:0']['output'] ?? $on);
|
|
|
|
$payload = [
|
|
'lane_id' => $lane_id,
|
|
'relay' => $relay,
|
|
...$extra,
|
|
'relay_id' => (string)($status['relay_id'] ?? ''),
|
|
'online' => (bool)($status['online'] ?? false),
|
|
'on' => $on,
|
|
'status' => $switch_status,
|
|
'transport' => $this->requestedShellyTransportOverride(),
|
|
];
|
|
|
|
foreach (['binding', 'execution', 'raw'] as $metadata_key) {
|
|
if (isset($status[$metadata_key]) && is_array($status[$metadata_key]) && $status[$metadata_key] !== []) {
|
|
$payload[$metadata_key] = $status[$metadata_key];
|
|
}
|
|
}
|
|
|
|
return $payload;
|
|
}
|
|
|
|
/**
|
|
* @throws \Exception
|
|
*/
|
|
private function applyShellyTransportOverride(object $lane): void
|
|
{
|
|
$transport = $this->requestedShellyTransportOverride();
|
|
if ($transport === null) {
|
|
return;
|
|
}
|
|
|
|
if (!method_exists($lane, 'setShellyTransportOverride')) {
|
|
throw new \Exception('This lane does not support Shelly transport overrides.');
|
|
}
|
|
|
|
$lane->setShellyTransportOverride($transport);
|
|
}
|
|
|
|
/**
|
|
* @return array{0:bool,1:bool,2:bool} [customer self-serve allowed, active wash required, department active wash fallback allowed]
|
|
*/
|
|
private function customerSelfServeCommandAccessRequirements(selfserve_lane_command $command): array
|
|
{
|
|
return match ($command) {
|
|
selfserve_lane_command::START => [true, false, false],
|
|
selfserve_lane_command::STOP => [true, true, false],
|
|
selfserve_lane_command::OPEN_PROPERTY_ACCESS_GATE,
|
|
selfserve_lane_command::OPEN_PROPERTY_EXIT_GATE => [true, true, true],
|
|
selfserve_lane_command::RESERVE,
|
|
selfserve_lane_command::RELEASE,
|
|
selfserve_lane_command::RESET => [false, false, false],
|
|
};
|
|
}
|
|
|
|
private function requireSelfServeLaneDepartmentOrCustomerAccess(
|
|
selfserve_lane $lane,
|
|
int $customer_number,
|
|
bool $allow_customer_self_serve,
|
|
bool $requires_active_wash = false,
|
|
bool $allow_department_active_wash = false
|
|
): void {
|
|
$department_id = $this->departmentIdForLane($lane);
|
|
if ($department_id > 0 && $this->hasDepartmentAccess((string)$department_id)) {
|
|
return;
|
|
}
|
|
|
|
if ($allow_customer_self_serve) {
|
|
$customer_allowed = $requires_active_wash
|
|
? $this->canCustomerUseActiveOperationalSelfServeLane($lane, $customer_number, $allow_department_active_wash)
|
|
: $this->canCustomerUseSelfServeLane($lane, $customer_number);
|
|
|
|
if ($customer_allowed) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
$missing_permissions = $department_id > 0 ? ['department_access_' . $department_id] : [];
|
|
if ($allow_customer_self_serve) {
|
|
$missing_permissions[] = self::CUSTOMER_SELFSERVE_PERMISSION;
|
|
}
|
|
$this->emitForbidden($missing_permissions);
|
|
}
|
|
|
|
/**
|
|
* @param array<int,string> $permissions
|
|
*/
|
|
private function hasAllPermissions(array $permissions): bool
|
|
{
|
|
foreach ($permissions as $permission) {
|
|
if (!$this->hasPermission($permission)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param array<int,string> $elevated_permissions
|
|
*/
|
|
private function requireSelfServeLaneAccess(
|
|
selfserve_lane $lane,
|
|
int $customer_number,
|
|
array $elevated_permissions,
|
|
bool $requires_active_wash = false,
|
|
bool $requires_operational_lane = true
|
|
): void {
|
|
if ($this->hasAllPermissions($elevated_permissions)) {
|
|
return;
|
|
}
|
|
|
|
if ($requires_active_wash) {
|
|
$customer_allowed = $this->canCustomerUseActiveSelfServeLane($lane, $customer_number)
|
|
&& (!$requires_operational_lane || $this->isLaneSelfServeOperationallyEnabled($lane));
|
|
} else {
|
|
$customer_allowed = $this->canCustomerUseSelfServeLane($lane, $customer_number);
|
|
}
|
|
|
|
if ($customer_allowed) {
|
|
return;
|
|
}
|
|
|
|
$this->emitForbidden([...$elevated_permissions, self::CUSTOMER_SELFSERVE_PERMISSION]);
|
|
}
|
|
|
|
private function requireSelfServeLaneCommandPermission(
|
|
selfserve_lane $lane,
|
|
int $customer_number,
|
|
string $command_permission,
|
|
bool $allow_customer_self_serve,
|
|
bool $requires_active_wash = false,
|
|
bool $allow_department_active_wash = false
|
|
): void {
|
|
$elevated_permissions = [
|
|
'modules_selfserve_lane_command_execute',
|
|
$command_permission,
|
|
];
|
|
if ($this->hasAllPermissions($elevated_permissions)) {
|
|
return;
|
|
}
|
|
|
|
if ($allow_customer_self_serve && $this->isSelfServeModuleEnabled()) {
|
|
$customer_allowed = $requires_active_wash
|
|
? $this->canCustomerUseActiveOperationalSelfServeLane($lane, $customer_number, $allow_department_active_wash)
|
|
: $this->canCustomerUseSelfServeLane($lane, $customer_number);
|
|
|
|
if ($customer_allowed) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
$this->emitForbidden(
|
|
$allow_customer_self_serve
|
|
? [...$elevated_permissions, self::CUSTOMER_SELFSERVE_PERMISSION]
|
|
: $elevated_permissions
|
|
);
|
|
}
|
|
|
|
private function requireOperatorLaneCommandPermission(selfserve_lane $lane, string $command_permission): void
|
|
{
|
|
if (empty($lane->department_lane) || empty($lane->department_lane->department)) {
|
|
global $response;
|
|
$response->error('Lane department not found', 404);
|
|
}
|
|
|
|
self::requireDepartmentAccess((string)$lane->department_lane->department->value());
|
|
self::requirePermission('modules_selfserve_lane_command_execute');
|
|
self::requirePermission($command_permission);
|
|
}
|
|
|
|
private function requirePropertyGateCommandPermission(string $permission, selfserve_lane $lane, int $customer_number): void
|
|
{
|
|
$elevated_permissions = [
|
|
'modules_selfserve_lane_command_execute',
|
|
$permission,
|
|
];
|
|
if ($this->hasAllPermissions($elevated_permissions)) {
|
|
return;
|
|
}
|
|
|
|
if ($this->canCustomerUsePropertyGateForLane($lane, $customer_number)) {
|
|
return;
|
|
}
|
|
|
|
$this->emitForbidden([...$elevated_permissions, self::CUSTOMER_SELFSERVE_PERMISSION]);
|
|
}
|
|
|
|
protected function isSelfServeModuleEnabled(): bool
|
|
{
|
|
try {
|
|
return (bool)(new selfserve())->config->enabled->getVariableValue();
|
|
} catch (\Throwable) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
protected function canCustomerUseSelfServeLane(selfserve_lane $lane, int $customer_number): bool
|
|
{
|
|
return $customer_number > 0
|
|
&& $this->hasPermission(self::CUSTOMER_SELFSERVE_PERMISSION)
|
|
&& $this->isLaneSelfServeOperationallyEnabled($lane);
|
|
}
|
|
|
|
protected function canCustomerUseActiveSelfServeLane(selfserve_lane $lane, int $customer_number): bool
|
|
{
|
|
if ($customer_number <= 0 || !$this->hasPermission(self::CUSTOMER_SELFSERVE_PERMISSION)) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
if ((int)$lane->getCustomerNumber() === $customer_number) {
|
|
return true;
|
|
}
|
|
} catch (\Throwable) {
|
|
// Fall back to the persisted session lookup below.
|
|
}
|
|
|
|
$department_id = $this->departmentIdForLane($lane);
|
|
return $department_id > 0
|
|
&& $this->customerHasActiveSelfServeWashInDepartment($department_id, $customer_number);
|
|
}
|
|
|
|
protected function canCustomerUsePropertyGateForLane(selfserve_lane $lane, int $customer_number): bool
|
|
{
|
|
return $this->canCustomerUseActiveOperationalSelfServeLane($lane, $customer_number, true);
|
|
}
|
|
|
|
protected function canCustomerUseActiveOperationalSelfServeLane(
|
|
selfserve_lane $lane,
|
|
int $customer_number,
|
|
bool $allow_department_active_wash = false
|
|
): bool {
|
|
return $this->isLaneSelfServeOperationallyEnabled($lane)
|
|
&& (
|
|
$allow_department_active_wash
|
|
? $this->canCustomerUseActiveSelfServeLane($lane, $customer_number)
|
|
: $this->canCustomerUseActiveSelfServeLaneSession($lane, $customer_number)
|
|
);
|
|
}
|
|
|
|
protected function canCustomerUseActiveSelfServeLaneSession(selfserve_lane $lane, int $customer_number): bool
|
|
{
|
|
if ($customer_number <= 0 || !$this->hasPermission(self::CUSTOMER_SELFSERVE_PERMISSION)) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
if ((int)$lane->getCustomerNumber() === $customer_number) {
|
|
return true;
|
|
}
|
|
} catch (\Throwable) {
|
|
// Fall back to the persisted lane session lookup below.
|
|
}
|
|
|
|
$active_statuses = array_map(
|
|
static fn(selfserve_wash_session_status $status): string => $status->value,
|
|
[
|
|
selfserve_wash_session_status::MACHINE_RELAY_ENABLED,
|
|
selfserve_wash_session_status::READY_FOR_MACHINE_START,
|
|
selfserve_wash_session_status::MACHINE_STARTED,
|
|
selfserve_wash_session_status::PENDING_QUESTIONS,
|
|
selfserve_wash_session_status::MACHINE_NOT_ALLOWED,
|
|
]
|
|
);
|
|
|
|
$sessions = (new selfserve_wash_sessions_o())->getFieldsWhere([
|
|
'lane_id' => (int)$lane->id,
|
|
'customer_number' => $customer_number,
|
|
'completed_at' => null,
|
|
'deleted_at' => null,
|
|
], ['id', 'status']);
|
|
|
|
foreach ($sessions as $session) {
|
|
if (in_array((string)($session['status'] ?? ''), $active_statuses, true)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
protected function isLaneSelfServeOperationallyEnabled(selfserve_lane $lane): bool
|
|
{
|
|
try {
|
|
if (empty($lane->department_lane) || !$lane->department_lane->isSelfServeEnabled()) {
|
|
return false;
|
|
}
|
|
} catch (\Throwable) {
|
|
return false;
|
|
}
|
|
|
|
$department_id = $this->departmentIdForLane($lane);
|
|
if ($department_id <= 0) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
$department = (new departments_o())->select($department_id);
|
|
return $department->exists() && $department->getSelfServeEnabled();
|
|
} catch (\Throwable) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
protected function departmentIdForLane(selfserve_lane $lane): int
|
|
{
|
|
try {
|
|
return (int)$lane->department_lane?->department?->value();
|
|
} catch (\Throwable) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
protected function customerHasActiveSelfServeWashInDepartment(int $department_id, int $customer_number): bool
|
|
{
|
|
if ($department_id <= 0 || $customer_number <= 0) {
|
|
return false;
|
|
}
|
|
|
|
$active_statuses = array_map(
|
|
static fn(selfserve_wash_session_status $status): string => $status->value,
|
|
[
|
|
selfserve_wash_session_status::MACHINE_RELAY_ENABLED,
|
|
selfserve_wash_session_status::READY_FOR_MACHINE_START,
|
|
selfserve_wash_session_status::MACHINE_STARTED,
|
|
selfserve_wash_session_status::PENDING_QUESTIONS,
|
|
selfserve_wash_session_status::MACHINE_NOT_ALLOWED,
|
|
]
|
|
);
|
|
|
|
$sessions = (new selfserve_wash_sessions_o())->getFieldsWhere([
|
|
'department_id' => $department_id,
|
|
'customer_number' => $customer_number,
|
|
'completed_at' => null,
|
|
'deleted_at' => null,
|
|
], ['id', 'status']);
|
|
|
|
foreach ($sessions as $session) {
|
|
if (in_array((string)($session['status'] ?? ''), $active_statuses, true)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
foreach ((new department_lanes_o())->getDepartmentLanes($department_id) as $department_lane) {
|
|
try {
|
|
$lane = (new selfserve())->lane((int)$department_lane->id);
|
|
if ((int)$lane->getCustomerNumber() !== $customer_number) {
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
$lane->getLaneStatus()->equals(selfserve_lane_status::OCCUPIED)
|
|
|| $lane->getLaneState()->equals(selfserve_lane_state::IN_WASH)
|
|
) {
|
|
return true;
|
|
}
|
|
} catch (\Throwable) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param array<int,int> $task_ids
|
|
* @return array<int,array<int,string>>
|
|
*/
|
|
private function publishedConfigTaskServicesForLane(selfserve_lane $lane, array $task_ids): array
|
|
{
|
|
$department_id = $this->departmentIdForLane($lane);
|
|
if ($department_id <= 0 || $task_ids === []) {
|
|
return [];
|
|
}
|
|
|
|
$published = (new selfserve_config_versioning())->getPublishedConfig($department_id);
|
|
$config = is_array($published) ? (array)($published['config'] ?? []) : [];
|
|
$tasks = is_array($config['tasks'] ?? null) ? $config['tasks'] : [];
|
|
if ($tasks === []) {
|
|
return [];
|
|
}
|
|
|
|
$requested_task_ids = array_fill_keys(array_map(static fn($task_id): int => (int)$task_id, $task_ids), true);
|
|
$services_by_task_id = [];
|
|
foreach ($tasks as $task) {
|
|
if (!is_array($task)) {
|
|
continue;
|
|
}
|
|
|
|
$task_id = (int)($task['id'] ?? $task['task_id'] ?? 0);
|
|
if ($task_id <= 0 || !isset($requested_task_ids[$task_id])) {
|
|
continue;
|
|
}
|
|
|
|
$task_department_id = (int)($task['department'] ?? $task['department_id'] ?? 0);
|
|
if ($task_department_id !== 0 && $task_department_id !== $department_id) {
|
|
continue;
|
|
}
|
|
|
|
$task_lane_id = (int)($task['lane'] ?? $task['lane_id'] ?? 0);
|
|
if ($task_lane_id !== 0 && $task_lane_id !== (int)$lane->id) {
|
|
continue;
|
|
}
|
|
|
|
$services = $task['services'] ?? [];
|
|
if (is_string($services)) {
|
|
$decoded = json_decode($services, true);
|
|
$services = json_last_error() === JSON_ERROR_NONE && is_array($decoded) ? $decoded : [];
|
|
}
|
|
|
|
$services_by_task_id[$task_id] = is_array($services) ? $services : [];
|
|
}
|
|
|
|
return $services_by_task_id;
|
|
}
|
|
|
|
private function requestedShellyTransportOverride(): ?string
|
|
{
|
|
$transport = null;
|
|
foreach (['transport', 'test_transport', 'transport_mode'] as $parameter) {
|
|
if (self::isParametersSet([$parameter])) {
|
|
$transport = self::getParameter($parameter);
|
|
break;
|
|
}
|
|
}
|
|
|
|
$normalized = strtolower(trim((string)$transport));
|
|
if ($normalized === '') {
|
|
return null;
|
|
}
|
|
|
|
if ($normalized === 'gateway') {
|
|
return 'local';
|
|
}
|
|
|
|
if (!in_array($normalized, ['local', 'cloud'], true)) {
|
|
throw new \InvalidArgumentException('Invalid transport. Expected local, gateway, or cloud.');
|
|
}
|
|
|
|
return $normalized;
|
|
}
|
|
|
|
private function requestedRelayToggleAfter(int $default_seconds): int
|
|
{
|
|
$toggle_after = $default_seconds;
|
|
foreach (['toggle_after', 'toggleAfter', 'timer'] as $parameter) {
|
|
if (self::isParametersSet([$parameter])) {
|
|
$toggle_after = (int)self::getParameter($parameter);
|
|
break;
|
|
}
|
|
}
|
|
|
|
self::requireMinValue($toggle_after, 1);
|
|
self::requireMaxValue($toggle_after, self::MAX_GATE_OPEN_TOGGLE_AFTER_SECONDS);
|
|
return $toggle_after;
|
|
}
|
|
|
|
private function machineStatusAuditUserName(?object $user): ?string
|
|
{
|
|
if (!$user) {
|
|
return null;
|
|
}
|
|
|
|
foreach (['display_name', 'email'] as $property) {
|
|
if (!isset($user->{$property}) || !is_object($user->{$property}) || !method_exists($user->{$property}, 'value')) {
|
|
continue;
|
|
}
|
|
|
|
$value = trim((string)$user->{$property}->value());
|
|
if ($value !== '' && strtolower($value) !== 'unnamed') {
|
|
return $value;
|
|
}
|
|
}
|
|
|
|
if (isset($user->customer_number) && is_object($user->customer_number) && method_exists($user->customer_number, 'value')) {
|
|
$customer_number = (int)$user->customer_number->value();
|
|
if ($customer_number > 0) {
|
|
return 'Kunde ' . $customer_number;
|
|
}
|
|
}
|
|
|
|
return isset($user->id) ? 'Bruger #' . (int)$user->id : null;
|
|
}
|
|
|
|
private function requestedBoolean(string $parameter, bool $default = false): bool
|
|
{
|
|
if (!self::isParametersSet([$parameter])) {
|
|
return $default;
|
|
}
|
|
|
|
$value = self::getParameter($parameter);
|
|
if (is_bool($value)) {
|
|
return $value;
|
|
}
|
|
if (is_int($value)) {
|
|
return $value === 1;
|
|
}
|
|
$normalized = strtolower(trim((string)$value));
|
|
return in_array($normalized, ['1', 'true', 'yes', 'on'], true);
|
|
}
|
|
}
|