Add tests for customer-scoped vehicle conditions and property gate permissions

- Introduced tests for `SelfserveNonOwnedVehicleWashAccess` to validate customer-scoped conditions for non-owned vehicles.
- Added `SelfservePropertyGatePermissionBypassTest` to ensure proper permission handling for lanes and departments.
- Updated `department_selfserve_vehicle_conditions_o` and routes to prevent cross-customer answer persistence.
- Enhanced `selfserve_wash_flow` with customer-scoped persisted answer logic and improved method parameters for vehicle eligibility and session synchronization.
- Adjusted OpenAPI spec and unit tests to reflect new customer-scoping behavior in self-serve operations.
This commit is contained in:
Jeppe Bundgaard
2026-04-28 17:59:33 +02:00
parent 4dd00cd7a2
commit 690e114d38
11 changed files with 325 additions and 37 deletions
File diff suppressed because one or more lines are too long
@@ -66,9 +66,9 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
$this->conditionEvaluator ??= new selfserve_condition_evaluator();
}
public function previewVehicleEligibility(int $laneId, string $reg, ?int $customerNumber = null, ?int $vehicleTypeIdOverride = null): array
public function previewVehicleEligibility(int $laneId, string $reg, ?int $customerNumber = null, ?int $vehicleTypeIdOverride = null, array $options = []): array
{
$snapshot = $this->buildEligibilitySnapshot($laneId, $reg, $customerNumber, $vehicleTypeIdOverride);
$snapshot = $this->buildEligibilitySnapshot($laneId, $reg, $customerNumber, $vehicleTypeIdOverride, $options);
$session = $this->findLatestOpenSession($laneId, $snapshot['reg'], $snapshot['customer_number']);
return $this->formatSnapshotResponse($snapshot, $session->exists() ? $session->asArray() : null);
@@ -93,9 +93,9 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
return $response;
}
public function synchronizeSession(int $laneId, string $reg, ?int $customerNumber = null, bool $activateMachine = true, ?int $vehicleTypeIdOverride = null, bool $syncRelayState = true): array
public function synchronizeSession(int $laneId, string $reg, ?int $customerNumber = null, bool $activateMachine = true, ?int $vehicleTypeIdOverride = null, bool $syncRelayState = true, array $options = []): array
{
$snapshot = $this->buildEligibilitySnapshot($laneId, $reg, $customerNumber, $vehicleTypeIdOverride);
$snapshot = $this->buildEligibilitySnapshot($laneId, $reg, $customerNumber, $vehicleTypeIdOverride, $options);
$session = $this->findLatestOpenSession($laneId, $snapshot['reg'], $snapshot['customer_number']);
if (!$session->exists()) {
@@ -414,11 +414,12 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
$vehicleData = $vehicle?->asArray();
$vehicleTypeId = $this->resolveVehicleTypeId($vehicle, $vehicleTypeIdOverride);
$resolvedCustomerNumber = $customerNumber ?? ($vehicle !== null ? (int)$vehicle->customer_id->value() : null);
$persistedAnswerCustomerNumber = $this->resolvePersistedAnswerCustomerNumber($resolvedCustomerNumber);
$questions = $this->loadQuestions($departmentId, $laneId, $vehicleTypeId, $publishedConfigPayload);
$conditions = $this->loadConditions($departmentId, $laneId, $vehicleTypeId, $machineTypeId, $publishedConfigPayload);
$rules = $this->loadConditionRules($conditions, $publishedConfigPayload);
$persistedAnswers = (new department_selfserve_vehicle_conditions_o())->getAnswerMapForVehicle($departmentId, $laneId, $normalizedReg);
$persistedAnswers = $this->loadPersistedAnswers($departmentId, $laneId, $normalizedReg, $persistedAnswerCustomerNumber);
$answerOverrides = $this->normalizeAnswerOverrides($options['answer_overrides'] ?? []);
$answers = $this->applyAnswerOverrides($persistedAnswers, $answerOverrides);
$answerSources = $this->buildAnswerSources($persistedAnswers, $answerOverrides);
@@ -543,6 +544,7 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
'vehicle_type_id' => $vehicleTypeId,
'answers' => $answers,
'persisted_answers' => $persistedAnswers,
'persisted_answer_customer_number' => $persistedAnswerCustomerNumber,
'answer_overrides' => $answerOverrides,
'answer_sources' => $answerSources,
'questions' => $visibleQuestions,
@@ -767,6 +769,32 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
return $sources;
}
protected function resolvePersistedAnswerCustomerNumber(?int $resolvedCustomerNumber): ?int
{
if ($resolvedCustomerNumber === null || $resolvedCustomerNumber <= 0) {
return null;
}
return $resolvedCustomerNumber;
}
/**
* @return array<int,bool>
*/
protected function loadPersistedAnswers(int $departmentId, int $laneId, string $reg, ?int $customerNumber): array
{
if ($customerNumber === null || $customerNumber <= 0) {
return [];
}
return (new department_selfserve_vehicle_conditions_o())->getAnswerMapForVehicle(
$departmentId,
$laneId,
$reg,
$customerNumber
);
}
/**
* @param array<string,mixed> $snapshot
* @param array<int,array<string,mixed>> $questions
@@ -4,9 +4,9 @@ namespace modules\selfserve\interfaces;
interface selfserve_wash_flow_i
{
public function previewVehicleEligibility(int $laneId, string $reg, ?int $customerNumber = null, ?int $vehicleTypeIdOverride = null): array;
public function previewVehicleEligibility(int $laneId, string $reg, ?int $customerNumber = null, ?int $vehicleTypeIdOverride = null, array $options = []): array;
public function synchronizeSession(int $laneId, string $reg, ?int $customerNumber = null, bool $activateMachine = true, ?int $vehicleTypeIdOverride = null, bool $syncRelayState = true): array;
public function synchronizeSession(int $laneId, string $reg, ?int $customerNumber = null, bool $activateMachine = true, ?int $vehicleTypeIdOverride = null, bool $syncRelayState = true, array $options = []): array;
public function recordMachineStartWebhook(int $laneId, ?string $reg = null, array $payload = []): array;
@@ -311,12 +311,12 @@ 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. |
| `GET /department/selfserve/vehicle/allowed` | `lane_id`, `reg` | `list_department_selfserve_vehicle_conditions` or `list_own_department_selfserve_vehicle_conditions` | Calls `selfserve_wash_flow::previewVehicleEligibility()` and returns questions, tasks, allowed services, and the current session if one exists. Own-permission customers may evaluate borrowed plates; saved answers only apply when scoped to the authenticated customer. |
Typical failures:
- `400` missing `lane_id` or `reg`
- `403` permission denied or wrong vehicle ownership
- `403` permission denied, missing customer context, or wrong department
- `404` lane not found
### `/department/selfserve/washes/summary`
@@ -53,8 +53,10 @@ class department_selfserve_vehicle_conditions_o extends db
$question = (int)$question;
$value = (bool)$value;
$customer_id = $customer_id !== null ? (int)$customer_id : null;
// Remove any existing entry for the same department, lane, reg and question
$sql = "DELETE FROM $this->table WHERE department = $department AND lane = $lane AND reg = '$reg' AND question = $question";
// Remove any existing entry for the same department, lane, customer, reg and question.
// Saved answers must not bleed across customers that temporarily wash the same plate.
$customer_filter = $customer_id === null ? 'customer_id IS NULL' : 'customer_id = ' . $customer_id;
$sql = "DELETE FROM $this->table WHERE department = $department AND lane = $lane AND $customer_filter AND reg = '$reg' AND question = $question";
$db->query($sql);
// Add the object
$tmp_id = self::add_object([
@@ -105,11 +107,16 @@ class department_selfserve_vehicle_conditions_o extends db
];
}
public function getAnswerMapForVehicle(int $departmentId, int $laneId, string $reg): array
public function getAnswerMapForVehicle(int $departmentId, int $laneId, string $reg, ?int $customerId = null): array
{
if ($customerId === null || $customerId <= 0) {
return [];
}
$rows = self::getFieldsWhere([
'department' => $departmentId,
'lane' => $laneId,
'customer_id' => $customerId,
'reg' => selfserve::standardize_registration($reg),
'deleted_at' => null,
], ['question', 'value']);
+2
View File
@@ -4802,6 +4802,7 @@ paths:
tags:
- Self-Serve
summary: Check whether self-serve is allowed for a vehicle on a lane
description: Customers with own self-serve permissions may evaluate any registration plate for their wash. Persisted self-serve answers are only applied when they are scoped to the authenticated customer.
operationId: getSelfserveVehicleAllowed
parameters:
- name: lane_id
@@ -8881,6 +8882,7 @@ paths:
description: |
Send a command (e.g., start, stop, reset) to a self-serve lane.
Property gate commands (`OPEN_PROPERTY_ACCESS_GATE`, `OPEN_PROPERTY_EXIT_GATE`) are also supported here.
Property gate command permissions are bypassed for authenticated customers with an active self-serve wash in the target department.
operationId: sendSelfServeLaneCommand
requestBody:
required: true
@@ -134,8 +134,7 @@ class departmentSelfserveVehicleConditionsRoute
$lane = $this->assertLaneAccess($user, $lane_id, $has_global);
$customer_number = null;
if (!$has_global && $has_own) {
$vehicle = $this->assertOwnVehicle($user, $reg, 'list_department_selfserve_vehicle_conditions');
$customer_number = (int)$vehicle->customer_id->value();
$customer_number = $this->requireAuthenticatedCustomerNumber($user, 'list_department_selfserve_vehicle_conditions');
}
$vehicle_type_id = $this->resolveVehicleTypeIdFromQuery();
$flow = $this->getWashFlow();
@@ -148,7 +147,7 @@ class departmentSelfserveVehicleConditionsRoute
$response->success($flow->previewVehicleEligibility($lane_id, $reg, $customer_number, $vehicle_type_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 an owned vehicle'
'list_own_department_selfserve_vehicle_conditions' => 'Check whether self-serve is allowed for a customer-scoped vehicle'
]);
/**
@@ -197,8 +196,7 @@ class departmentSelfserveVehicleConditionsRoute
$this->assertLaneAccess($user, $lane_id, $has_global);
$customer_number = null;
if (!$has_global && $has_own) {
$vehicle = $this->assertOwnVehicle($user, $reg, 'list_department_selfserve_vehicle_conditions');
$customer_number = (int)$vehicle->customer_id->value();
$customer_number = $this->requireAuthenticatedCustomerNumber($user, 'list_department_selfserve_vehicle_conditions');
}
if ($vehicle_type_id !== null) {
@@ -213,7 +211,7 @@ class departmentSelfserveVehicleConditionsRoute
}
}, [
'list_department_selfserve_vehicle_conditions' => 'View self-serve wash summaries',
'list_own_department_selfserve_vehicle_conditions' => 'View self-serve wash summaries for owned vehicles'
'list_own_department_selfserve_vehicle_conditions' => 'View self-serve wash summaries for customer-scoped vehicles'
]);
/**
@@ -253,9 +251,7 @@ class departmentSelfserveVehicleConditionsRoute
$this->forbidDepartmentAccess($department);
}
} else {
$customer_id = (int)$user->customer_number->value();
$vehicle_o = $this->assertOwnVehicle($user, $reg, 'add_department_selfserve_vehicle_conditions');
$customer_id = (int)$vehicle_o->customer_id->value();
$customer_id = $this->requireAuthenticatedCustomerNumber($user, 'add_department_selfserve_vehicle_conditions');
}
try {
@@ -272,7 +268,7 @@ class departmentSelfserveVehicleConditionsRoute
}
}, [
'add_department_selfserve_vehicle_conditions' => 'Add a department self-serve vehicle condition',
'add_own_department_selfserve_vehicle_conditions' => 'Add own department self-serve vehicle condition'
'add_own_department_selfserve_vehicle_conditions' => 'Add customer-scoped department self-serve vehicle condition'
]);
/**
@@ -331,12 +327,6 @@ class departmentSelfserveVehicleConditionsRoute
}
if ($response->isRequestParameterSet('reg')) {
$new_reg = selfserve::standardize_registration((string)$response->getRequestParameter('reg'));
if (!$has_global && $has_own) {
$vehicle_o = $this->assertOwnVehicle($user, $new_reg, 'update_department_selfserve_vehicle_conditions');
if ((int)$vehicle_o->customer_id->value() !== $customer_number) {
$response->forbidden(['update_department_selfserve_vehicle_conditions']);
}
}
$condition_o->reg->update($new_reg);
}
if ($response->isRequestParameterSet('question')) {
@@ -375,7 +365,7 @@ class departmentSelfserveVehicleConditionsRoute
}
}, [
'update_department_selfserve_vehicle_conditions' => 'Update a department self-serve vehicle condition',
'update_own_department_selfserve_vehicle_conditions' => 'Update own department self-serve vehicle condition'
'update_own_department_selfserve_vehicle_conditions' => 'Update customer-scoped department self-serve vehicle condition'
]);
/**
@@ -556,16 +546,16 @@ class departmentSelfserveVehicleConditionsRoute
return $lane;
}
private function assertOwnVehicle(object $user, string $reg, ?string $elevatedPermission = null): customer_vehicles_o
private function requireAuthenticatedCustomerNumber(object $user, string $elevatedPermission): int
{
global $response;
$vehicle_o = (new customer_vehicles_o())->selectByPlate($reg);
if (!$vehicle_o->exists() || (int)$vehicle_o->customer_id->value() !== (int)$user->customer_number->value()) {
$response->forbidden([$elevatedPermission ?? 'list_department_selfserve_vehicle_conditions']);
$customer_number = (int)$user->customer_number->value();
if ($customer_number <= 0) {
$response->forbidden([$elevatedPermission]);
}
return $vehicle_o;
return $customer_number;
}
private function assertSummaryAccess(object $user, array $summary, bool $hasGlobalPermission, string $elevatedPermission): void
@@ -8,11 +8,15 @@ use classes\response;
use classes\router;
use classes\selfserve;
use classes\stripe;
use modules\selfserve\classes\selfserve_lane;
use modules\selfserve\classes\selfserve_wash_flow;
use modules\selfserve\helpers\selfserve_lane_command;
use modules\selfserve\helpers\selfserve_lane_port;
use modules\selfserve\helpers\selfserve_lane_relay;
use modules\selfserve\helpers\selfserve_lane_state;
use modules\selfserve\helpers\selfserve_lane_status;
use modules\selfserve\helpers\selfserve_wash_session_status;
use objects\department_lanes_o;
use objects\departments_o;
use objects\logs_o;
use objects\orders_o;
@@ -425,10 +429,18 @@ class moduleSelfServeRoute
self::requirePermission('modules_selfserve_lane_command_execute_reset');
break;
case selfserve_lane_command::OPEN_PROPERTY_ACCESS_GATE:
self::requirePermission('modules_selfserve_lane_command_execute_open_property_access_gate');
$this->requirePropertyGateCommandPermission(
'modules_selfserve_lane_command_execute_open_property_access_gate',
$lane,
(int)$user->customer_number->value()
);
break;
case selfserve_lane_command::OPEN_PROPERTY_EXIT_GATE:
self::requirePermission('modules_selfserve_lane_command_execute_open_property_exit_gate');
$this->requirePropertyGateCommandPermission(
'modules_selfserve_lane_command_execute_open_property_exit_gate',
$lane,
(int)$user->customer_number->value()
);
break;
}
// Execute the command
@@ -1181,6 +1193,84 @@ class moduleSelfServeRoute
$lane->setShellyTransportOverride($transport);
}
private function requirePropertyGateCommandPermission(string $permission, selfserve_lane $lane, int $customer_number): void
{
if (self::hasPermission($permission)) {
return;
}
if ($this->canCustomerUsePropertyGateForLane($lane, $customer_number)) {
return;
}
self::requirePermission($permission);
}
protected function canCustomerUsePropertyGateForLane(selfserve_lane $lane, int $customer_number): bool
{
if ($customer_number <= 0) {
return false;
}
$department_id = (int)$lane->department_lane?->department?->value();
if ($department_id <= 0) {
return false;
}
return $this->customerHasActiveSelfServeWashInDepartment($department_id, $customer_number);
}
protected function customerHasActiveSelfServeWashInDepartment(int $department_id, int $customer_number): bool
{
if ($department_id <= 0 || $customer_number <= 0) {
return false;
}
$active_statuses = array_map(
static fn(selfserve_wash_session_status $status): string => $status->value,
[
selfserve_wash_session_status::MACHINE_RELAY_ENABLED,
selfserve_wash_session_status::READY_FOR_MACHINE_START,
selfserve_wash_session_status::MACHINE_STARTED,
selfserve_wash_session_status::PENDING_QUESTIONS,
selfserve_wash_session_status::MACHINE_NOT_ALLOWED,
]
);
$sessions = (new selfserve_wash_sessions_o())->getFieldsWhere([
'department_id' => $department_id,
'customer_number' => $customer_number,
'completed_at' => null,
'deleted_at' => null,
], ['id', 'status']);
foreach ($sessions as $session) {
if (in_array((string)($session['status'] ?? ''), $active_statuses, true)) {
return true;
}
}
foreach ((new department_lanes_o())->getDepartmentLanes($department_id) as $department_lane) {
try {
$lane = (new selfserve())->lane((int)$department_lane->id);
if ((int)$lane->getCustomerNumber() !== $customer_number) {
continue;
}
if (
$lane->getLaneStatus()->equals(selfserve_lane_status::OCCUPIED)
|| $lane->getLaneState()->equals(selfserve_lane_state::IN_WASH)
) {
return true;
}
} catch (\Throwable) {
continue;
}
}
return false;
}
private function requestedShellyTransportOverride(): ?string
{
$transport = null;
@@ -0,0 +1,73 @@
<?php
app_require('modules/selfserve/classes/selfserve_wash_flow.php');
use modules\selfserve\classes\selfserve_wash_flow;
class SelfserveNonOwnedVehicleWashFlowHarness extends selfserve_wash_flow
{
public function __construct()
{
// Skip schema bootstrapping for this unit test.
}
public function persistedAnswerCustomer(?int $resolvedCustomerNumber): ?int
{
return $this->resolvePersistedAnswerCustomerNumber($resolvedCustomerNumber);
}
}
function selfserve_non_owned_vehicle_route_block(string $route, string $method, string $path): string
{
$start = strpos($route, "\$this->{$method}('{$path}'");
if ($start === false) {
throw new RuntimeException("Route block not found: {$method} {$path}");
}
$nextComment = strpos($route, "\n /**", $start + 1);
if ($nextComment === false) {
return substr($route, $start);
}
return substr($route, $start, $nextComment - $start);
}
it('allows own-permission customers to preview borrowed registration plates without ownership checks', function (): void {
$route = file_get_contents(app_path('routes/departmentSelfserveVehicleConditionsRoute.php'));
expect($route)->not->toBeFalse();
$allowedBlock = selfserve_non_owned_vehicle_route_block($route, 'get', '/department/selfserve/vehicle/allowed');
expect($allowedBlock)->toContain("requireAuthenticatedCustomerNumber(\$user, 'list_department_selfserve_vehicle_conditions')");
expect($allowedBlock)->toContain('previewVehicleEligibility($lane_id, $reg, $customer_number, $vehicle_type_id)');
expect($allowedBlock)->not->toContain('assertOwnVehicle');
});
it('allows customer-scoped answers to be stored for borrowed registration plates', function (): void {
$route = file_get_contents(app_path('routes/departmentSelfserveVehicleConditionsRoute.php'));
expect($route)->not->toBeFalse();
$addBlock = selfserve_non_owned_vehicle_route_block($route, 'post', '/department/selfserve/vehicle/conditions');
expect($addBlock)->toContain("requireAuthenticatedCustomerNumber(\$user, 'add_department_selfserve_vehicle_conditions')");
expect($addBlock)->toContain('$condition_o->add($department, $lane, $reg, $question, $value, $customer_id)');
expect($addBlock)->not->toContain('assertOwnVehicle');
});
it('loads saved answers from the authenticated customer context instead of the plate owner', function (): void {
$flow = new SelfserveNonOwnedVehicleWashFlowHarness();
expect($flow->persistedAnswerCustomer(10001))->toBe(10001);
expect($flow->persistedAnswerCustomer(20002))->toBe(20002);
expect($flow->persistedAnswerCustomer(null))->toBeNull();
expect($flow->persistedAnswerCustomer(0))->toBeNull();
});
it('scopes saved self-serve answers by customer number and registration plate', function (): void {
$source = file_get_contents(app_path('objects/department_selfserve_vehicle_conditions_o.php'));
expect($source)->not->toBeFalse();
expect($source)->toContain('department, lane, customer, reg and question');
expect($source)->toContain('$customer_filter = $customer_id === null');
expect($source)->toContain("'customer_id' => \$customerId");
});
@@ -60,6 +60,8 @@ it('documents self-serve machine type, eligibility, summary, and webhook endpoin
expect($content)->toContain('/modules/self-serve/lane/relay/machine_cleaner/set:');
expect($allowedPathBlock)->toContain('name: vehicle_type_id');
expect($allowedPathBlock)->toContain('name: vehicle_type');
expect($allowedPathBlock)->toContain('may evaluate any registration plate');
expect($allowedPathBlock)->toContain('Persisted self-serve answers are only applied');
expect($summaryPathBlock)->toContain('name: vehicle_type_id');
expect($summaryPathBlock)->toContain('name: vehicle_type');
});
@@ -0,0 +1,96 @@
<?php
app_require('routes/moduleSelfServeRoute.php');
app_require('modules/selfserve/classes/selfserve_lane.php');
app_require('objects/department_lanes_o.php');
app_require('classes/object_property.php');
use classes\object_property;
use modules\selfserve\classes\selfserve_lane;
use objects\department_lanes_o;
use routes\moduleSelfServeRoute;
class SelfservePropertyGatePermissionBypassRouteHarness extends moduleSelfServeRoute
{
public bool $activeWashResult = false;
/** @var array<int,array{department_id:int,customer_number:int}> */
public array $activeWashChecks = [];
public function canUsePropertyGate(selfserve_lane $lane, int $customer_number): bool
{
return $this->canCustomerUsePropertyGateForLane($lane, $customer_number);
}
protected function customerHasActiveSelfServeWashInDepartment(int $department_id, int $customer_number): bool
{
$this->activeWashChecks[] = [
'department_id' => $department_id,
'customer_number' => $customer_number,
];
return $this->activeWashResult;
}
}
class SelfservePropertyGatePermissionBypassDepartmentLaneFake extends department_lanes_o
{
public function __construct(int $department_id)
{
$this->id = -1;
$this->department = new object_property('department_lanes', -1, 'department', 'int');
$this->department->set($department_id);
}
public function structure(): void
{
// Skip database bootstrap for this unit test.
}
}
function selfserve_property_gate_permission_lane_for_department(int $department_id): selfserve_lane
{
$reflection = new ReflectionClass(selfserve_lane::class);
/** @var selfserve_lane $lane */
$lane = $reflection->newInstanceWithoutConstructor();
$lane->id = 77;
$lane->department_lane = new SelfservePropertyGatePermissionBypassDepartmentLaneFake($department_id);
return $lane;
}
it('allows property gate commands for customers with an active wash in the target department', function (): void {
$route = new SelfservePropertyGatePermissionBypassRouteHarness();
$route->activeWashResult = true;
$lane = selfserve_property_gate_permission_lane_for_department(6);
expect($route->canUsePropertyGate($lane, 12345679))->toBeTrue();
expect($route->activeWashChecks)->toBe([
[
'department_id' => 6,
'customer_number' => 12345679,
],
]);
});
it('does not bypass property gate permissions without a positive customer number', function (): void {
$route = new SelfservePropertyGatePermissionBypassRouteHarness();
$route->activeWashResult = true;
$lane = selfserve_property_gate_permission_lane_for_department(6);
expect($route->canUsePropertyGate($lane, 0))->toBeFalse();
expect($route->activeWashChecks)->toBe([]);
});
it('does not bypass property gate permissions when the customer has no active wash in the target department', function (): void {
$route = new SelfservePropertyGatePermissionBypassRouteHarness();
$route->activeWashResult = false;
$lane = selfserve_property_gate_permission_lane_for_department(6);
expect($route->canUsePropertyGate($lane, 12345679))->toBeFalse();
expect($route->activeWashChecks)->toBe([
[
'department_id' => 6,
'customer_number' => 12345679,
],
]);
});