- Describe architecture, domain models, eligibility rules, API endpoints, runtime status, and lane control. - Include end-to-end examples for happy paths and failure scenarios. - Provide PHP and Bash usage samples for integration. - Add detailed flow diagrams and entity definitions for clarity.
1036 lines
40 KiB
Markdown
1036 lines
40 KiB
Markdown
# 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
|
|
|
|
```mermaid
|
|
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®=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
|
|
|
|
```mermaid
|
|
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. The only current enum case is `MACHINE`.
|
|
- 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. |
|
|
|
|
Typical failures:
|
|
|
|
- `400` missing `lane_id` or `reg`
|
|
- `403` permission denied or wrong vehicle ownership
|
|
- `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 | Valid commands: `START`, `STOP`, `RESET`, `RESERVE`, `RELEASE`. |
|
|
| `POST /modules/self-serve/lane/services/allowed` | `lane_id`, optional `task_ids` | `modules_selfserve_lane_services_set_allowed` | Writes allowed service names to the lane cache. |
|
|
| `POST /modules/self-serve/lane/relay/machine/enable` | `lane_id`, optional `duration` | `modules_selfserve_lane_relay_enable_machine` | Manual enable, still gated by allowed services. |
|
|
| `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. |
|
|
|
|
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.
|
|
|
|
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)` | STOP has finished and you want to close the latest open 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 only `MACHINE`
|
|
|
|
### 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
|
|
|
|
```bash
|
|
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.
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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"
|
|
}'
|
|
```
|
|
|
|
```bash
|
|
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`
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
curl "$BASE_URL/department/selfserve/vehicle/allowed?lane_id=12®=AB12345" \
|
|
-H "Authorization: Bearer $TOKEN"
|
|
```
|
|
|
|
Typical result before all answers are present:
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
curl "$BASE_URL/department/selfserve/washes/summary?lane_id=12®=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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
curl "$BASE_URL/department/selfserve/vehicle/allowed?lane_id=12®=AB12345" \
|
|
-H "Authorization: Bearer $TOKEN"
|
|
```
|
|
|
|
Example response:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```bash
|
|
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
|
|
<?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
|
|
<?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
|
|
<?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
|
|
<?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
|
|
<?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
|
|
<?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 the Shelly module through `selfserve_lane_relay_controller_t`.
|
|
- 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.
|