Files
api/services/nginx/app/modules/selfserve/selfserve.md
T

44 KiB

Self-Serve Module

This document explains the current self-serve implementation in services/nginx/app/modules/selfserve.

It is written for two audiences:

  • API consumers who need to configure or call the self-serve endpoints.
  • Engineers who need to extend the module without reverse-engineering the whole flow.

openapi.yaml in the repository root remains the contract source of truth. This guide explains how the module behaves at runtime and how the pieces fit together.

What The Module Does

The self-serve module lets a customer answer eligibility questions for a vehicle, derives the tasks that apply to the lane, enables the machine relay when the answers allow it, records the physical machine start through a webhook, bills the wash when the lane is stopped, and keeps a session summary with questions, answers, tasks, and lifecycle events.

At a high level:

  1. Shared self-serve questions are loaded.
  2. Machine-type-specific conditions, rules, and tasks are resolved for the lane.
  3. Vehicle answers are evaluated into condition results.
  4. If all visible questions are answered and an active task exposes the MACHINE service, the lane relay is enabled.
  5. When the physical machine button is pressed, /relay/button/press/post records the machine start.
  6. When the lane is stopped, the wash is invoiced and the latest open self-serve session is completed with the generated order id.

Main Flow

sequenceDiagram
    autonumber
    participant Customer
    participant API as "Department Self-Serve API"
    participant Flow as "selfserve_wash_flow"
    participant Lane as "selfserve_lane"
    participant Shelly as "Shelly relay"
    participant Scanner as "Plate scanner webhook"

    Customer->>API: GET /department/selfserve/vehicle/allowed?lane_id=12&reg=AB12345
    API->>Flow: previewVehicleEligibility()
    Flow-->>API: Questions, tasks, allowed=false/true
    API-->>Customer: Eligibility preview

    Customer->>API: POST /department/selfserve/vehicle/conditions
    API->>Flow: synchronizeSession()
    Flow->>Lane: setLaneCache(allowed_services)
    Flow->>Lane: turnOnRelay(MACHINE)
    Lane->>Shelly: switch(true)
    Flow-->>API: Session summary with MACHINE_RELAY_ENABLED event
    API-->>Customer: Updated summary

    Scanner->>API: POST /relay/button/press/post
    API->>Flow: recordMachineStartWebhook()
    Flow->>Lane: set status/state, reg, customer, wash timer
    Flow-->>API: Session summary with MACHINE_START_TRIGGERED event

    Customer->>API: POST /modules/self-serve/lane/command (STOP)
    API->>Lane: execute(STOP)
    Lane->>Lane: invoice() and open exit port
    Lane->>Flow: completeLatestSessionForLane(order_id)
    Flow-->>API: Session summary with SESSION_COMPLETED event
    API-->>Customer: Lane reset response

Architecture

flowchart LR
    Q["department_selfserve_questions<br/>shared questions preferred"] --> F["selfserve_wash_flow"]
    VC["department_selfserve_vehicle_conditions<br/>vehicle answers"] --> F
    C["department_selfserve_conditions<br/>machine-type or legacy"] --> E["selfserve_condition_evaluator"]
    R["department_selfserve_condition_rules"] --> E
    T["department_selfserve_tasks<br/>machine-type or legacy"] --> F
    MT["selfserve_machine_types"] --> L["department_lanes.machine_type_id"]
    E --> F
    L --> F
    F --> SL["selfserve_lane"]
    SL --> SH["Shelly machine relay"]
    F --> S["selfserve_wash_sessions"]
    F --> SA["selfserve_wash_session_answers"]
    F --> ST["selfserve_wash_session_tasks"]
    F --> SE["selfserve_wash_session_events"]

Domain Model

Entity Roles

Concept Main object/class Scope Purpose
Shared questions department_selfserve_questions_o Shared rows are preferred over legacy lane/product rows Defines the yes/no questions the customer must answer.
Conditions department_selfserve_conditions_o Prefer machine_type_id, else fall back to legacy department/lane/product rows Groups rules into named boolean gates.
Condition rules department_selfserve_condition_rules_o Attached to a condition Evaluates question answers or nested conditions.
Tasks department_selfserve_tasks_o Prefer machine_type_id, else fall back to legacy department/lane/product rows Drives the visible self-serve tasks and exposed services such as MACHINE.
Machine types selfserve_machine_types_o Reusable across lanes Lets multiple lanes share the same conditions and tasks.
Vehicle answers department_selfserve_vehicle_conditions_o Per department, lane, registration, and question Stores the customer's answers.
Wash session selfserve_wash_sessions_o Per lane and vehicle Tracks current wash lifecycle, timestamps, machine flags, and billing order.
Session answers selfserve_wash_session_answers_o Per session Snapshot of the answered visible questions.
Session tasks selfserve_wash_session_tasks_o Per session Snapshot of the active tasks and attached services/buttons.
Session events selfserve_wash_session_events_o Per session Audit trail for sync, relay enable, machine start, and completion.
Lane runtime selfserve_lane Per lane Applies lane status/state changes, relay control, billing, and STOP/RESET behavior.

Scoping Rules

  • Questions should now be defined as shared questions. selfserve_wash_flow::loadQuestions() first calls department_selfserve_questions_o::getSharedQuestions(). If shared questions exist, they are used for every department and lane.
  • Conditions and tasks are machine-type-first. If a lane has department_lanes.machine_type_id and matching rows exist, selfserve_wash_flow uses those rows and ignores the legacy lane/product rows.
  • Legacy fallback still exists. If a lane has no machine type or there are no machine-type-specific rows, the flow falls back to the old department/lane/product plus vehicle-type lookup.
  • Vehicle answers are still stored per department, lane, registration number, and question. Those answers feed both direct task gates and nested condition evaluation.

Runtime Status Tables

Wash Session Status

Status Meaning
PENDING_QUESTIONS At least one visible question has no answer yet.
READY_FOR_MACHINE_START The snapshot is eligible and ready to enable or start the machine.
MACHINE_NOT_ALLOWED All visible questions are answered, but the lane cannot start the machine.
MACHINE_RELAY_ENABLED The relay has already been enabled for the session.
MACHINE_STARTED The physical machine start webhook has been recorded.
COMPLETED The STOP flow completed the session, optionally with an order_id.

Wash Event Types

Event Meaning
SESSION_SYNCED A snapshot was written to the current session.
MACHINE_RELAY_ENABLED The relay was enabled because the snapshot allowed machine start.
MACHINE_START_TRIGGERED The physical machine start webhook was recorded.
SESSION_COMPLETED The latest open session for the lane was closed, usually from STOP.

Lane Status

Status Meaning
AVAILABLE Lane is free and ready.
OCCUPIED Lane is currently in use.
RESERVED Lane is reserved but not yet started.
FAULT Lane cannot be used until the fault is cleared.
MAINTENANCE Lane is intentionally unavailable.
CLOSED Lane is closed.

Lane State

State Meaning
IDLE Resting lane state.
ENTRANCE_PORT_OPEN_QUEUED Entrance gate open is queued.
ENTRANCE_PORT_OPEN Entrance gate is open.
MACHINE_RELAY_ON_QUEUED Machine relay enable is queued.
MACHINE_RELAY_ON Machine relay is on.
MACHINE_RELAY_OFF_QUEUED Machine relay disable is queued.
MACHINE_RELAY_OFF Machine relay is off.
IN_WASH The machine has started or the wash is in progress.
EXIT_PORT_OPEN_QUEUED Exit gate open is queued.
EXIT_PORT_OPEN Exit gate is open.
FAULT Lane fault state.
MAINTENANCE Maintenance state.
CLOSED Closed state.

Eligibility Rules

selfserve_wash_flow allows machine start only when all of these are true:

  1. Every visible question has an answer.
  2. The lane has relay_machine_id.
  3. At least one active task exposes the MACHINE service.

Visible questions are determined by question condition_id gates. Active tasks are determined by selfserve_condition_evaluator::taskGateSatisfied().

selfserve_condition_evaluator uses these rules:

  • Non-IS_TRUE_OR_ANY_TRUE rules are AND-ed together.
  • IS_TRUE_OR_ANY_TRUE rules are OR-ed together inside the same condition.
  • A task gate id is resolved as a condition result first. If there is no condition with that id, it falls back to the raw answer for that question id.

Supported rule types:

  • IS_TRUE
  • IS_FALSE
  • IS_SET
  • IS_TRUE_OR_NOT_SET
  • IS_FALSE_OR_NOT_SET
  • IS_TRUE_OR_ANY_TRUE

Supported rule object types:

  • question
  • condition

Public APIs

All /department/selfserve/* and /modules/self-serve/* routes require an authenticated user session and the listed permissions.

/relay/button/press/post is different: it uses plate-scanner authentication through authentication::get_plate_scanner(), which accepts the token from:

  • Authorization: Bearer <token>
  • query parameter token
  • POST body token
  • JSON body field token

/department/selfserve/questions

Purpose: manage shared or legacy self-serve questions.

Method Required params Permissions Notes
GET /department/selfserve/questions none, optional id, department, lane, product list_department_selfserve_questions, optional view_all_department_selfserve_questions Returns one question by id or a filtered paginated list.
POST /department/selfserve/questions question, description add_department_selfserve_questions Optional department, lane, product, condition_id, order_priority. Use 0/0/0 for shared questions.
PUT /department/selfserve/questions id edit_department_selfserve_questions Updates any subset of fields.
DELETE /department/selfserve/questions id delete_department_selfserve_questions Soft-deletes the question.

Typical failures:

  • 400 missing required fields
  • 403 user cannot access the target department
  • 404 question not found

/department/selfserve/conditions

Purpose: manage named condition gates used by tasks and question visibility.

Method Required params Permissions Notes
GET /department/selfserve/conditions none, optional id, department, lane, product, machine_type_id list_department_selfserve_conditions, optional view_all_department_selfserve_conditions Lists one or many conditions.
POST /department/selfserve/conditions name, description, and either machine_type_id or department+lane+product add_department_selfserve_conditions Optional parent condition_id supports nested trees.
PUT /department/selfserve/conditions id update_department_selfserve_conditions Can move a condition between machine types or legacy scopes.
DELETE /department/selfserve/conditions id delete_department_selfserve_conditions Soft-deletes the condition.

Typical failures:

  • 400 missing scope information
  • 403 user cannot access the source or target department
  • 404 condition not found

/department/selfserve/condition/rules

Purpose: define how a condition becomes true or false.

Method Required params Permissions Notes
GET /department/selfserve/condition/rules none, optional id, condition_id, type, object_type, object_id list_department_selfserve_condition_rules, optional view_all_department_selfserve_condition_rules Access is checked through the parent condition's department.
POST /department/selfserve/condition/rules condition_id, type, object_type, object_id, name, description add_department_selfserve_condition_rules object_type must be question or condition.
PUT /department/selfserve/condition/rules id update_department_selfserve_condition_rules Supports moving the rule to another condition.
DELETE /department/selfserve/condition/rules id delete_department_selfserve_condition_rules Soft-deletes the rule.

Typical failures:

  • 400 missing fields
  • 403 department access denied through the parent condition
  • 404 rule or target condition not found

/department/selfserve/tasks

Purpose: manage the task list shown after eligibility evaluation.

Method Required params Permissions Notes
GET /department/selfserve/tasks none, optional id, department, lane, product, condition_id, machine_type_id list_department_selfserve_tasks, optional view_all_department_selfserve_tasks Lists one or many tasks.
POST /department/selfserve/tasks task, description, and either machine_type_id or department+lane+product add_department_selfserve_tasks Optional condition_id, order_priority, services, buttons, dynamic_images_vehicle_type.
PUT /department/selfserve/tasks id edit_department_selfserve_tasks Updates any subset of fields.
DELETE /department/selfserve/tasks id delete_department_selfserve_tasks Soft-deletes the task.

Notes:

  • Route parameter name is condition_id. That value is used as the task gate id.
  • services accepts an array, JSON array string, or comma-separated string. Current enum cases are MACHINE and PROGRAM_PICKER.
  • If an active task does not expose MACHINE, the relay will not be enabled.

Typical failures:

  • 400 missing fields or invalid services format
  • 403 department access denied
  • 404 task not found

/department/selfserve/tasks/attachments*

Purpose: attach downloadable assets to self-serve tasks.

Method Required params Permissions Notes
GET /department/selfserve/tasks/attachments id list_department_selfserve_task_attachments Lists attachments for a task.
GET /department/selfserve/tasks/attachments/download task_id, attachment_id download_department_selfserve_task_attachments Returns a direct download URL.
POST /department/selfserve/tasks/attachments/upload task_id, base64_file, file_name add_department_selfserve_task_attachments Stores an attachment and links it to the task.
DELETE /department/selfserve/tasks/attachments task_id, attachment_id delete_department_selfserve_task_attachments Removes the linked attachment from the task.

Typical failures:

  • 400 missing fields
  • 403 department access denied
  • 404 task or attachment not found

/department/selfserve/vehicle/conditions

Purpose: create and manage the vehicle-specific answers that drive eligibility.

Method Required params Permissions Notes
GET /department/selfserve/vehicle/conditions none, optional id, department, customer_id, lane, reg, question list_department_selfserve_vehicle_conditions or list_own_department_selfserve_vehicle_conditions own_* access is restricted to the user's customer number.
POST /department/selfserve/vehicle/conditions department, lane, reg, question, value add_department_selfserve_vehicle_conditions or add_own_department_selfserve_vehicle_conditions Calls selfserve_wash_flow::synchronizeSession() and returns both the condition row and selfserve summary.
PUT /department/selfserve/vehicle/conditions id update_department_selfserve_vehicle_conditions or update_own_department_selfserve_vehicle_conditions Re-synchronizes the session after the update.
DELETE /department/selfserve/vehicle/conditions id delete_department_selfserve_vehicle_conditions or delete_own_department_selfserve_vehicle_conditions Deletes the answer and attempts to re-synchronize the session.

Typical failures:

  • 400 missing fields or invalid session
  • 403 permission denied, wrong department, or wrong customer ownership
  • 404 answer row not found

/department/selfserve/vehicle/allowed

Purpose: preview whether self-serve is currently allowed for a vehicle on a lane.

Method Required params Permissions Notes
GET /department/selfserve/vehicle/allowed lane_id, reg list_department_selfserve_vehicle_conditions or list_own_department_selfserve_vehicle_conditions Calls selfserve_wash_flow::previewVehicleEligibility() and returns questions, tasks, allowed services, and the current session if one exists. Own-permission customers may evaluate borrowed plates; saved answers only apply when scoped to the authenticated customer.

Typical failures:

  • 400 missing lane_id or reg
  • 403 permission denied, missing customer context, or wrong department
  • 404 lane not found

/department/selfserve/washes/summary

Purpose: inspect the summary of a self-serve wash.

Method Required params Permissions Notes
GET /department/selfserve/washes/summary either session_id, or lane_id plus reg list_department_selfserve_vehicle_conditions or list_own_department_selfserve_vehicle_conditions Returns session, lane, machine_type, questions, tasks, and events.

Typical failures:

  • 400 missing identifying parameters
  • 403 permission denied or wrong vehicle ownership
  • 404 session not found, or no session exists for lane and vehicle

/department/selfserve/machine-types

Purpose: manage reusable machine type profiles.

Method Required params Permissions Notes
GET /department/selfserve/machine-types none, optional id list_department_selfserve_machine_types Lists one or many machine types.
POST /department/selfserve/machine-types name add_department_selfserve_machine_types Optional description.
PUT /department/selfserve/machine-types id update_department_selfserve_machine_types Updates name and/or description.
DELETE /department/selfserve/machine-types id delete_department_selfserve_machine_types Soft-deletes the machine type.

Typical failures:

  • 400 missing name or empty update
  • 404 machine type not found

/modules/self-serve/lane/*

Purpose: operational lane control and relay management.

Method Required params Permissions Notes
GET /modules/self-serve/lane/status optional lane_id, default 1 modules_selfserve_lane_status_view Returns lane status, mode, state, wash timer, reg, and customer number.
POST /modules/self-serve/lane/command lane_id, command modules_selfserve_lane_command_execute plus command-specific permission, customer add_own_department_selfserve_vehicle_conditions for scoped START, or customer list_own_department_selfserve_vehicle_conditions for scoped STOP and property gate commands Valid commands: START, STOP, RESET, RESERVE, RELEASE, OPEN_PROPERTY_ACCESS_GATE, OPEN_PROPERTY_EXIT_GATE. Customer START requires an enabled self-serve lane. Customer STOP and property gate commands require the customer's active wash in the lane department.
POST /modules/self-serve/lane/services/allowed lane_id, optional task_ids modules_selfserve_lane_services_set_allowed, or customer add_own_department_selfserve_vehicle_conditions on an enabled self-serve lane Writes allowed service names to the lane cache. This is still read-from-visible-tasks only; it does not activate relays.
GET /modules/self-serve/lane/relay/machine_program_picker/status lane_id modules_selfserve_lane_relay_machine_program_picker_status_view Reads the Shelly MACHINE_PROGRAM_PICKER relay state (on/off) for the lane.
POST /modules/self-serve/lane/relay/machine_program_picker/set lane_id, on modules_selfserve_lane_relay_machine_program_picker_status_set Sets Shelly MACHINE_PROGRAM_PICKER relay state directly (on=true/false) and returns updated status.
GET /modules/self-serve/lane/relay/machine_cleaner/status lane_id modules_selfserve_lane_relay_machine_cleaner_status_view Reads the Shelly MACHINE_CLEANER relay state (on/off) for the lane.
POST /modules/self-serve/lane/relay/machine_cleaner/set lane_id, on modules_selfserve_lane_relay_machine_cleaner_status_set Sets Shelly MACHINE_CLEANER relay state directly (on=true/false) and returns updated status.
GET /modules/self-serve/lane/relay/machine/status lane_id modules_selfserve_lane_relay_machine_status_view Reads the Shelly MACHINE relay state (on/off) for the lane.
POST /modules/self-serve/lane/relay/machine/set lane_id, on modules_selfserve_lane_relay_machine_status_set Sets Shelly MACHINE relay state directly (on=true/false) and returns updated status.
POST /modules/self-serve/lane/relay/machine/enable lane_id, optional duration modules_selfserve_lane_relay_enable_machine, or customer list_own_department_selfserve_vehicle_conditions with an active wash in the lane department Manual enable, still gated by allowed services. Customer flow calls this only after START and only when MACHINE is allowed.
POST /modules/self-serve/lane/force/machine/enable lane_id, optional duration, optional license_plate modules_selfserve_lane_force_machine_enable Bypasses service gating and marks the lane as in wash.
POST /modules/self-serve/lane/force/machine/disable lane_id, optional license_plate modules_selfserve_lane_force_machine_disable Keeps the lane in wash but turns the machine relay off.

Shelly transport behavior:

  • The department variable shelly_transport_mode controls the default relay path. Missing or cloud keeps Shelly cloud as the default. gateway routes self-serve relay status, set, manual enable, force, START/STOP side effects, and gate relay operations through the edge gateway/local edge agent.
  • Local edge operation requires an active edge gateway plus an edge_gateway_relay_bindings row for each logical Shelly relay id. Each binding must resolve a bound device_id, local_ip, and channel.
  • Binding fallback stays unchanged: PREFER_LOCAL uses the local edge agent first and may fall back to cloud, LOCAL_ONLY fails instead of falling back, and CLOUD_ONLY bypasses local dispatch.
  • Operator diagnostics can add transport=local or transport=gateway to the /modules/self-serve/lane/* relay, gate, command, and allowed-services endpoints to force local-only dispatch. transport=cloud forces Shelly cloud. Regular customer flows do not send these overrides.
  • Relay status and set responses keep relay_id, online, on, and status.switch:0.output stable. Gateway responses may also include binding, execution, and raw metadata for diagnostics.

STOP flow details:

  • selfserve_lane_command::STOP requires lane status OCCUPIED.
  • Unless bypass is enabled, the lane customer number must match the current authenticated user's customer number.
  • STOP calls invoice(), opens the exit port, turns off the machine relay if self-serve is enabled for the department, completes the latest open self-serve session, and then resets the lane.

Customer start-wash release checklist:

  • Canary hardware validation must use the department and lane configured for the live Playwright/release credentials. Record the exact department_id and lane_id in the release notes before the live run.
  • Verify the self-serve module is enabled, department self-serve is enabled, the target lane has selfserve_enabled=1, relays and property gates are bound, minute billing product is configured, and the machine task exposes the MACHINE service before promoting canary.
  • Validate one supervised real-lane manual wash and, when configured, one machine wash before stable promotion. Confirm no relay changes before customer confirmation, active wash restore works across reloads, property gates open only during the active wash, STOP completes the session, and billing/order linkage is present.

Typical failures:

  • 400 invalid parameters
  • 403 missing permission
  • 404 lane not found
  • 403 from /relay/machine/enable if the MACHINE service is not currently allowed

/relay/button/press/post

Purpose: record the physical machine start trigger.

Method Required params Authentication Notes
GET /relay/button/press/post optional reg, optional lane_id depending on department lane count Plate-scanner auth Supported for devices that can only call GET.
POST /relay/button/press/post optional reg, optional lane_id depending on department lane count Plate-scanner auth Same behavior as GET.

Lane resolution behavior:

  • If lane_id is provided, the route verifies that the lane belongs to the scanner's department.
  • If lane_id is not provided and the department has exactly one lane, that lane is used automatically.
  • If lane_id is not provided and the department has multiple lanes, the request fails with 400.

Runtime behavior:

  • Calls selfserve_wash_flow::recordMachineStartWebhook().
  • Marks the session as machine-started.
  • Updates lane status/state to occupied and in-wash if needed.
  • Sets registration number, customer number, and wash start timestamp on the lane.

Typical failures:

  • 403 invalid plate scanner token
  • 400 missing lane_id in a multi-lane department
  • 403 lane does not belong to the scanner department
  • 404 no active self-serve wash session for the lane

Public Code Interfaces

selfserve_wash_flow and selfserve_wash_flow_i

Main orchestration class for the self-serve lifecycle.

Public methods:

Method Use it when Returns
previewVehicleEligibility(int $laneId, string $reg, ?int $customerNumber = null) You need a read-only eligibility preview without mutating state. Snapshot with questions, tasks, allowed_services, allowed, and optional current session.
synchronizeSession(int $laneId, string $reg, ?int $customerNumber = null, bool $activateMachine = true) Answers changed and you want session state, tasks, events, and relay enable to stay in sync. Full session summary.
recordMachineStartWebhook(int $laneId, ?string $reg = null, array $payload = []) The machine button or hardware event fired. Full session summary after the machine-start event.
getSessionSummary(int $sessionId) You have a session id already. Full session summary.
getLatestSessionSummary(int $laneId, string $reg) You want the latest session for a lane and vehicle. Full session summary.
completeLatestSessionForLane(int $laneId, ?string $reg = null, ?int $customerNumber = null, ?int $orderId = null, bool $disableRelays = true, ?int $subuserId = null) STOP has finished and you want to close the latest open session. Normal STOP passes false because it already disabled relays before opening the exit port. Driver flows pass subuserId so same-customer drivers close only their own session. Full summary, or null if no open session exists.

Key implementation details:

  • Calls selfserve_schema_bootstrap::ensureTables() in the constructor, so the session and machine-type tables are created lazily and idempotently at runtime.
  • Loads shared questions first, then machine-type conditions and tasks first, then legacy fallback only when needed.
  • Persists answer, task, and event snapshots on every sync.
  • Enables the Shelly machine relay automatically when the snapshot is eligible and activateMachine is true.

selfserve_condition_evaluator and selfserve_condition_evaluator_i

Pure evaluation layer for conditions and task gates.

Public methods:

Method Use it when
evaluate(array $conditions, array $rules, array $answers) You need a boolean result map keyed by condition id.
taskGateSatisfied(?int $gateId, array $conditionResults, array $answers) You need to decide whether a task should be active.

selfserve_lane

Runtime lane aggregate built from traits.

Important operational methods used by the module:

  • execute(selfserve_lane_command $command, selfserve_lane_command_arguments $arguments)
  • turnOnRelay(selfserve_lane_relay::MACHINE, ?int $duration = null)
  • turnOffRelay(selfserve_lane_relay::MACHINE)
  • forceTurnOnMachineRelay(?int $duration = null)
  • forceTurnOffMachineRelay()
  • getLaneStatus()
  • getLaneState()
  • setLaneStatus(...)
  • setLaneState(...)
  • setWashStartTime(...)
  • getElapsedWashTime()
  • setLicensePlate(...)
  • setCustomerNumber(...)
  • setLaneCache(...)
  • invoice()

Important enums:

  • selfserve_lane_command: START, STOP, RESET, RESERVE, RELEASE
  • selfserve_lane_status: AVAILABLE, OCCUPIED, RESERVED, FAULT, MAINTENANCE, CLOSED
  • selfserve_lane_state: IDLE, IN_WASH, relay states, gate states, and fault states
  • selfserve_lane_services: currently MACHINE and PROGRAM_PICKER

Machine Type And Wash Session Objects

selfserve_machine_types_o is a reusable configuration object. It stores the profile name and description and is attached to a lane through department_lanes.machine_type_id.

selfserve_wash_sessions_o is the lifecycle record. At behavior level it supports:

  • creating a new session with lane, machine type, customer, vehicle, and metadata
  • updating status
  • marking relay enabled
  • marking machine start triggered
  • marking completion with optional order_id
  • selecting the latest open or latest overall session for a lane and registration number

End-To-End Example: Happy Path

The example below shows the intended production flow with one shared question and one machine-type-specific task that exposes MACHINE.

1. Create a machine type

curl -X POST "$BASE_URL/department/selfserve/machine-types" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "HighPressureFoam",
    "description": "Shared profile for foam cannon lanes"
  }'

2. Associate the machine type with the lane

This is done through the existing department lanes route, not a self-serve-specific route.

curl -X PUT "$BASE_URL/department/lanes" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "id": 12,
    "machine_type_id": 3
  }'

3. Create a shared question

curl -X POST "$BASE_URL/department/selfserve/questions" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "department": 0,
    "lane": 0,
    "product": 0,
    "question": "Is the hydraulic lock engaged?",
    "description": "Required before machine start",
    "order_priority": 10
  }'

4. Create a machine-type-specific condition and rule

curl -X POST "$BASE_URL/department/selfserve/conditions" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "machine_type_id": 3,
    "name": "Machine may start",
    "description": "All mandatory safety checks passed"
  }'
curl -X POST "$BASE_URL/department/selfserve/condition/rules" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "condition_id": 14,
    "type": "IS_TRUE",
    "object_type": "question",
    "object_id": 21,
    "name": "Hydraulic lock is engaged",
    "description": "Question 21 must be answered true"
  }'

5. Create a machine-type-specific task that exposes MACHINE

curl -X POST "$BASE_URL/department/selfserve/tasks" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "machine_type_id": 3,
    "condition_id": 14,
    "task": "Press the machine start button",
    "description": "The relay is enabled automatically when all answers allow it.",
    "order_priority": 10,
    "services": ["MACHINE"]
  }'

6. Preview eligibility before answering

curl "$BASE_URL/department/selfserve/vehicle/allowed?lane_id=12&reg=AB12345" \
  -H "Authorization: Bearer $TOKEN"

Typical result before all answers are present:

{
  "success": true,
  "data": {
    "allowed": false,
    "all_visible_questions_answered": false,
    "machine_available": true,
    "questions": [
      {
        "id": 21,
        "question": "Is the hydraulic lock engaged?",
        "answer": null
      }
    ],
    "tasks": []
  }
}

7. Submit the answer

curl -X POST "$BASE_URL/department/selfserve/vehicle/conditions" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "department": 7,
    "lane": 12,
    "reg": "AB12345",
    "question": 21,
    "value": true,
    "customer_id": 100234
  }'

The POST response includes a selfserve summary. When the answer makes the vehicle eligible, synchronizeSession() will:

  • create or update the session
  • snapshot the answer and tasks
  • populate lane cache with allowed_services
  • enable the Shelly relay
  • log a MACHINE_RELAY_ENABLED event

8. Inspect the summary

curl "$BASE_URL/department/selfserve/washes/summary?lane_id=12&reg=AB12345" \
  -H "Authorization: Bearer $TOKEN"

Expected highlights:

  • session.status becomes MACHINE_RELAY_ENABLED
  • questions contains the answered question
  • tasks contains the machine start task
  • events contains both SESSION_SYNCED and MACHINE_RELAY_ENABLED

9. Record the physical machine start

curl -X POST "$BASE_URL/relay/button/press/post?token=$SCANNER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "lane_id": 12,
    "reg": "AB12345",
    "source": "shelly-button"
  }'

Expected highlights:

  • session.status becomes MACHINE_STARTED
  • the lane is OCCUPIED
  • the lane state is IN_WASH
  • machine_start_triggered_at is set
  • the summary includes a MACHINE_START_TRIGGERED event

10. Stop the lane and complete billing

curl -X POST "$BASE_URL/modules/self-serve/lane/command" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "lane_id": 12,
    "command": "STOP"
  }'

STOP invoices the elapsed wash time using the configured minute product, writes order_id to the session when available, logs SESSION_COMPLETED, and resets the lane.

End-To-End Example: Machine Not Allowed

This example shows the failure mode the UI usually needs to handle.

Preview shows the blocking reason

curl "$BASE_URL/department/selfserve/vehicle/allowed?lane_id=12&reg=AB12345" \
  -H "Authorization: Bearer $TOKEN"

Example response:

{
  "success": true,
  "data": {
    "allowed": false,
    "all_visible_questions_answered": true,
    "machine_available": false,
    "allowed_services": [],
    "questions": [
      {
        "id": 21,
        "question": "Is the hydraulic lock engaged?",
        "answer": true
      }
    ],
    "tasks": []
  }
}

Interpretation:

  • All questions are answered.
  • The machine is still not allowed because either relay_machine_id is missing on the lane or no active task exposed MACHINE.

If an operator tries to enable the relay manually anyway:

curl -X POST "$BASE_URL/modules/self-serve/lane/relay/machine/enable" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "lane_id": 12
  }'

The route returns 403 when the lane cache does not currently allow MACHINE.

PHP Examples

These examples assume you are running inside the app runtime after the normal bootstrap has loaded the classes and database connection.

Preview eligibility

<?php

use modules\selfserve\classes\selfserve_wash_flow;

$flow = new selfserve_wash_flow();
$preview = $flow->previewVehicleEligibility(12, 'AB12345', 100234);

var_dump([
    'allowed' => $preview['allowed'],
    'all_visible_questions_answered' => $preview['all_visible_questions_answered'],
    'questions' => $preview['questions'],
    'tasks' => $preview['tasks'],
]);

Synchronize a session after answers change

<?php

use modules\selfserve\classes\selfserve_wash_flow;
use objects\department_selfserve_vehicle_conditions_o;

$answers = new department_selfserve_vehicle_conditions_o();
$answers->add(
    7,
    12,
    'AB12345',
    21,
    true,
    100234
);

$flow = new selfserve_wash_flow();
$summary = $flow->synchronizeSession(12, 'AB12345', 100234);

var_dump([
    'session_id' => $summary['session']['id'],
    'status' => $summary['session']['status'],
    'events' => $summary['events'],
]);

Record the machine start webhook

<?php

use modules\selfserve\classes\selfserve_wash_flow;

$flow = new selfserve_wash_flow();
$summary = $flow->recordMachineStartWebhook(12, 'AB12345', [
    'source' => 'manual-test',
    'button' => 'start',
]);

var_dump([
    'status' => $summary['session']['status'],
    'machine_start_triggered' => $summary['session']['machine_start_triggered'],
    'events' => $summary['events'],
]);

Retrieve the latest wash summary

<?php

use modules\selfserve\classes\selfserve_wash_flow;

$flow = new selfserve_wash_flow();
$summary = $flow->getLatestSessionSummary(12, 'AB12345');

var_dump([
    'session' => $summary['session'],
    'questions' => $summary['questions'],
    'tasks' => $summary['tasks'],
    'events' => $summary['events'],
]);

Define a machine type and associate it with a lane

<?php

use objects\department_lanes_o;
use objects\selfserve_machine_types_o;

$machineType = (new selfserve_machine_types_o())->add(
    'HighPressureFoam',
    'Reusable profile for foam cannon lanes'
);

$lane = (new department_lanes_o())->select(12);
$lane->machine_type_id->set((int)$machineType->id);

var_dump([
    'machine_type_id' => $machineType->id,
    'lane_id' => $lane->id,
]);

Add a shared question and machine-type-specific conditions, rules, and tasks

<?php

use modules\selfserve\helpers\selfserve_condition_rule_object_type;
use modules\selfserve\helpers\selfserve_condition_rule_type;
use modules\selfserve\helpers\selfserve_lane_services;
use objects\department_selfserve_condition_rules_o;
use objects\department_selfserve_conditions_o;
use objects\department_selfserve_questions_o;
use objects\department_selfserve_tasks_o;

$question = (new department_selfserve_questions_o())->add(
    0,
    0,
    0,
    'Is the hydraulic lock engaged?',
    'Required before machine start',
    null,
    10
);

$condition = (new department_selfserve_conditions_o())->add(
    0,
    0,
    0,
    'Machine may start',
    'All mandatory safety checks passed',
    null,
    3
);

(new department_selfserve_condition_rules_o())->add(
    (int)$condition->id,
    selfserve_condition_rule_type::IS_TRUE->value,
    selfserve_condition_rule_object_type::QUESTION->value,
    (int)$question->id,
    'Hydraulic lock must be engaged',
    'The machine may only start when the shared question is true'
);

$task = (new department_selfserve_tasks_o())->add(
    0,
    0,
    0,
    (int)$condition->id,
    'Press the machine start button',
    'The relay is already enabled when this task becomes active',
    10,
    [selfserve_lane_services::MACHINE],
    null,
    null,
    3
);

var_dump([
    'question_id' => $question->id,
    'condition_id' => $condition->id,
    'task_id' => $task->id,
]);

Implementation Recipes

Add a new self-serve machine type

  1. Create a selfserve_machine_types row through /department/selfserve/machine-types or selfserve_machine_types_o.
  2. Set department_lanes.machine_type_id through /department/lanes or department_lanes_o.
  3. Add the machine-type-specific conditions.
  4. Add the condition rules that evaluate your questions or nested conditions.
  5. Add the machine-type-specific tasks.
  6. Ensure at least one active task exposes MACHINE if the lane should auto-enable the relay.

Configure shared questions

Use /department/selfserve/questions with:

  • department = 0
  • lane = 0
  • product = 0

Once shared questions exist, selfserve_wash_flow prefers them over legacy lane/product question rows.

Add machine-type-specific conditions, rules, and tasks

Recommended pattern:

  1. Keep questions shared.
  2. Define one or more conditions per machine type.
  3. Attach rules that reference shared questions or nested conditions.
  4. Gate tasks with condition_id.
  5. Put MACHINE on the task that should allow relay enable.

Understand webhook and STOP interaction

  • synchronizeSession() can enable the machine relay before the physical machine has started.
  • /relay/button/press/post is the authoritative machine-start signal. That is the point where the session becomes MACHINE_STARTED and the lane wash timer is initialized if needed.
  • STOP is the point where billing is finalized.
  • selfserve_lane_invoice_t::invoice() bills ceil(elapsedWashTime / 60) units of the configured minute product.
  • The current implementation creates the order with system user id 2285.
  • completeLatestSessionForLane() stores the final order_id on the session when STOP can provide it.

Billing prerequisites

Billing on STOP depends on:

  • self-serve minute product config being set
  • lane status being OCCUPIED
  • lane customer number being set
  • lane license plate being set
  • a positive elapsed wash time

Relevant config:

  • selfserve.enabled
  • selfserve.minute_product
  • department variable selfserve_enabled

Troubleshooting

allowed is always false

Check all three eligibility requirements:

  1. Every visible question must have an answer.
  2. The lane must have relay_machine_id.
  3. At least one active task must expose MACHINE.

Also check whether the machine-type-specific tasks and conditions exist for the lane's machine_type_id. If they do not, the flow may fall back to legacy lane/product data instead.

The relay is not enabled after an answer update

Common causes:

  • synchronizeSession() was called with activateMachine = false
  • the active task list does not expose MACHINE
  • the lane is CLOSED, MAINTENANCE, or FAULT
  • relay_machine_id is missing on the lane

Inspect the latest summary and look for:

  • allowed_services
  • session.machine_relay_enabled
  • a MACHINE_RELAY_ENABLED event

The machine button webhook returns 404

This means the route could not find an active self-serve session for the resolved lane.

Check:

  • the lane id resolved from the scanner department is correct
  • the vehicle registration matches the session registration
  • the session was synchronized before the button press

If you pass reg and no open session exists yet, recordMachineStartWebhook() will attempt a non-activating synchronize first. If that still cannot resolve a session, inspect the lane and answer data.

The summary does not show questions, tasks, or events you expected

Check:

  • whether you are reading by session_id or by lane_id plus reg
  • whether a newer session exists for the same lane and vehicle
  • whether the lane changed machine type after the session was created
  • whether the question or task was visible at the moment the session was synchronized

Remember that session answers and tasks are snapshots, not live joins.

The webhook fails in a multi-lane department

If the plate scanner belongs to a department with more than one lane, you must include lane_id in the webhook request.

STOP did not create an order_id on the session

Check:

  • the minute product configuration
  • lane customer number and registration number
  • that wash time was greater than zero
  • whether invoice() threw before completion

The STOP flow tries not to let relay or session-completion errors block the lane reset. If billing failed earlier, the lane may still reset without a final order_id.

Operational Notes

  • Runtime schema changes are additive and lazy through classes/selfserve_schema_bootstrap.php.
  • The session tables are safe to create idempotently from runtime flows because the project does not use a centralized migration runner.
  • Machine relay control is delegated to selfserve_lane_relay_controller_t, which resolves Shelly cloud vs. edge gateway transport per department.
  • Manual relay enable is still gated by the lane cache, while force enable and force disable bypass that gate.

Suggested Usage Pattern

For new implementations, the intended setup is:

  1. Define one or more reusable machine types.
  2. Attach each self-serve lane to a machine type.
  3. Keep questions shared.
  4. Put machine-specific logic in machine-type conditions, rules, and tasks.
  5. Let vehicle answer changes call synchronizeSession().
  6. Let the physical button or PLC call /relay/button/press/post.
  7. Let the normal STOP lane command complete billing and close the session.