Update self-serve permissions and enhance UI components for customer interactions
This commit is contained in:
@@ -14,7 +14,7 @@ interface selfserve_wash_flow_i
|
||||
|
||||
public function getLatestSessionSummary(int $laneId, string $reg): array;
|
||||
|
||||
public function completeLatestSessionForLane(int $laneId, ?string $reg = null, ?int $customerNumber = null, ?int $orderId = null, bool $disableRelays = true): ?array;
|
||||
public function completeLatestSessionForLane(int $laneId, ?string $reg = null, ?int $customerNumber = null, ?int $orderId = null, bool $disableRelays = true, ?int $subuserId = null): ?array;
|
||||
|
||||
public function forceStopLane(int $laneId, ?int $sessionId = null, bool $bill = false, ?string $reason = null, ?int $userId = null): array;
|
||||
}
|
||||
|
||||
@@ -356,8 +356,8 @@ 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, or customer `list_own_department_selfserve_vehicle_conditions` for scoped `START`, 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 `list_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. |
|
||||
| `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. |
|
||||
@@ -439,7 +439,7 @@ Public methods:
|
||||
| `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)` | 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. | Full summary, or `null` if no open session exists. |
|
||||
| `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:
|
||||
|
||||
|
||||
@@ -120,14 +120,15 @@ trait selfserve_lane_command_t
|
||||
/**
|
||||
* Machine-wash billing is based on the physical machine ON signal, not selector relay status.
|
||||
*/
|
||||
protected function hasMachineStartSignalForStop(): bool
|
||||
protected function hasMachineStartSignalForStop(?int $subuser_id = null): bool
|
||||
{
|
||||
try {
|
||||
$customerNumber = method_exists($this, 'getCustomerNumber') ? (int)$this->getCustomerNumber() : null;
|
||||
return (new selfserve_wash_flow())->hasMachineStartTriggeredForLane(
|
||||
(int)$this->id,
|
||||
method_exists($this, 'getLicensePlate') ? ($this->getLicensePlate() ?: null) : null,
|
||||
$customerNumber !== null && $customerNumber > 0 ? $customerNumber : null
|
||||
$customerNumber !== null && $customerNumber > 0 ? $customerNumber : null,
|
||||
$subuser_id
|
||||
);
|
||||
} catch (\Throwable) {
|
||||
return false;
|
||||
@@ -240,7 +241,9 @@ trait selfserve_lane_command_t
|
||||
$snapshot = (new selfserve_wash_flow())->previewVehicleEligibility(
|
||||
(int)$this->id,
|
||||
$reg,
|
||||
$customerNumber !== null && $customerNumber > 0 ? $customerNumber : null
|
||||
$customerNumber !== null && $customerNumber > 0 ? $customerNumber : null,
|
||||
null,
|
||||
['subuser_id' => $arguments?->subuser_id]
|
||||
);
|
||||
|
||||
return (bool)($snapshot['allowed'] ?? false);
|
||||
@@ -531,7 +534,7 @@ trait selfserve_lane_command_t
|
||||
/**
|
||||
* Finalize any active self-serve wash session before resetting lane state.
|
||||
*/
|
||||
protected function completeLatestSessionForStop(): void
|
||||
protected function completeLatestSessionForStop(?int $subuser_id = null): void
|
||||
{
|
||||
try {
|
||||
(new \modules\selfserve\classes\selfserve_wash_flow())->completeLatestSessionForLane(
|
||||
@@ -539,7 +542,8 @@ trait selfserve_lane_command_t
|
||||
$this->getLicensePlate() ?: null,
|
||||
$this->getCustomerNumber() ?: null,
|
||||
method_exists($this, 'getLastInvoiceOrderId') ? $this->getLastInvoiceOrderId() : null,
|
||||
false
|
||||
false,
|
||||
$subuser_id
|
||||
);
|
||||
} catch (\Throwable) {
|
||||
// Session completion must not block STOP flow.
|
||||
@@ -711,7 +715,7 @@ trait selfserve_lane_command_t
|
||||
throw new \InvalidArgumentException("Customer number mismatch: Lane customer number " . $this->getCustomerNumber() . " does not match argument customer number " . $arguments->customer_number);
|
||||
}
|
||||
// Snapshot the physical machine ON signal before session completion/reset.
|
||||
$machine_start_triggered = $this->hasMachineStartSignalForStop();
|
||||
$machine_start_triggered = $this->hasMachineStartSignalForStop($arguments->subuser_id);
|
||||
// Turn off relays before any configured or default exit gate opens.
|
||||
$this->turnOffRelaysAfterStop();
|
||||
$this->runPublishedStudioActions(
|
||||
@@ -733,7 +737,7 @@ trait selfserve_lane_command_t
|
||||
// Only bill the machine wash product when the physical machine start signal was recorded.
|
||||
$this->addVehicleTypeProductToInvoiceIfNeeded($machine_start_triggered);
|
||||
// Finalize any active self-serve wash session for this lane
|
||||
$this->completeLatestSessionForStop();
|
||||
$this->completeLatestSessionForStop($arguments->subuser_id);
|
||||
// Reset the lane
|
||||
self::execute(selfserve_lane_command::RESET, new selfserve_lane_command_arguments());
|
||||
break;
|
||||
|
||||
@@ -260,8 +260,9 @@ trait selfserve_lane_invoice_t
|
||||
?int $draft_customer_number,
|
||||
?selfserve_lane_command_arguments $arguments = null
|
||||
): array {
|
||||
$session = $this->findOpenSelfServeSessionForAttachment($billing_customer_number);
|
||||
$subuser_id = $arguments?->subuser_id;
|
||||
$session = $this->findOpenSelfServeSessionForAttachment($billing_customer_number, $arguments?->subuser_id);
|
||||
$session_subuser_id = $session?->subuser_id->value() === null ? null : (int)$session->subuser_id->value();
|
||||
$subuser_id = $arguments?->subuser_id ?? $session_subuser_id;
|
||||
|
||||
return [
|
||||
'type' => attachment_content::OTHER_TYPE_SELF_SERVE_WASH,
|
||||
@@ -282,7 +283,7 @@ trait selfserve_lane_invoice_t
|
||||
];
|
||||
}
|
||||
|
||||
protected function findOpenSelfServeSessionForAttachment(int $billing_customer_number): ?selfserve_wash_sessions_o
|
||||
protected function findOpenSelfServeSessionForAttachment(int $billing_customer_number, ?int $subuser_id = null): ?selfserve_wash_sessions_o
|
||||
{
|
||||
$license_plate = trim((string)$this->getLicensePlate());
|
||||
if ($license_plate === '') {
|
||||
@@ -293,7 +294,8 @@ trait selfserve_lane_invoice_t
|
||||
$session = (new selfserve_wash_sessions_o())->selectLatestOpenByLaneAndReg(
|
||||
(int)$this->id,
|
||||
selfserve::standardize_registration($license_plate),
|
||||
$billing_customer_number > 0 ? $billing_customer_number : null
|
||||
$billing_customer_number > 0 ? $billing_customer_number : null,
|
||||
$subuser_id
|
||||
);
|
||||
return $session->exists() ? $session : null;
|
||||
} catch (\Throwable) {
|
||||
|
||||
@@ -9851,6 +9851,9 @@ paths:
|
||||
customer_number:
|
||||
type: integer
|
||||
nullable: true
|
||||
subuser_id:
|
||||
type: integer
|
||||
nullable: true
|
||||
vehicle_id:
|
||||
type: integer
|
||||
nullable: true
|
||||
@@ -9903,6 +9906,18 @@ paths:
|
||||
phone:
|
||||
type: string
|
||||
nullable: true
|
||||
subuser:
|
||||
type: object
|
||||
nullable: true
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
nullable: true
|
||||
username:
|
||||
type: string
|
||||
nullable: true
|
||||
vehicle:
|
||||
type: object
|
||||
nullable: true
|
||||
@@ -9930,7 +9945,8 @@ paths:
|
||||
summary: Get the authenticated customer's active self-serve wash
|
||||
description: |
|
||||
Returns the latest active self-serve wash for the authenticated customer,
|
||||
without requiring the frontend to know or poll a lane id.
|
||||
without requiring the frontend to know or poll a lane id. Driver sessions
|
||||
are scoped to the authenticated subuser under the selected customer number.
|
||||
operationId: getMyActiveSelfServeWash
|
||||
responses:
|
||||
'200':
|
||||
@@ -9951,9 +9967,16 @@ paths:
|
||||
session:
|
||||
type: object
|
||||
nullable: true
|
||||
properties:
|
||||
subuser_id:
|
||||
type: integer
|
||||
nullable: true
|
||||
customer:
|
||||
type: object
|
||||
nullable: true
|
||||
subuser:
|
||||
type: object
|
||||
nullable: true
|
||||
vehicle:
|
||||
type: object
|
||||
nullable: true
|
||||
@@ -10088,8 +10111,9 @@ paths:
|
||||
Property gate commands (`OPEN_PROPERTY_ACCESS_GATE`, `OPEN_PROPERTY_EXIT_GATE`) are also supported here.
|
||||
Property gate commands require the matching explicit command permissions.
|
||||
Operator callers require the base command permission plus the command-specific permission. Authenticated
|
||||
customers with `list_own_department_selfserve_vehicle_conditions` may send `START` on enabled self-serve
|
||||
lanes. Customer `STOP` requires the customer's active self-serve wash in the target department.
|
||||
customers with `add_own_department_selfserve_vehicle_conditions` may send `START` on enabled self-serve
|
||||
lanes. Customer `STOP` requires `list_own_department_selfserve_vehicle_conditions` and the customer's active
|
||||
self-serve wash in the target department.
|
||||
operationId: sendSelfServeLaneCommand
|
||||
requestBody:
|
||||
required: true
|
||||
@@ -10156,7 +10180,7 @@ paths:
|
||||
derived from the tasks currently shown to the user after answering the self-serve questions.
|
||||
This endpoint does not activate anything by itself; it only sets what is allowed to be activated.
|
||||
Operator callers require `modules_selfserve_lane_services_set_allowed`; authenticated customers with
|
||||
`list_own_department_selfserve_vehicle_conditions` may update their enabled self-serve lane before
|
||||
`add_own_department_selfserve_vehicle_conditions` may update their enabled self-serve lane before
|
||||
confirming a wash start.
|
||||
operationId: setSelfServeLaneAllowedServices
|
||||
requestBody:
|
||||
|
||||
@@ -119,7 +119,7 @@ class departmentSelfserveVehicleConditionsRoute
|
||||
*/
|
||||
$this->get('/department/selfserve/vehicle/allowed', function () {
|
||||
global $response;
|
||||
[$user, $actor_id] = $this->getAuthenticatedSelfServePrincipal();
|
||||
[$user, $actor_id, $subuser_id] = $this->getAuthenticatedSelfServePrincipal();
|
||||
$own_permission = self::definePermission('list_own_department_selfserve_vehicle_conditions', subusers_permission_node_key::SELFSERVE_LIST);
|
||||
|
||||
$has_global = $user !== null && $this->hasPermission('list_department_selfserve_vehicle_conditions');
|
||||
@@ -147,11 +147,14 @@ class departmentSelfserveVehicleConditionsRoute
|
||||
if ($vehicle_type_id !== null) {
|
||||
$flow->synchronizeSession($lane_id, $reg, $customer_number, false, $vehicle_type_id, false, [
|
||||
'create_session' => false,
|
||||
'subuser_id' => $subuser_id,
|
||||
]);
|
||||
}
|
||||
|
||||
(new logs_o())->add('department_selfserve_vehicle_conditions', (int)$lane->department->value(), 1, $actor_id, 'CHECK_VEHICLE_ALLOWED', 'User checked self-serve eligibility for lane ' . $lane_id . ' and vehicle ' . $reg);
|
||||
$response->success($flow->previewVehicleEligibility($lane_id, $reg, $customer_number, $vehicle_type_id));
|
||||
$response->success($flow->previewVehicleEligibility($lane_id, $reg, $customer_number, $vehicle_type_id, [
|
||||
'subuser_id' => $subuser_id,
|
||||
]));
|
||||
}, [
|
||||
'list_department_selfserve_vehicle_conditions' => 'Check whether self-serve is allowed for a specific vehicle',
|
||||
'list_own_department_selfserve_vehicle_conditions' => 'Check whether self-serve is allowed for a customer-scoped vehicle'
|
||||
@@ -162,7 +165,7 @@ class departmentSelfserveVehicleConditionsRoute
|
||||
*/
|
||||
$this->get('/department/selfserve/washes/summary', function () {
|
||||
global $response;
|
||||
[$user] = $this->getAuthenticatedSelfServePrincipal();
|
||||
[$user, $_actor_id, $subuser_id] = $this->getAuthenticatedSelfServePrincipal();
|
||||
$own_permission = self::definePermission('list_own_department_selfserve_vehicle_conditions', subusers_permission_node_key::SELFSERVE_LIST);
|
||||
|
||||
$has_global = $user !== null && $this->hasPermission('list_department_selfserve_vehicle_conditions');
|
||||
@@ -192,7 +195,10 @@ class departmentSelfserveVehicleConditionsRoute
|
||||
false,
|
||||
$vehicle_type_id,
|
||||
false,
|
||||
['create_session' => false]
|
||||
[
|
||||
'create_session' => false,
|
||||
'subuser_id' => $subuser_id,
|
||||
]
|
||||
);
|
||||
if (!empty($refreshed_summary['session']['id'])) {
|
||||
$summary = $refreshed_summary;
|
||||
@@ -215,6 +221,7 @@ class departmentSelfserveVehicleConditionsRoute
|
||||
if ($vehicle_type_id !== null) {
|
||||
$summary = $flow->synchronizeSession($lane_id, $reg, $customer_number, false, $vehicle_type_id, false, [
|
||||
'create_session' => false,
|
||||
'subuser_id' => $subuser_id,
|
||||
]);
|
||||
if (empty($summary['session']['id'])) {
|
||||
try {
|
||||
@@ -241,7 +248,7 @@ class departmentSelfserveVehicleConditionsRoute
|
||||
*/
|
||||
$this->post('/department/selfserve/vehicle/conditions', function () {
|
||||
global $response;
|
||||
[$user, $actor_id] = $this->getAuthenticatedSelfServePrincipal();
|
||||
[$user, $actor_id, $subuser_id] = $this->getAuthenticatedSelfServePrincipal();
|
||||
$own_permission = self::definePermission('add_own_department_selfserve_vehicle_conditions', subusers_permission_node_key::SELFSERVE_ADD);
|
||||
|
||||
$has_global = $user !== null && $this->hasPermission('add_department_selfserve_vehicle_conditions');
|
||||
@@ -281,7 +288,9 @@ class departmentSelfserveVehicleConditionsRoute
|
||||
try {
|
||||
$condition_o = new department_selfserve_vehicle_conditions_o();
|
||||
$condition_o->add($department, $lane, $reg, $question, $value, $customer_id);
|
||||
$summary = $this->getWashFlow()->synchronizeSession($lane, $reg, $customer_id, $activate_machine, $vehicle_type_id, $sync_relay_state);
|
||||
$summary = $this->getWashFlow()->synchronizeSession($lane, $reg, $customer_id, $activate_machine, $vehicle_type_id, $sync_relay_state, [
|
||||
'subuser_id' => $subuser_id,
|
||||
]);
|
||||
(new logs_o())->add('department_selfserve_vehicle_conditions', 'global', 1, $actor_id, 'ADD_VEHICLE_CONDITION', 'User added department self-serve vehicle condition ' . $condition_o->id);
|
||||
$response->success([
|
||||
'condition' => $condition_o->asArray(),
|
||||
@@ -469,12 +478,12 @@ class departmentSelfserveVehicleConditionsRoute
|
||||
$auth = new authentication();
|
||||
$user = $auth->get_user();
|
||||
if ($user !== false) {
|
||||
return [$user, (int)$user->id];
|
||||
return [$user, (int)$user->id, null];
|
||||
}
|
||||
|
||||
$subuser = $auth->get_subuser();
|
||||
if ($subuser !== false) {
|
||||
return [null, (int)$subuser->id];
|
||||
return [null, (int)$subuser->id, (int)$subuser->id];
|
||||
}
|
||||
|
||||
$response->error('Invalid session', 400);
|
||||
|
||||
@@ -29,6 +29,7 @@ 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\subusers_o;
|
||||
use objects\users_o;
|
||||
use traits\route_t;
|
||||
|
||||
@@ -38,6 +39,7 @@ class moduleSelfServeRoute
|
||||
|
||||
private const MAX_GATE_OPEN_TOGGLE_AFTER_SECONDS = 5;
|
||||
private const CUSTOMER_SELFSERVE_PERMISSION = 'list_own_department_selfserve_vehicle_conditions';
|
||||
private const CUSTOMER_SELFSERVE_USE_PERMISSION = 'add_own_department_selfserve_vehicle_conditions';
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
@@ -262,6 +264,7 @@ class moduleSelfServeRoute
|
||||
'status' => $lane_state->name,
|
||||
'reg' => $runtime_reg,
|
||||
'customer_number' => $runtime_customer_number,
|
||||
'subuser_id' => null,
|
||||
'vehicle_id' => $vehicle['id'] ?? null,
|
||||
'vehicle_type_id' => $vehicle['type'] ?? null,
|
||||
'included_minutes' => $included_minutes,
|
||||
@@ -273,14 +276,16 @@ class moduleSelfServeRoute
|
||||
'wash_started_at' => $wash_started_at,
|
||||
'created_at' => null,
|
||||
'updated_at' => null,
|
||||
],
|
||||
'customer' => $customer,
|
||||
'vehicle' => $vehicle,
|
||||
], $customer_scope));
|
||||
],
|
||||
'customer' => $customer,
|
||||
'subuser' => null,
|
||||
'vehicle' => $vehicle,
|
||||
], $customer_scope));
|
||||
return;
|
||||
}
|
||||
|
||||
$customer_number = $session->customer_number->value() === null ? null : (int)$session->customer_number->value();
|
||||
$subuser_id = $session->subuser_id->value() === null ? null : (int)$session->subuser_id->value();
|
||||
$vehicle_id = $session->vehicle_id->value() === null ? null : (int)$session->vehicle_id->value();
|
||||
$session_reg = trim((string)$session->reg->value());
|
||||
if ($session_reg === '') {
|
||||
@@ -318,6 +323,7 @@ class moduleSelfServeRoute
|
||||
'status' => (string)$session->status->value(),
|
||||
'reg' => $session_reg,
|
||||
'customer_number' => $customer_number,
|
||||
'subuser_id' => $subuser_id,
|
||||
'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,
|
||||
@@ -331,12 +337,13 @@ class moduleSelfServeRoute
|
||||
'updated_at' => $session->updated_at->value() === null ? null : (string)$session->updated_at->value(),
|
||||
],
|
||||
'customer' => $customer,
|
||||
'subuser' => $this->buildInProgressWashSubuserPayload($subuser_id),
|
||||
'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',
|
||||
'add_own_department_selfserve_vehicle_conditions' => 'Use in-progress self-serve wash details for the authenticated customer',
|
||||
]
|
||||
);
|
||||
|
||||
@@ -344,8 +351,11 @@ class moduleSelfServeRoute
|
||||
$this->get('/modules/self-serve/lane/wash/my-active-wash', function () {
|
||||
global $response;
|
||||
|
||||
$customer_number = $this->requireMyActiveWashCustomerNumber();
|
||||
$session = $this->findLatestActiveSelfServeSessionForCustomer($customer_number);
|
||||
$principal_scope = $this->requireMyActiveWashPrincipalScope();
|
||||
$session = $this->findLatestActiveSelfServeSessionForCustomer(
|
||||
(int)$principal_scope['customer_number'],
|
||||
$principal_scope['subuser_id']
|
||||
);
|
||||
if (!$session->exists()) {
|
||||
$response->error('No active self-serve wash found.', 404);
|
||||
}
|
||||
@@ -353,7 +363,7 @@ class moduleSelfServeRoute
|
||||
$response->success($this->buildActiveSelfServeSessionResponse($session));
|
||||
},
|
||||
[
|
||||
'list_own_department_selfserve_vehicle_conditions' => 'View the authenticated customer\'s active self-serve wash',
|
||||
'add_own_department_selfserve_vehicle_conditions' => 'Use the authenticated customer\'s active self-serve wash',
|
||||
]
|
||||
);
|
||||
|
||||
@@ -369,6 +379,7 @@ class moduleSelfServeRoute
|
||||
'department_id',
|
||||
'machine_type_id',
|
||||
'customer_number',
|
||||
'subuser_id',
|
||||
'vehicle_id',
|
||||
'vehicle_type_id',
|
||||
'reg',
|
||||
@@ -392,6 +403,9 @@ class moduleSelfServeRoute
|
||||
|
||||
return [
|
||||
...$session->asArray(),
|
||||
'subuser' => $this->buildInProgressWashSubuserPayload(
|
||||
$session->subuser_id->value() === null ? null : (int)$session->subuser_id->value()
|
||||
),
|
||||
'elapsed_minutes' => $session->getElapsedMinutes(),
|
||||
'open' => $session->isOpen(),
|
||||
];
|
||||
@@ -585,7 +599,8 @@ class moduleSelfServeRoute
|
||||
$customer_number > 0 ? $customer_number : null,
|
||||
false,
|
||||
null,
|
||||
false
|
||||
false,
|
||||
['subuser_id' => $args->subuser_id]
|
||||
);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
@@ -1421,7 +1436,7 @@ class moduleSelfServeRoute
|
||||
]);
|
||||
}
|
||||
|
||||
private function requireInProgressWashDetailsAccess(int $lane_id): ?int
|
||||
private function requireInProgressWashDetailsAccess(int $lane_id): ?array
|
||||
{
|
||||
global $response;
|
||||
|
||||
@@ -1438,16 +1453,19 @@ class moduleSelfServeRoute
|
||||
return null;
|
||||
}
|
||||
|
||||
if (self::hasPermission($this->customerSelfServePermission())) {
|
||||
if (self::hasPermission($this->customerSelfServeUsePermission())) {
|
||||
$customer_number = $this->resolveEffectiveCustomerNumber();
|
||||
if ($customer_number !== null && $customer_number > 0) {
|
||||
return (int)$customer_number;
|
||||
return [
|
||||
'customer_number' => (int)$customer_number,
|
||||
'subuser_id' => $this->authenticatedSubuserId(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$this->emitForbidden([
|
||||
'modules_selfserve_lane_wash_in_progress_view',
|
||||
$this->customerSelfServePermission(),
|
||||
$this->customerSelfServeUsePermission(),
|
||||
]);
|
||||
return null;
|
||||
}
|
||||
@@ -1459,14 +1477,19 @@ class moduleSelfServeRoute
|
||||
* @param array<string,mixed> $payload
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
protected function scopeInProgressWashResponseForCustomer(array $payload, ?int $customer_number): array
|
||||
protected function scopeInProgressWashResponseForCustomer(array $payload, mixed $customer_scope): array
|
||||
{
|
||||
$scope = $this->normalizeCustomerScope($customer_scope);
|
||||
$customer_number = $scope['customer_number'];
|
||||
$subuser_id = $scope['subuser_id'];
|
||||
|
||||
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) {
|
||||
$session_subuser_id = $this->extractInProgressWashSubuserId($payload);
|
||||
if ($session_customer_number === $customer_number && ($subuser_id === null || $session_subuser_id === $subuser_id)) {
|
||||
return $payload;
|
||||
}
|
||||
|
||||
@@ -1479,6 +1502,24 @@ class moduleSelfServeRoute
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{customer_number:?int,subuser_id:?int}
|
||||
*/
|
||||
private function normalizeCustomerScope(mixed $customer_scope): array
|
||||
{
|
||||
if (is_array($customer_scope)) {
|
||||
return [
|
||||
'customer_number' => $this->normalizePositiveInt($customer_scope['customer_number'] ?? null),
|
||||
'subuser_id' => $this->normalizePositiveInt($customer_scope['subuser_id'] ?? null),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'customer_number' => $this->normalizePositiveInt($customer_scope),
|
||||
'subuser_id' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $payload
|
||||
*/
|
||||
@@ -1502,7 +1543,31 @@ class moduleSelfServeRoute
|
||||
return null;
|
||||
}
|
||||
|
||||
private function requireMyActiveWashCustomerNumber(): int
|
||||
/**
|
||||
* @param array<string,mixed> $payload
|
||||
*/
|
||||
private function extractInProgressWashSubuserId(array $payload): ?int
|
||||
{
|
||||
$candidates = [
|
||||
$payload['session']['subuser_id'] ?? null,
|
||||
$payload['session']['subuser']['id'] ?? null,
|
||||
$payload['subuser']['id'] ?? null,
|
||||
];
|
||||
|
||||
foreach ($candidates as $candidate) {
|
||||
$subuser_id = $this->normalizePositiveInt($candidate);
|
||||
if ($subuser_id !== null) {
|
||||
return $subuser_id;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{customer_number:int,subuser_id:?int}
|
||||
*/
|
||||
private function requireMyActiveWashPrincipalScope(): array
|
||||
{
|
||||
global $response;
|
||||
|
||||
@@ -1510,8 +1575,8 @@ class moduleSelfServeRoute
|
||||
$response->error('Authentication failed. Invalid or missing token.', 401);
|
||||
}
|
||||
|
||||
if (!self::hasPermission($this->customerSelfServePermission())) {
|
||||
$this->emitForbidden([$this->customerSelfServePermission()]);
|
||||
if (!self::hasPermission($this->customerSelfServeUsePermission())) {
|
||||
$this->emitForbidden([$this->customerSelfServeUsePermission()]);
|
||||
}
|
||||
|
||||
$customer_number = $this->resolveEffectiveCustomerNumber();
|
||||
@@ -1519,7 +1584,10 @@ class moduleSelfServeRoute
|
||||
$response->error('No customer number found for authenticated user.', 404);
|
||||
}
|
||||
|
||||
return (int)$customer_number;
|
||||
return [
|
||||
'customer_number' => (int)$customer_number,
|
||||
'subuser_id' => $this->authenticatedSubuserId(),
|
||||
];
|
||||
}
|
||||
|
||||
private function customerSelfServePermission(): \classes\permission_node
|
||||
@@ -1527,23 +1595,53 @@ class moduleSelfServeRoute
|
||||
return self::definePermission(self::CUSTOMER_SELFSERVE_PERMISSION, subusers_permission_node_key::SELFSERVE_LIST);
|
||||
}
|
||||
|
||||
private function customerSelfServeUsePermission(): \classes\permission_node
|
||||
{
|
||||
return self::definePermission(self::CUSTOMER_SELFSERVE_USE_PERMISSION, subusers_permission_node_key::SELFSERVE_ADD);
|
||||
}
|
||||
|
||||
private function authenticatedSubuserId(): ?int
|
||||
{
|
||||
try {
|
||||
$subuser = (new authentication())->get_subuser();
|
||||
return $subuser === false ? null : (int)$subuser->id;
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function normalizePositiveInt(mixed $value): ?int
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$normalized = (int)$value;
|
||||
return $normalized > 0 ? $normalized : null;
|
||||
}
|
||||
|
||||
private function hasAuthenticatedUserOrSubuser(): bool
|
||||
{
|
||||
$auth = new authentication();
|
||||
return $auth->get_user() !== false || $auth->get_subuser() !== false;
|
||||
}
|
||||
|
||||
private function findLatestActiveSelfServeSessionForCustomer(int $customer_number): selfserve_wash_sessions_o
|
||||
private function findLatestActiveSelfServeSessionForCustomer(int $customer_number, ?int $subuser_id = null): selfserve_wash_sessions_o
|
||||
{
|
||||
if ($customer_number <= 0) {
|
||||
return new selfserve_wash_sessions_o();
|
||||
}
|
||||
|
||||
$rows = (new selfserve_wash_sessions_o())->getFieldsWhere([
|
||||
$filters = [
|
||||
'customer_number' => $customer_number,
|
||||
'completed_at' => null,
|
||||
'deleted_at' => null,
|
||||
], ['id', 'status']);
|
||||
];
|
||||
if ($subuser_id !== null) {
|
||||
$filters['subuser_id'] = $subuser_id;
|
||||
}
|
||||
|
||||
$rows = (new selfserve_wash_sessions_o())->getFieldsWhere($filters, ['id', 'status']);
|
||||
|
||||
$active_statuses = $this->activeSelfServeWashSessionStatusValues();
|
||||
$rows = array_values(array_filter(
|
||||
@@ -1582,6 +1680,7 @@ class moduleSelfServeRoute
|
||||
{
|
||||
$lane_id = (int)$session->lane_id->value();
|
||||
$customer_number = $session->customer_number->value() === null ? null : (int)$session->customer_number->value();
|
||||
$subuser_id = $session->subuser_id->value() === null ? null : (int)$session->subuser_id->value();
|
||||
$vehicle_id = $session->vehicle_id->value() === null ? null : (int)$session->vehicle_id->value();
|
||||
$session_reg = trim((string)$session->reg->value());
|
||||
if ($session_reg === '') {
|
||||
@@ -1615,6 +1714,7 @@ class moduleSelfServeRoute
|
||||
'lane_id' => $lane_id,
|
||||
'reg' => $session_reg,
|
||||
'customer_number' => $customer_number,
|
||||
'subuser_id' => $subuser_id,
|
||||
'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,
|
||||
@@ -1628,6 +1728,7 @@ class moduleSelfServeRoute
|
||||
'updated_at' => $session->updated_at->value() === null ? null : (string)$session->updated_at->value(),
|
||||
],
|
||||
'customer' => $this->buildInProgressWashCustomerPayload($customer_number),
|
||||
'subuser' => $this->buildInProgressWashSubuserPayload($subuser_id),
|
||||
'vehicle' => $this->buildInProgressWashVehiclePayload($vehicle_id, $session_reg),
|
||||
];
|
||||
}
|
||||
@@ -1663,6 +1764,35 @@ class moduleSelfServeRoute
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
private function buildInProgressWashSubuserPayload(?int $subuser_id): ?array
|
||||
{
|
||||
if ($subuser_id === null || $subuser_id <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$subuser = (new subusers_o())->select($subuser_id);
|
||||
if (!$subuser->exists()) {
|
||||
return [
|
||||
'id' => $subuser_id,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (int)$subuser->id,
|
||||
'name' => $subuser->name->value() === null ? null : (string)$subuser->name->value(),
|
||||
'username' => $subuser->username->value() === null ? null : (string)$subuser->username->value(),
|
||||
];
|
||||
} catch (\Throwable) {
|
||||
return [
|
||||
'id' => $subuser_id,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
@@ -2088,7 +2218,9 @@ class moduleSelfServeRoute
|
||||
|
||||
$missing_permissions = $department_id > 0 ? ['department_access_' . $department_id] : [];
|
||||
if ($allow_customer_self_serve) {
|
||||
$missing_permissions[] = $this->customerSelfServePermission();
|
||||
$missing_permissions[] = $requires_active_wash
|
||||
? $this->customerSelfServePermission()
|
||||
: $this->customerSelfServeUsePermission();
|
||||
}
|
||||
$this->emitForbidden($missing_permissions);
|
||||
}
|
||||
@@ -2132,7 +2264,10 @@ class moduleSelfServeRoute
|
||||
return;
|
||||
}
|
||||
|
||||
$this->emitForbidden([...$elevated_permissions, $this->customerSelfServePermission()]);
|
||||
$this->emitForbidden([
|
||||
...$elevated_permissions,
|
||||
$requires_active_wash ? $this->customerSelfServePermission() : $this->customerSelfServeUsePermission(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function requireSelfServeLaneCommandPermission(
|
||||
@@ -2163,7 +2298,10 @@ class moduleSelfServeRoute
|
||||
|
||||
$this->emitForbidden(
|
||||
$allow_customer_self_serve
|
||||
? [...$elevated_permissions, $this->customerSelfServePermission()]
|
||||
? [
|
||||
...$elevated_permissions,
|
||||
$requires_active_wash ? $this->customerSelfServePermission() : $this->customerSelfServeUsePermission(),
|
||||
]
|
||||
: $elevated_permissions
|
||||
);
|
||||
}
|
||||
@@ -2218,7 +2356,7 @@ class moduleSelfServeRoute
|
||||
protected function canCustomerUseSelfServeLane(selfserve_lane $lane, int $customer_number): bool
|
||||
{
|
||||
return $customer_number > 0
|
||||
&& $this->hasPermission($this->customerSelfServePermission())
|
||||
&& $this->hasPermission($this->customerSelfServeUsePermission())
|
||||
&& $this->isLaneSelfServeOperationallyEnabled($lane);
|
||||
}
|
||||
|
||||
@@ -2228,8 +2366,9 @@ class moduleSelfServeRoute
|
||||
return false;
|
||||
}
|
||||
|
||||
$subuser_id = $this->authenticatedSubuserId();
|
||||
try {
|
||||
if ((int)$lane->getCustomerNumber() === $customer_number) {
|
||||
if ($subuser_id === null && (int)$lane->getCustomerNumber() === $customer_number) {
|
||||
return true;
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
@@ -2238,7 +2377,7 @@ class moduleSelfServeRoute
|
||||
|
||||
$department_id = $this->departmentIdForLane($lane);
|
||||
return $department_id > 0
|
||||
&& $this->customerHasActiveSelfServeWashInDepartment($department_id, $customer_number);
|
||||
&& $this->customerHasActiveSelfServeWashInDepartment($department_id, $customer_number, $subuser_id);
|
||||
}
|
||||
|
||||
protected function canCustomerUsePropertyGateForLane(selfserve_lane $lane, int $customer_number): bool
|
||||
@@ -2253,7 +2392,7 @@ class moduleSelfServeRoute
|
||||
|
||||
$department_id = $this->departmentIdForLane($lane);
|
||||
return $department_id > 0
|
||||
&& $this->customerHasActiveSelfServeWashInDepartment($department_id, $customer_number);
|
||||
&& $this->customerHasActiveSelfServeWashInDepartment($department_id, $customer_number, $this->authenticatedSubuserId());
|
||||
}
|
||||
|
||||
protected function canCustomerUseActiveOperationalSelfServeLane(
|
||||
@@ -2275,8 +2414,9 @@ class moduleSelfServeRoute
|
||||
return false;
|
||||
}
|
||||
|
||||
$subuser_id = $this->authenticatedSubuserId();
|
||||
try {
|
||||
if ((int)$lane->getCustomerNumber() === $customer_number) {
|
||||
if ($subuser_id === null && (int)$lane->getCustomerNumber() === $customer_number) {
|
||||
return true;
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
@@ -2294,12 +2434,17 @@ class moduleSelfServeRoute
|
||||
]
|
||||
);
|
||||
|
||||
$sessions = (new selfserve_wash_sessions_o())->getFieldsWhere([
|
||||
$filters = [
|
||||
'lane_id' => (int)$lane->id,
|
||||
'customer_number' => $customer_number,
|
||||
'completed_at' => null,
|
||||
'deleted_at' => null,
|
||||
], ['id', 'status']);
|
||||
];
|
||||
if ($subuser_id !== null) {
|
||||
$filters['subuser_id'] = $subuser_id;
|
||||
}
|
||||
|
||||
$sessions = (new selfserve_wash_sessions_o())->getFieldsWhere($filters, ['id', 'status']);
|
||||
|
||||
foreach ($sessions as $session) {
|
||||
if (in_array((string)($session['status'] ?? ''), $active_statuses, true)) {
|
||||
@@ -2342,7 +2487,7 @@ class moduleSelfServeRoute
|
||||
}
|
||||
}
|
||||
|
||||
protected function customerHasActiveSelfServeWashInDepartment(int $department_id, int $customer_number): bool
|
||||
protected function customerHasActiveSelfServeWashInDepartment(int $department_id, int $customer_number, ?int $subuser_id = null): bool
|
||||
{
|
||||
if ($department_id <= 0 || $customer_number <= 0) {
|
||||
return false;
|
||||
@@ -2359,12 +2504,17 @@ class moduleSelfServeRoute
|
||||
]
|
||||
);
|
||||
|
||||
$sessions = (new selfserve_wash_sessions_o())->getFieldsWhere([
|
||||
$filters = [
|
||||
'department_id' => $department_id,
|
||||
'customer_number' => $customer_number,
|
||||
'completed_at' => null,
|
||||
'deleted_at' => null,
|
||||
], ['id', 'status']);
|
||||
];
|
||||
if ($subuser_id !== null) {
|
||||
$filters['subuser_id'] = $subuser_id;
|
||||
}
|
||||
|
||||
$sessions = (new selfserve_wash_sessions_o())->getFieldsWhere($filters, ['id', 'status']);
|
||||
|
||||
foreach ($sessions as $session) {
|
||||
if (in_array((string)($session['status'] ?? ''), $active_statuses, true)) {
|
||||
@@ -2373,6 +2523,9 @@ class moduleSelfServeRoute
|
||||
}
|
||||
|
||||
foreach ((new department_lanes_o())->getDepartmentLanes($department_id) as $department_lane) {
|
||||
if ($subuser_id !== null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$lane = (new selfserve())->lane((int)$department_lane->id);
|
||||
if ((int)$lane->getCustomerNumber() !== $customer_number) {
|
||||
|
||||
@@ -36,6 +36,7 @@ function selfserve_customer_start_make_available(int $laneId): void
|
||||
it('allows the customer self-serve start sequence without department access', function (): void {
|
||||
$group = api_fixtures()->createGroup([], [
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
'add_own_department_selfserve_vehicle_conditions',
|
||||
]);
|
||||
$scenario = api_fixtures()->createSelfServeScenario([
|
||||
'customer' => ['group_id' => $group['id']],
|
||||
@@ -90,6 +91,7 @@ it('allows the customer self-serve start sequence without department access', fu
|
||||
it('marks the active customer session relay-enabled after machine relay enable', function (): void {
|
||||
$group = api_fixtures()->createGroup([], [
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
'add_own_department_selfserve_vehicle_conditions',
|
||||
'modules_selfserve_lane_relay_enable_machine',
|
||||
]);
|
||||
$scenario = api_fixtures()->createSelfServeScenario([
|
||||
@@ -158,6 +160,7 @@ it('marks the active customer session relay-enabled after machine relay enable',
|
||||
it('derives allowed services from v2 session task snapshots when task rows are not legacy records', function (): void {
|
||||
$group = api_fixtures()->createGroup([], [
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
'add_own_department_selfserve_vehicle_conditions',
|
||||
]);
|
||||
$scenario = api_fixtures()->createSelfServeScenario([
|
||||
'customer' => ['group_id' => $group['id']],
|
||||
@@ -201,6 +204,7 @@ it('derives allowed services from v2 session task snapshots when task rows are n
|
||||
it('derives allowed services from published v2 config task snapshots when no session exists yet', function (): void {
|
||||
$group = api_fixtures()->createGroup([], [
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
'add_own_department_selfserve_vehicle_conditions',
|
||||
]);
|
||||
$scenario = api_fixtures()->createSelfServeScenario([
|
||||
'customer' => ['group_id' => $group['id']],
|
||||
@@ -267,6 +271,7 @@ it('derives allowed services from published v2 config task snapshots when no ses
|
||||
it('keeps questions visible but disables machine services and tasks when machine wash is globally disabled', function (): void {
|
||||
$group = api_fixtures()->createGroup([], [
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
'add_own_department_selfserve_vehicle_conditions',
|
||||
]);
|
||||
$scenario = api_fixtures()->createSelfServeScenario([
|
||||
'customer' => ['group_id' => $group['id']],
|
||||
@@ -316,6 +321,7 @@ it('keeps questions visible but disables machine services and tasks when machine
|
||||
it('keeps long generated task descriptions when refreshing vehicle eligibility snapshots', function (): void {
|
||||
$group = api_fixtures()->createGroup([], [
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
'add_own_department_selfserve_vehicle_conditions',
|
||||
]);
|
||||
$scenario = api_fixtures()->createSelfServeScenario([
|
||||
'customer' => ['group_id' => $group['id']],
|
||||
@@ -360,6 +366,7 @@ it('keeps long generated task descriptions when refreshing vehicle eligibility s
|
||||
it('does not create an active wash preview session from read-only eligibility checks', function (): void {
|
||||
$group = api_fixtures()->createGroup([], [
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
'add_own_department_selfserve_vehicle_conditions',
|
||||
]);
|
||||
$scenario = api_fixtures()->createSelfServeScenario([
|
||||
'customer' => ['group_id' => $group['id']],
|
||||
@@ -414,6 +421,7 @@ it('does not create an active wash preview session from read-only eligibility ch
|
||||
it('creates a durable wash session from customer start after read-only eligibility', function (): void {
|
||||
$group = api_fixtures()->createGroup([], [
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
'add_own_department_selfserve_vehicle_conditions',
|
||||
]);
|
||||
$scenario = api_fixtures()->createSelfServeScenario([
|
||||
'customer' => ['group_id' => $group['id']],
|
||||
@@ -482,6 +490,7 @@ it('creates a durable wash session from customer start after read-only eligibili
|
||||
it('refreshes an active wash summary with vehicle type without a namespace error', function (): void {
|
||||
$group = api_fixtures()->createGroup([], [
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
'add_own_department_selfserve_vehicle_conditions',
|
||||
]);
|
||||
$scenario = api_fixtures()->createSelfServeScenario([
|
||||
'customer' => ['group_id' => $group['id']],
|
||||
|
||||
@@ -51,6 +51,7 @@ function selfserve_lane_command_make_occupied(int $laneId, int $customerNumber,
|
||||
it('allows customer self-serve permission to execute START without department access when department and lane are enabled', function (): void {
|
||||
$group = api_fixtures()->createGroup([], [
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
'add_own_department_selfserve_vehicle_conditions',
|
||||
]);
|
||||
$scenario = api_fixtures()->createSelfServeScenario([
|
||||
'customer' => ['group_id' => $group['id']],
|
||||
@@ -79,6 +80,7 @@ it('allows customer self-serve permission to execute START without department ac
|
||||
it('denies customer START when department self-serve is disabled', function (): void {
|
||||
$group = api_fixtures()->createGroup([], [
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
'add_own_department_selfserve_vehicle_conditions',
|
||||
]);
|
||||
$scenario = api_fixtures()->createSelfServeScenario([
|
||||
'customer' => ['group_id' => $group['id']],
|
||||
@@ -98,13 +100,14 @@ it('denies customer START when department self-serve is disabled', function ():
|
||||
->assertStatus(403)
|
||||
->assertMissingPermissions([
|
||||
'department_access_' . (int)$scenario['department']['id'],
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
'add_own_department_selfserve_vehicle_conditions',
|
||||
]);
|
||||
});
|
||||
|
||||
it('denies customer START when lane self-serve is disabled', function (): void {
|
||||
$group = api_fixtures()->createGroup([], [
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
'add_own_department_selfserve_vehicle_conditions',
|
||||
]);
|
||||
$scenario = api_fixtures()->createSelfServeScenario([
|
||||
'customer' => ['group_id' => $group['id']],
|
||||
@@ -124,7 +127,7 @@ it('denies customer START when lane self-serve is disabled', function (): void {
|
||||
->assertStatus(403)
|
||||
->assertMissingPermissions([
|
||||
'department_access_' . (int)$scenario['department']['id'],
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
'add_own_department_selfserve_vehicle_conditions',
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ it('reports both elevated and customer self-serve permissions when lane polling
|
||||
->assertStatus(403)
|
||||
->assertMissingPermissions([
|
||||
'modules_selfserve_lane_wash_in_progress_view',
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
'add_own_department_selfserve_vehicle_conditions',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -42,13 +42,29 @@ it('requires customer self-serve permission before checking my active wash', fun
|
||||
$response
|
||||
->assertStatus(403)
|
||||
->assertMissingPermissions([
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
'add_own_department_selfserve_vehicle_conditions',
|
||||
]);
|
||||
});
|
||||
|
||||
it('denies list-only drivers from operational self-serve active wash restore', function (): void {
|
||||
$customer = api_fixtures()->createUser();
|
||||
$driverSession = api_fixtures()->createSubuserSession((int)$customer['customer_number'], [
|
||||
'SELFSERVE_LIST',
|
||||
]);
|
||||
|
||||
$response = api_client()->get('/modules/self-serve/lane/wash/my-active-wash', $driverSession['headers']);
|
||||
|
||||
$response
|
||||
->assertStatus(403)
|
||||
->assertMissingPermissions([
|
||||
'add_own_department_selfserve_vehicle_conditions',
|
||||
]);
|
||||
});
|
||||
|
||||
it('allows customer self-serve permission to view their own in-progress wash details', function (): void {
|
||||
$group = api_fixtures()->createGroup([], [
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
'add_own_department_selfserve_vehicle_conditions',
|
||||
]);
|
||||
$scenario = api_fixtures()->createSelfServeScenario([
|
||||
'customer' => [
|
||||
@@ -74,6 +90,7 @@ it('allows customer self-serve permission to view their own in-progress wash det
|
||||
it('returns the authenticated customers active self-serve wash without requiring a lane id', function (): void {
|
||||
$group = api_fixtures()->createGroup([], [
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
'add_own_department_selfserve_vehicle_conditions',
|
||||
]);
|
||||
$scenario = api_fixtures()->createSelfServeScenario([
|
||||
'customer' => [
|
||||
@@ -99,9 +116,67 @@ it('returns the authenticated customers active self-serve wash without requiring
|
||||
->and($response->data()['vehicle']['reg'] ?? null)->toBe($scenario['vehicle']['reg']);
|
||||
});
|
||||
|
||||
it('scopes active self-serve wash restore to the authenticated driver under a shared customer number', function (): void {
|
||||
$scenario = api_fixtures()->createSelfServeScenario();
|
||||
$customerNumber = (int)$scenario['customer']['customer_number'];
|
||||
$ownerDriver = api_fixtures()->createSubuserSession($customerNumber, [
|
||||
'SELFSERVE_LIST',
|
||||
'SELFSERVE_ADD',
|
||||
], [
|
||||
'name' => 'Self-Serve Owner Driver',
|
||||
]);
|
||||
$otherDriver = api_fixtures()->createSubuserSession($customerNumber, [
|
||||
'SELFSERVE_LIST',
|
||||
'SELFSERVE_ADD',
|
||||
], [
|
||||
'name' => 'Self-Serve Other Driver',
|
||||
]);
|
||||
|
||||
api_test_runtime()->db()->query(
|
||||
'UPDATE selfserve_wash_sessions SET subuser_id = '
|
||||
. (int)$ownerDriver['subuser']['id']
|
||||
. ' WHERE id = '
|
||||
. (int)$scenario['session']['id']
|
||||
);
|
||||
|
||||
$ownerResponse = api_client()->get(
|
||||
'/modules/self-serve/lane/wash/my-active-wash',
|
||||
$ownerDriver['headers']
|
||||
);
|
||||
$otherResponse = api_client()->get(
|
||||
'/modules/self-serve/lane/wash/my-active-wash',
|
||||
$otherDriver['headers']
|
||||
);
|
||||
$otherLanePoll = api_client()->get(
|
||||
'/modules/self-serve/lane/wash/in-progress?lane_id=' . (int)$scenario['lane']['id'],
|
||||
$otherDriver['headers']
|
||||
);
|
||||
|
||||
$ownerResponse
|
||||
->assertStatus(200)
|
||||
->assertSuccess(true);
|
||||
$otherResponse
|
||||
->assertStatus(404)
|
||||
->assertMessage('No active self-serve wash found.');
|
||||
$otherLanePoll
|
||||
->assertStatus(200)
|
||||
->assertSuccess(true);
|
||||
|
||||
expect($ownerResponse->data()['session']['subuser_id'] ?? null)->toBe((int)$ownerDriver['subuser']['id'])
|
||||
->and($ownerResponse->data()['subuser']['id'] ?? null)->toBe((int)$ownerDriver['subuser']['id'])
|
||||
->and($otherLanePoll->data())->toMatchArray([
|
||||
'lane_id' => (int)$scenario['lane']['id'],
|
||||
'in_progress' => true,
|
||||
'session' => null,
|
||||
'customer' => null,
|
||||
'vehicle' => null,
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns 404 when the authenticated customer has no active self-serve wash', function (): void {
|
||||
$session = api_fixtures()->createUserSession([
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
'add_own_department_selfserve_vehicle_conditions',
|
||||
]);
|
||||
|
||||
$response = api_client()->get('/modules/self-serve/lane/wash/my-active-wash', $session['headers']);
|
||||
@@ -115,6 +190,7 @@ it('redacts another customers in-progress wash from customer self-serve lane pol
|
||||
$scenario = api_fixtures()->createSelfServeScenario();
|
||||
$otherSession = api_fixtures()->createUserSession([
|
||||
'list_own_department_selfserve_vehicle_conditions',
|
||||
'add_own_department_selfserve_vehicle_conditions',
|
||||
]);
|
||||
|
||||
$response = api_client()->get(
|
||||
|
||||
@@ -446,6 +446,7 @@ CREATE TABLE IF NOT EXISTS `selfserve_wash_sessions` (
|
||||
`department_id` INT NOT NULL,
|
||||
`machine_type_id` INT NULL,
|
||||
`customer_number` INT NULL,
|
||||
`subuser_id` INT NULL,
|
||||
`vehicle_id` INT NULL,
|
||||
`vehicle_type_id` INT NULL,
|
||||
`reg` VARCHAR(255) NOT NULL,
|
||||
@@ -466,6 +467,8 @@ CREATE TABLE IF NOT EXISTS `selfserve_wash_sessions` (
|
||||
KEY `idx_selfserve_wash_sessions_lane_reg` (`lane_id`, `reg`),
|
||||
KEY `idx_selfserve_wash_sessions_status` (`status`),
|
||||
KEY `idx_selfserve_wash_sessions_customer` (`customer_number`),
|
||||
KEY `idx_selfserve_wash_sessions_subuser_active` (`subuser_id`, `completed_at`),
|
||||
KEY `idx_selfserve_wash_sessions_customer_subuser_active` (`customer_number`, `subuser_id`, `completed_at`),
|
||||
KEY `idx_selfserve_wash_sessions_department_completed` (`department_id`, `completed_at`),
|
||||
KEY `idx_selfserve_wash_sessions_order` (`order_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
|
||||
@@ -15,6 +15,7 @@ class SelfserveCustomerLaneAccessRouteHarness extends moduleSelfServeRoute
|
||||
/** @var array<string,bool> */
|
||||
public array $permissions = [
|
||||
'list_own_department_selfserve_vehicle_conditions' => true,
|
||||
'add_own_department_selfserve_vehicle_conditions' => true,
|
||||
];
|
||||
public bool $laneEnabled = true;
|
||||
public bool $moduleEnabled = true;
|
||||
@@ -77,11 +78,12 @@ class SelfserveCustomerLaneAccessRouteHarness extends moduleSelfServeRoute
|
||||
$method->invoke($this, $lane, $customer_number, $command_permission, $allow_customer_self_serve, $requires_active_wash);
|
||||
}
|
||||
|
||||
protected function customerHasActiveSelfServeWashInDepartment(int $department_id, int $customer_number): bool
|
||||
protected function customerHasActiveSelfServeWashInDepartment(int $department_id, int $customer_number, ?int $subuser_id = null): bool
|
||||
{
|
||||
$this->activeWashChecks[] = [
|
||||
'department_id' => $department_id,
|
||||
'customer_number' => $customer_number,
|
||||
'subuser_id' => $subuser_id,
|
||||
];
|
||||
|
||||
return $this->activeWashResult;
|
||||
@@ -170,6 +172,7 @@ it('falls back to active department sessions for customer active wash operations
|
||||
[
|
||||
'department_id' => 4,
|
||||
'customer_number' => 12345679,
|
||||
'subuser_id' => null,
|
||||
],
|
||||
]);
|
||||
});
|
||||
@@ -184,6 +187,7 @@ it('blocks active wash operations for other customers', function (): void {
|
||||
[
|
||||
'department_id' => 4,
|
||||
'customer_number' => 12345679,
|
||||
'subuser_id' => null,
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -14,6 +14,14 @@ if (!class_exists(SelfserveInProgressWashAccessHarness::class)) {
|
||||
{
|
||||
return $this->scopeInProgressWashResponseForCustomer($payload, $customer_number);
|
||||
}
|
||||
|
||||
public function scopeForPrincipal(array $payload, int $customer_number, ?int $subuser_id): array
|
||||
{
|
||||
return $this->scopeInProgressWashResponseForCustomer($payload, [
|
||||
'customer_number' => $customer_number,
|
||||
'subuser_id' => $subuser_id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,3 +82,68 @@ it('redacts another customers in-progress wash details during customer lane poll
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps own driver in-progress self-serve wash details visible', function (): void {
|
||||
$route = new SelfserveInProgressWashAccessHarness();
|
||||
|
||||
$payload = [
|
||||
'lane_id' => 7,
|
||||
'in_progress' => true,
|
||||
'session' => [
|
||||
'id' => 704,
|
||||
'customer_number' => 12345679,
|
||||
'subuser_id' => 501,
|
||||
'reg' => 'AB12345',
|
||||
],
|
||||
'subuser' => [
|
||||
'id' => 501,
|
||||
'name' => 'Driver One',
|
||||
],
|
||||
'customer' => [
|
||||
'customer_number' => 12345679,
|
||||
'display_name' => 'Example Customer',
|
||||
],
|
||||
'vehicle' => [
|
||||
'id' => 55,
|
||||
'reg' => 'AB12345',
|
||||
],
|
||||
];
|
||||
|
||||
expect($route->scopeForPrincipal($payload, 12345679, 501))->toBe($payload);
|
||||
});
|
||||
|
||||
it('redacts another driver in the same customers in-progress wash during driver lane polling', function (): void {
|
||||
$route = new SelfserveInProgressWashAccessHarness();
|
||||
|
||||
$scoped = $route->scopeForPrincipal([
|
||||
'lane_id' => 9,
|
||||
'status' => 'MACHINE_STARTED',
|
||||
'in_progress' => true,
|
||||
'elapsed_minutes' => 4,
|
||||
'session' => [
|
||||
'id' => 804,
|
||||
'customer_number' => 12345679,
|
||||
'subuser_id' => 502,
|
||||
'reg' => 'CD67890',
|
||||
],
|
||||
'subuser' => [
|
||||
'id' => 502,
|
||||
'name' => 'Driver Two',
|
||||
],
|
||||
'customer' => [
|
||||
'customer_number' => 12345679,
|
||||
'display_name' => 'Example Customer',
|
||||
],
|
||||
'vehicle' => [
|
||||
'id' => 77,
|
||||
'reg' => 'CD67890',
|
||||
],
|
||||
], 12345679, 501);
|
||||
|
||||
expect($scoped)->toBe([
|
||||
'lane_id' => 9,
|
||||
'in_progress' => true,
|
||||
'session' => null,
|
||||
'customer' => null,
|
||||
'vehicle' => null,
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -336,7 +336,7 @@ it('wires self-serve property gate command permissions', function (): void {
|
||||
expect($moduleSelfServeRoute)->toContain('Failed to execute self-serve property gate command');
|
||||
expect($moduleSelfServeRoute)->toContain('$this->canCustomerUsePropertyGateForLane($lane, $customer_number)');
|
||||
expect($moduleSelfServeRoute)->toContain('$this->isOwnCustomerContext($customer_number)');
|
||||
expect($moduleSelfServeRoute)->toContain('$this->customerHasActiveSelfServeWashInDepartment($department_id, $customer_number)');
|
||||
expect($moduleSelfServeRoute)->toContain('$this->customerHasActiveSelfServeWashInDepartment($department_id, $customer_number, $subuser_id)');
|
||||
|
||||
expect($commandTrait)->not->toBeFalse();
|
||||
expect($commandTrait)->toContain('Failed to open property access gate.');
|
||||
@@ -351,9 +351,9 @@ it('wires in-progress self-serve wash details endpoint', function (): void {
|
||||
expect($moduleSelfServeRoute)->toContain('/modules/self-serve/lane/wash/in-progress');
|
||||
expect($moduleSelfServeRoute)->toContain('/modules/self-serve/lane/wash/my-active-wash');
|
||||
expect($moduleSelfServeRoute)->toContain('modules_selfserve_lane_wash_in_progress_view');
|
||||
expect($moduleSelfServeRoute)->toContain('list_own_department_selfserve_vehicle_conditions');
|
||||
expect($moduleSelfServeRoute)->toContain('add_own_department_selfserve_vehicle_conditions');
|
||||
expect($moduleSelfServeRoute)->toContain('requireInProgressWashDetailsAccess($lane_id)');
|
||||
expect($moduleSelfServeRoute)->toContain('findLatestActiveSelfServeSessionForCustomer($customer_number)');
|
||||
expect($moduleSelfServeRoute)->toContain('findLatestActiveSelfServeSessionForCustomer(');
|
||||
expect($moduleSelfServeRoute)->toContain('buildActiveSelfServeSessionResponse($session)');
|
||||
expect($moduleSelfServeRoute)->toContain('(new department_lanes_o())->select($lane_id)');
|
||||
expect($moduleSelfServeRoute)->toContain('self::requireDepartmentAccess((string)$department_lane->department->value())');
|
||||
@@ -549,7 +549,7 @@ it('keeps read-only self-serve preview and summary refreshes from touching relay
|
||||
expect($vehicleConditionsRoute)->toContain('$summary = $flow->getLatestSessionSummary($lane_id, $reg);');
|
||||
expect($vehicleConditionsRoute)->toContain('$activate_machine = $this->requestBooleanFlag(\'activate_machine\', true);');
|
||||
expect($vehicleConditionsRoute)->toContain('$sync_relay_state = $this->requestBooleanFlag(\'sync_relay_state\', true);');
|
||||
expect($vehicleConditionsRoute)->toContain('$this->getWashFlow()->synchronizeSession($lane, $reg, $customer_id, $activate_machine, $vehicle_type_id, $sync_relay_state);');
|
||||
expect($vehicleConditionsRoute)->toContain('$this->getWashFlow()->synchronizeSession($lane, $reg, $customer_id, $activate_machine, $vehicle_type_id, $sync_relay_state, [');
|
||||
expect($vehicleConditionsRoute)->toContain('$this->getWashFlow()->synchronizeSession($lane_id, $reg, $customer_id, $activate_machine, $vehicle_type_id, $sync_relay_state);');
|
||||
});
|
||||
|
||||
@@ -565,7 +565,8 @@ it('classifies self-serve lane command route authorization by customer product b
|
||||
// Product flow confirms the customer UI sends STOP, while route docs expose START and property gates
|
||||
// as customer-scoped lane commands. RESERVE, RELEASE, and RESET remain absent from customer flow/docs.
|
||||
expect($productDocs)->toContain('Customer->>API: POST /modules/self-serve/lane/command (STOP)');
|
||||
expect($productDocs)->toContain('customer `list_own_department_selfserve_vehicle_conditions` for scoped `START`, scoped `STOP`, and property gate commands');
|
||||
expect($productDocs)->toContain('customer `add_own_department_selfserve_vehicle_conditions` for scoped `START`');
|
||||
expect($productDocs)->toContain('customer `list_own_department_selfserve_vehicle_conditions` for scoped `STOP` and property gate commands');
|
||||
expect($productDocs)->toContain('Customer `START` requires an enabled self-serve lane. Customer `STOP` and property gate commands require the customer\'s active wash in the lane department.');
|
||||
expect($washFlow)->toContain('$payload[\'command\'] = $relayRole === \'PROPERTY_ENTRANCE\' ? \'OPEN_PROPERTY_ACCESS_GATE\' : \'OPEN_PROPERTY_EXIT_GATE\'');
|
||||
expect($washFlow)->toContain('$signalType = \'studio_action_gate_open\'');
|
||||
|
||||
Reference in New Issue
Block a user