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:
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']);
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
]);
|
||||
});
|
||||
Reference in New Issue
Block a user