not->toBeFalse(); expect($machineTypesRoute)->toContain('/department/selfserve/machine-types'); expect($vehicleConditionsRoute)->not->toBeFalse(); expect($vehicleConditionsRoute)->toContain('/department/selfserve/vehicle/allowed'); expect($vehicleConditionsRoute)->toContain('/department/selfserve/washes/summary'); expect($vehicleConditionsRoute)->toContain('synchronizeSession'); expect($webhookRoute)->not->toBeFalse(); expect($webhookRoute)->toContain('/relay/button/press/post'); expect($webhookRoute)->toContain('/relay/machine/on/post'); expect($webhookRoute)->toContain('recordMachineStartWebhook'); expect($webhookRoute)->toContain('recordCloudShellySignal'); }); it('wires local edge gateway machine ON signal monitor endpoints', function (): void { $edgeGatewayRoute = file_get_contents(app_path('modules/edgegateway/routes/edgeGatewaysRoute.php')); $agent = file_get_contents(app_path('resources/edge-gateway-agent/agent.php')); $worker = file_get_contents(app_path('resources/edge-gateway-agent/lan-worker.php')); expect($edgeGatewayRoute)->not->toBeFalse() ->and($edgeGatewayRoute)->toContain('/edge-agent/gateways/{id}/selfserve/machine-signal-bindings') ->and($edgeGatewayRoute)->toContain('/edge-agent/gateways/{id}/selfserve/machine-signal') ->and($edgeGatewayRoute)->toContain('recordEdgeGatewaySignal') ->and($agent)->toContain('pollMachineStartSignals') ->and($agent)->toContain('/selfserve/machine-signal') ->and($worker)->toContain('Input.GetStatus') ->and($worker)->toContain('/relay/input-status') ->and($worker)->toContain('worker_require_authorization') ->and($worker)->toContain('HTTP_X_TRUCKWASH_WORKER_TOKEN'); }); it('keeps machine type support wired into lanes, tasks, and conditions routes', function (): void { $lanesRoute = file_get_contents(app_path('routes/departmentLanesRoute.php')); $tasksRoute = file_get_contents(app_path('routes/departmentSelfserveTasksRoute.php')); $conditionsRoute = file_get_contents(app_path('routes/departmentSelfserveConditionsRoute.php')); $questionsRoute = file_get_contents(app_path('routes/departmentSelfserveQuestionsRoute.php')); expect($lanesRoute)->toContain('machine_type_id'); expect($tasksRoute)->toContain('machine_type_id'); expect($conditionsRoute)->toContain('machine_type_id'); expect($questionsRoute)->toContain('department') // shared questions still use the legacy columns with default 0 scope ->toContain('lane') ->toContain('product'); }); it('wires lane-level self-serve toggles through lane APIs, guest payloads, and edge workspace readiness', function (): void { $lanesRoute = file_get_contents(app_path('routes/departmentLanesRoute.php')); $laneObject = file_get_contents(app_path('objects/department_lanes_o.php')); $guestRoute = file_get_contents(app_path('routes/guestRoute.php')); $edgeWorkspace = file_get_contents(app_path('modules/edgegateway/classes/edge_gateway_department_workspace_service.php')); expect($lanesRoute)->not->toBeFalse() ->and($lanesRoute)->toContain("'selfserve_enabled'") ->and($lanesRoute)->toContain('normalizeSelfServeEnabledValue') ->and($lanesRoute)->toContain('disableSelfServeRelaysBestEffort') ->and($lanesRoute)->toContain("'lane' => \$created_lane->asArray()") ->and($lanesRoute)->toContain("'lane' => \$department_lane->asArray()"); expect($laneObject)->not->toBeFalse() ->and($laneObject)->toContain('public object_property $selfserve_enabled') ->and($laneObject)->toContain('public function isSelfServeEnabled(): bool') ->and($laneObject)->toContain('public static function normalizeSelfServeEnabledValue') ->and($laneObject)->toContain('public static function disableSelfServeRelaysBestEffort') ->and($laneObject)->toContain('setMachineRelayStatusHard(false)') ->and($laneObject)->toContain('setMachineProgramPickerRelayStatusHard(false)') ->and($laneObject)->toContain('setMachineCleanerRelayStatusHard(false)'); expect($guestRoute)->not->toBeFalse() ->and($guestRoute)->toContain("'selfserve_enabled' => \$lane->isSelfServeEnabled()") ->and($guestRoute)->toContain("'machine_available' => \$lane->isSelfServeEnabled() && !empty(\$lane->relay_machine_id->value())"); expect($edgeWorkspace)->not->toBeFalse() ->and($edgeWorkspace)->toContain("'selfserve_enabled' => \$lane->isSelfServeEnabled()") ->and($edgeWorkspace)->toContain("'enabled_lanes' => count(\$enabledLanes)") ->and($edgeWorkspace)->toContain("'ready_lanes' => count(\$readyLanes)") ->and($edgeWorkspace)->toContain("(int)(\$selfServe['ready_lanes'] ?? 0) < (int)(\$selfServe['enabled_lanes'] ?? 0)"); }); it('sources guest lane product availability from the published v2 self-serve config before legacy tasks', function (): void { $laneObject = file_get_contents(app_path('objects/department_lanes_o.php')); expect($laneObject)->not->toBeFalse() ->and($laneObject)->toContain('getPublishedSelfServeLaneProducts') ->and($laneObject)->toContain('selfserve_config_versioning') ->and($laneObject)->toContain('getPublishedV2Config') ->and($laneObject)->toContain("\$taskLane !== 0 && \$taskLane !== \$laneId") ->and($laneObject)->toContain('department_selfserve_tasks_o::getLaneProducts'); }); it('wires self-serve config draft/publish/rollback lifecycle endpoints', function (): void { $configRoute = file_get_contents(app_path('routes/departmentSelfserveConfigVersionsRoute.php')); expect($configRoute)->not->toBeFalse(); expect($configRoute)->toContain('/department/selfserve/config/versions'); expect($configRoute)->toContain('/department/selfserve/config/history'); expect($configRoute)->toContain('/department/selfserve/config/active'); expect($configRoute)->toContain('/department/selfserve/config/draft'); expect($configRoute)->toContain('/department/selfserve/config/validate'); expect($configRoute)->toContain('/department/selfserve/config/publish'); expect($configRoute)->toContain('/department/selfserve/config/rollback'); expect($configRoute)->toContain('publish_department_selfserve_config_versions'); expect($configRoute)->toContain('rollback_department_selfserve_config_versions'); }); it('wires the all-in-one self-serve studio replacement endpoints', function (): void { $studioRoute = file_get_contents(app_path('routes/departmentSelfserveStudioRoute.php')); $studioGraph = file_get_contents(app_path('modules/selfserve/classes/selfserve_studio_graph.php')); expect($studioRoute)->not->toBeFalse(); expect($studioRoute)->toContain('/department/selfserve/studio/graph'); expect($studioRoute)->toContain('/department/selfserve/studio/layout'); expect($studioRoute)->toContain('/department/selfserve/studio/validate'); expect($studioRoute)->toContain('/department/selfserve/studio/simulate'); expect($studioRoute)->toContain('/department/selfserve/studio/path-outcomes'); expect($studioRoute)->toContain('/department/selfserve/studio/path-outcomes/stream'); expect($studioRoute)->toContain('/department/selfserve/studio/path-confirmations'); expect($studioRoute)->toContain("ini_set('display_errors', '0')"); expect($studioRoute)->toContain('/department/selfserve/studio/publish'); expect($studioRoute)->toContain('/department/selfserve/studio/rollback'); expect($studioRoute)->toContain('/department/selfserve/studio/gateway-action'); expect($studioRoute)->toContain('projectPathOutcomes'); expect($studioRoute)->toContain('confirmPathOutcome'); expect($studioRoute)->toContain('modules_shelly_config'); expect($studioRoute)->toContain('modules_selfserve_sessions_force_stop'); $simulateStart = strpos($studioRoute, "/department/selfserve/studio/simulate"); $simulateEnd = strpos($studioRoute, "/department/selfserve/studio/path-outcomes/stream", $simulateStart); expect($simulateStart)->not->toBeFalse(); expect($simulateEnd)->not->toBeFalse(); $simulateRoute = substr($studioRoute, $simulateStart, $simulateEnd - $simulateStart); expect($simulateRoute)->toContain("requireStudioUser('list_department_selfserve_config_versions')"); expect($simulateRoute)->toContain("'list_department_selfserve_config_versions' => 'Run the self-serve studio simulator'"); expect($simulateRoute)->not->toContain('list_department_selfserve_vehicle_conditions'); expect($studioGraph)->not->toBeFalse(); $loadSimulationConfigStart = strpos($studioGraph, 'private function loadSimulationConfig'); $projectPathOutcomesStart = strpos($studioGraph, 'public function projectPathOutcomes', $loadSimulationConfigStart); expect($loadSimulationConfigStart)->not->toBeFalse(); expect($projectPathOutcomesStart)->not->toBeFalse(); $loadSimulationConfig = substr($studioGraph, $loadSimulationConfigStart, $projectPathOutcomesStart - $loadSimulationConfigStart); expect($loadSimulationConfig)->toContain('selectLatestByDepartmentAndStatus'); expect($loadSimulationConfig)->toContain('snapshotLegacyConfig($departmentId)'); expect($loadSimulationConfig)->not->toContain('ensureDraftFromLegacy'); expect($loadSimulationConfig)->toContain('Draft studio simulation requires list_department_selfserve_config_versions permission.'); expect($studioGraph)->toContain('department_selfserve_studio_layouts'); expect($studioGraph)->toContain('department_selfserve_path_confirmations'); expect($studioGraph)->toContain('buildGatewayWorkspace'); expect($studioGraph)->toContain('runGatewayAction'); expect($studioGraph)->toContain('layout_affects_runtime'); expect($studioGraph)->toContain('resolved'); }); it('wires machine wash included minutes into self-serve module config', function (): void { $selfserveConfig = file_get_contents(app_path('modules/selfserve/selfserve_c.php')); expect($selfserveConfig)->not->toBeFalse(); expect($selfserveConfig)->toContain('selfserve_machine_wash_minutes_included_c'); expect($selfserveConfig)->toContain('machine_wash_minutes_included'); }); it('keeps legacy self-serve CRUD routes syncing canonical drafts', function (): void { $questionsRoute = file_get_contents(app_path('routes/departmentSelfserveQuestionsRoute.php')); $conditionsRoute = file_get_contents(app_path('routes/departmentSelfserveConditionsRoute.php')); $rulesRoute = file_get_contents(app_path('routes/departmentSelfserveConditionRulesRoute.php')); $tasksRoute = file_get_contents(app_path('routes/departmentSelfserveTasksRoute.php')); expect($questionsRoute)->toContain('syncDraftFromLegacyForDepartment'); expect($conditionsRoute)->toContain('syncDraftFromLegacyForDepartment'); expect($rulesRoute)->toContain('syncDraftFromLegacyForDepartment'); expect($tasksRoute)->toContain('syncDraftFromLegacyForDepartment'); }); it('wires machine relay status get and set endpoints', function (): void { $moduleSelfServeRoute = file_get_contents(app_path('routes/moduleSelfServeRoute.php')); expect($moduleSelfServeRoute)->not->toBeFalse(); expect($moduleSelfServeRoute)->toContain('/modules/self-serve/lane/relay/machine/status'); expect($moduleSelfServeRoute)->toContain('/modules/self-serve/lane/relay/machine/set'); expect($moduleSelfServeRoute)->toContain('/modules/self-serve/lane/relay/machine_program_picker/status'); expect($moduleSelfServeRoute)->toContain('/modules/self-serve/lane/relay/machine_program_picker/set'); expect($moduleSelfServeRoute)->toContain('/modules/self-serve/lane/relay/machine_cleaner/status'); expect($moduleSelfServeRoute)->toContain('/modules/self-serve/lane/relay/machine_cleaner/set'); expect($moduleSelfServeRoute)->toContain('getMachineRelayStatus'); expect($moduleSelfServeRoute)->toContain('setMachineRelayStatus'); expect($moduleSelfServeRoute)->toContain('getMachineProgramPickerRelayStatus'); expect($moduleSelfServeRoute)->toContain('setMachineProgramPickerRelayStatus'); expect($moduleSelfServeRoute)->toContain('getMachineCleanerRelayStatus'); expect($moduleSelfServeRoute)->toContain('setMachineCleanerRelayStatus'); expect($moduleSelfServeRoute)->toContain('setMachineCleanerRelayStatusHard(true)'); expect($moduleSelfServeRoute)->toContain('applyShellyTransportOverride($lane)'); expect($moduleSelfServeRoute)->toContain("'transport' => \$this->requestedShellyTransportOverride()"); expect($moduleSelfServeRoute)->toContain('buildRelayStatusResponse'); }); it('wires dashboard lane machine status and Dognvask toggle endpoints', function (): void { $lanesRoute = file_get_contents(app_path('routes/departmentLanesRoute.php')); $laneObject = file_get_contents(app_path('objects/department_lanes_o.php')); $moduleSelfServeRoute = file_get_contents(app_path('routes/moduleSelfServeRoute.php')); expect($lanesRoute)->not->toBeFalse() ->and($lanesRoute)->toContain('/department/lanes/status-toggles') ->and($lanesRoute)->toContain('LIST_DEPARTMENT_LANE_STATUS_TOGGLES') ->and($lanesRoute)->toContain('getDepartmentLanes($department_id)'); expect($laneObject)->not->toBeFalse() ->and($laneObject)->toContain("'machine_status_enabled' => self::isOperationalStatusName(\$status)") ->and($laneObject)->toContain("'dognvask_configured' => \$selfserve_configuration_warnings === []") ->and($laneObject)->toContain("'dognvask_configuration_warnings' => \$selfserve_configuration_warnings") ->and($laneObject)->toContain('public function getSelfServeConfigurationWarnings(): array') ->and($laneObject)->toContain('relay_machine_program_picker_id') ->and($laneObject)->toContain('machine_type_id'); expect($moduleSelfServeRoute)->not->toBeFalse() ->and($moduleSelfServeRoute)->toContain("\$this->put('/modules/self-serve/lane/status'") ->and($moduleSelfServeRoute)->toContain('modules_selfserve_lane_status_set') ->and($moduleSelfServeRoute)->toContain('setLaneStatus($target_status)') ->and($moduleSelfServeRoute)->toContain('selfserve_lane_status::MAINTENANCE') ->and($moduleSelfServeRoute)->toContain('machine_status_enabled'); }); it('applies Shelly transport overrides across self-serve relay side-effect routes', function (): void { $moduleSelfServeRoute = file_get_contents(app_path('routes/moduleSelfServeRoute.php')); expect($moduleSelfServeRoute)->not->toBeFalse(); expect(substr_count($moduleSelfServeRoute, 'applyShellyTransportOverride($lane)'))->toBeGreaterThanOrEqual(17); expect($moduleSelfServeRoute)->toContain('$lane->execute($command, $args);'); expect($moduleSelfServeRoute)->toContain('$lane->setAllowedServicesFromVisibleTasks($allowed_services);'); expect($moduleSelfServeRoute)->toContain('$lane->open($gate, $toggle_after);'); expect($moduleSelfServeRoute)->toContain('$lane->turnOnRelay(selfserve_lane_relay::MACHINE, $duration);'); expect($moduleSelfServeRoute)->toContain('$lane->turnOnRelay(selfserve_lane_relay::MACHINE_PROGRAM_PICKER, $duration);'); expect($moduleSelfServeRoute)->toContain('$lane->turnOnRelay(selfserve_lane_relay::MACHINE_CLEANER, $duration);'); expect($moduleSelfServeRoute)->toContain('$lane->forceTurnOnMachineRelay($duration);'); expect($moduleSelfServeRoute)->toContain('$lane->forceTurnOffMachineRelay();'); expect($moduleSelfServeRoute)->toContain('$lane->forceTurnOnRelay(selfserve_lane_relay::MACHINE_PROGRAM_PICKER, $duration);'); expect($moduleSelfServeRoute)->toContain('$lane->forceTurnOffRelay(selfserve_lane_relay::MACHINE_PROGRAM_PICKER);'); expect($moduleSelfServeRoute)->toContain('$lane->forceTurnOnRelay(selfserve_lane_relay::MACHINE_CLEANER, $duration);'); expect($moduleSelfServeRoute)->toContain('$lane->forceTurnOffRelay(selfserve_lane_relay::MACHINE_CLEANER);'); }); it('wires allowed services route through machine relay visibility sync', function (): void { $moduleSelfServeRoute = file_get_contents(app_path('routes/moduleSelfServeRoute.php')); expect($moduleSelfServeRoute)->not->toBeFalse(); expect($moduleSelfServeRoute)->toContain('/modules/self-serve/lane/services/allowed'); expect($moduleSelfServeRoute)->toContain('selfserve_wash_session_tasks_o'); expect($moduleSelfServeRoute)->toContain('selectLatestOpenByLane'); expect($moduleSelfServeRoute)->toContain('$session_task_services[$task_id]'); expect($moduleSelfServeRoute)->toContain('setAllowedServicesFromVisibleTasks($allowed_services)'); expect($moduleSelfServeRoute)->not->toContain('syncMachineRelayFromVisibleServices($allowed_services, true)'); expect($moduleSelfServeRoute)->toContain("'relay_sync' => \$relay_sync"); }); it('wires self-serve lane gate open endpoint', function (): void { $moduleSelfServeRoute = file_get_contents(app_path('routes/moduleSelfServeRoute.php')); expect($moduleSelfServeRoute)->not->toBeFalse(); expect($moduleSelfServeRoute)->toContain('/modules/self-serve/lane/gate/open'); expect($moduleSelfServeRoute)->toContain('modules_selfserve_lane_gate_open'); expect($moduleSelfServeRoute)->toContain('selfserve_lane_port::ENTRANCE'); expect($moduleSelfServeRoute)->toContain('selfserve_lane_port::EXIT'); expect($moduleSelfServeRoute)->toContain('$toggle_after = $this->requestedRelayToggleAfter(1);'); expect($moduleSelfServeRoute)->toContain('MAX_GATE_OPEN_TOGGLE_AFTER_SECONDS = 5'); expect($moduleSelfServeRoute)->toContain('self::requireMaxValue($toggle_after, self::MAX_GATE_OPEN_TOGGLE_AFTER_SECONDS);'); expect($moduleSelfServeRoute)->toContain('$lane->open($gate, $toggle_after)'); expect($moduleSelfServeRoute)->toContain("'toggle_after' => \$toggle_after"); expect($moduleSelfServeRoute)->toContain('private function requestedRelayToggleAfter'); expect($moduleSelfServeRoute)->toContain('Failed to open self-serve lane gate'); expect($moduleSelfServeRoute)->toContain("'Failed to open ' . strtolower(\$gate->name) . ' gate.'"); expect($moduleSelfServeRoute)->not->toContain('Failed to open lane gate: '); }); it('wires self-serve property gate command permissions', function (): void { $moduleSelfServeRoute = file_get_contents(app_path('routes/moduleSelfServeRoute.php')); $commandTrait = file_get_contents(app_path('modules/selfserve/traits/selfserve_lane_command_t.php')); expect($moduleSelfServeRoute)->not->toBeFalse(); expect($moduleSelfServeRoute)->toContain('case selfserve_lane_command::OPEN_PROPERTY_ACCESS_GATE'); expect($moduleSelfServeRoute)->toContain('case selfserve_lane_command::OPEN_PROPERTY_EXIT_GATE'); expect($moduleSelfServeRoute)->toContain('modules_selfserve_lane_command_execute_open_property_access_gate'); expect($moduleSelfServeRoute)->toContain('modules_selfserve_lane_command_execute_open_property_exit_gate'); expect($moduleSelfServeRoute)->toContain('Failed to execute self-serve property gate command'); expect($commandTrait)->not->toBeFalse(); expect($commandTrait)->toContain('Failed to open property access gate.'); expect($commandTrait)->toContain('Failed to open property exit gate.'); expect($commandTrait)->not->toContain('Failed to open property access gate: '); }); it('wires in-progress self-serve wash details endpoint', function (): void { $moduleSelfServeRoute = file_get_contents(app_path('routes/moduleSelfServeRoute.php')); expect($moduleSelfServeRoute)->not->toBeFalse(); 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('requireInProgressWashDetailsAccess($lane_id)'); expect($moduleSelfServeRoute)->toContain('findLatestActiveSelfServeSessionForCustomer($customer_number)'); 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())'); expect($moduleSelfServeRoute)->toContain('scopeInProgressWashResponseForCustomer'); expect($moduleSelfServeRoute)->toContain("'in_progress' => true"); expect($moduleSelfServeRoute)->toContain("'in_progress' => false"); expect($moduleSelfServeRoute)->toContain('selfserve_lane_status::OCCUPIED'); expect($moduleSelfServeRoute)->toContain('selfserve_lane_state::IN_WASH'); expect($moduleSelfServeRoute)->toContain('getWashStartTime'); expect($moduleSelfServeRoute)->toContain('machine_wash_minutes_included'); expect($moduleSelfServeRoute)->toContain("'included_minutes' =>"); expect($moduleSelfServeRoute)->toContain("'machine_relay_enabled' =>"); expect($moduleSelfServeRoute)->toContain("'machine_relay_enabled_at' =>"); expect($moduleSelfServeRoute)->toContain("'wash_started_at' =>"); }); it('wires self-serve session management endpoints and OpenAPI coverage', function (): void { $moduleSelfServeRoute = file_get_contents(app_path('routes/moduleSelfServeRoute.php')); $openApiFiles = [app_path('openapi.yaml')]; $rootOpenApiFile = dirname(app_path(), 3) . '/openapi.yaml'; if (is_file($rootOpenApiFile)) { $openApiFiles[] = $rootOpenApiFile; } expect($moduleSelfServeRoute)->not->toBeFalse(); expect($moduleSelfServeRoute)->toContain('/modules/self-serve/sessions'); expect($moduleSelfServeRoute)->toContain('/modules/self-serve/sessions/{id}'); expect($moduleSelfServeRoute)->toContain('/modules/self-serve/lane/force/stop'); expect($moduleSelfServeRoute)->toContain('modules_selfserve_sessions_view'); expect($moduleSelfServeRoute)->toContain('modules_selfserve_sessions_force_stop'); expect($moduleSelfServeRoute)->toContain('modules_selfserve_sessions_force_stop_bill'); expect($moduleSelfServeRoute)->toContain('listObjectsWithPaginationIfSet'); expect($moduleSelfServeRoute)->toContain('getSessionSummary($session_id)'); expect($moduleSelfServeRoute)->toContain('forceStopLane($lane_id, $session_id, $bill, $reason, $user_id)'); foreach ($openApiFiles as $openApiFile) { $openApi = file_get_contents($openApiFile); expect($openApi)->not->toBeFalse(); expect($openApi)->toContain('/modules/self-serve/sessions:'); expect($openApi)->toContain('/modules/self-serve/sessions/{id}:'); expect($openApi)->toContain('/modules/self-serve/lane/force/stop:'); expect($openApi)->toContain('listSelfServeSessions'); expect($openApi)->toContain('getSelfServeSessionDetail'); expect($openApi)->toContain('forceStopSelfServeLane'); expect($openApi)->toContain('SelfserveForceStopResponse'); expect($openApi)->toContain('FORCE_STOPPED'); expect($openApi)->toContain('SESSION_FORCE_STOPPED'); } }); it('returns authoritative allowed service state from self-serve session summaries', function (): void { $washFlow = file_get_contents(app_path('modules/selfserve/classes/selfserve_wash_flow.php')); expect($washFlow)->not->toBeFalse(); $start = strpos($washFlow, 'public function getSessionSummary'); $end = strpos($washFlow, 'public function getLatestSessionSummary', $start); expect($start)->not->toBeFalse(); expect($end)->not->toBeFalse(); $summarySource = substr($washFlow, $start, $end - $start); expect($summarySource) ->toContain('$metadata = is_array($session->metadata_json->value())') ->toContain('$allowedServices = $this->normalizeServiceNames(') ->toContain('$tasks = $this->filterTasksForAllowedServices($tasks, $allowedServices)') ->toContain("'allowed_services' => \$allowedServices") ->toContain("'machine_available' => \$machineAvailable") ->toContain("'all_visible_questions_answered' => \$allVisibleQuestionsAnswered") ->toContain("'allowed' => (bool)\$session->allowed->value()"); }); it('filters machine button tasks out of self-serve snapshots when MACHINE is not allowed', function (): void { $washFlow = file_get_contents(app_path('modules/selfserve/classes/selfserve_wash_flow.php')); expect($washFlow)->not->toBeFalse() ->and($washFlow)->toContain('$visibleTasks = $this->filterTasksForAllowedServices($activeTasks, $allowedServices)') ->and($washFlow)->toContain("'tasks' => \$visibleTasks") ->and($washFlow)->toContain('protected function filterTasksForAllowedServices') ->and($washFlow)->toContain('protected function taskUsesMachineControls') ->and($washFlow)->toContain("in_array(selfserve_lane_services::MACHINE->name, \$allowedServices, true)") ->and($washFlow)->toContain("\$this->normalizeButtonList(\$task['buttons'] ?? null) !== []") ->and($washFlow)->toContain("\$task['dynamic_images_vehicle_type'] ?? null"); }); it('keeps self-serve force stop distinct from normal STOP relay and gate behavior', function (): void { $washFlow = file_get_contents(app_path('modules/selfserve/classes/selfserve_wash_flow.php')); $sessionObject = file_get_contents(app_path('objects/selfserve_wash_sessions_o.php')); $statusEnum = file_get_contents(app_path('modules/selfserve/helpers/selfserve_wash_session_status.php')); $eventEnum = file_get_contents(app_path('modules/selfserve/helpers/selfserve_wash_event_type.php')); expect($washFlow)->not->toBeFalse(); expect($sessionObject)->not->toBeFalse(); expect($statusEnum)->toContain('FORCE_STOPPED'); expect($eventEnum)->toContain('SESSION_FORCE_STOPPED'); expect($sessionObject)->toContain('markForceStopped'); expect($sessionObject)->toContain("metadata_json->set(\$existing)"); $start = strpos($washFlow, 'public function forceStopLane'); $end = strpos($washFlow, 'protected function buildEligibilitySnapshot', $start); expect($start)->not->toBeFalse(); expect($end)->not->toBeFalse(); $forceStopSource = substr($washFlow, $start, $end - $start); expect($forceStopSource)->toContain('$lane->invoice()'); expect($forceStopSource)->toContain('getLastInvoiceOrderId'); expect($forceStopSource)->toContain('markForceStopped'); expect($forceStopSource)->toContain('SESSION_FORCE_STOPPED'); expect($forceStopSource)->toContain('selfserve_lane_command::RESET'); expect($forceStopSource)->toContain('runtime_before_reset'); expect($forceStopSource)->not->toContain('selfserve_lane_command::STOP'); expect($forceStopSource)->not->toContain('selfserve_lane_port::EXIT'); expect($forceStopSource)->not->toContain('open('); expect($forceStopSource)->not->toContain('turnOffRelaysAfterStop'); expect($forceStopSource)->not->toContain('disableMachineRelayForCompletedWash'); expect($forceStopSource)->not->toContain('getRelayStatus'); expect($forceStopSource)->not->toContain('MACHINE_PROGRAM_PICKER'); expect($forceStopSource)->not->toContain('addVehicleTypeProductToInvoiceIfNeeded'); expect($forceStopSource)->not->toContain('addVehicleTypeProductToLastInvoiceOrder'); }); it('wires vehicle type override into self-serve preview and synchronization routes', function (): void { $vehicleConditionsRoute = file_get_contents(app_path('routes/departmentSelfserveVehicleConditionsRoute.php')); $washFlow = file_get_contents(app_path('modules/selfserve/classes/selfserve_wash_flow.php')); expect($vehicleConditionsRoute)->not->toBeFalse(); expect($vehicleConditionsRoute)->toContain('vehicle_type_id'); expect($vehicleConditionsRoute)->toContain('vehicle_type'); expect($vehicleConditionsRoute)->toContain('resolveVehicleTypeIdFromQuery'); expect($vehicleConditionsRoute)->toContain('resolveVehicleTypeIdFromRequest'); expect($vehicleConditionsRoute)->toContain('normalizeVehicleTypeOverride'); expect($vehicleConditionsRoute)->toContain('shouldRefreshSummaryForVehicleType'); expect($vehicleConditionsRoute)->toContain('previewVehicleEligibility($lane_id, $reg, $customer_number, $vehicle_type_id)'); expect($vehicleConditionsRoute)->toContain('synchronizeSession($lane_id, $reg, $customer_number, false, $vehicle_type_id, false)'); expect($vehicleConditionsRoute)->toContain('requestBooleanFlag'); expect($vehicleConditionsRoute)->toContain('synchronizeSession($lane, $reg, $customer_id, $activate_machine, $vehicle_type_id, $sync_relay_state)'); expect($vehicleConditionsRoute)->toContain('synchronizeSession($lane_id, $reg, $customer_id, $activate_machine, $vehicle_type_id, $sync_relay_state)'); expect($washFlow)->not->toBeFalse(); expect($washFlow)->toContain('resolveVehicleTypeId($vehicle, $vehicleTypeIdOverride)'); expect($washFlow)->toContain("'vehicle_type_id' => \$snapshot['vehicle_type_id']"); expect($washFlow)->toContain("\$session->vehicle_type_id->set(\$snapshot['vehicle_type_id']);"); expect($washFlow)->toContain("\$session->vehicle_id->set(\$snapshot['vehicle']['id'] ?? null);"); }); it('blocks new self-serve eligibility and session sync for disabled lanes', function (): void { $washFlow = file_get_contents(app_path('modules/selfserve/classes/selfserve_wash_flow.php')); $commandTrait = file_get_contents(app_path('modules/selfserve/traits/selfserve_lane_command_t.php')); expect($washFlow)->not->toBeFalse() ->and($washFlow)->toContain('if (!$lane->isSelfServeEnabled())') ->and($washFlow)->toContain("'allowed_services' => []") ->and($washFlow)->toContain("'machine_available' => false") ->and($washFlow)->toContain("'allowed' => false") ->and($washFlow)->toContain("'blocked_reason' => 'Self-serve is disabled for this lane.'") ->and($washFlow)->toContain("'blocking_reasons' => ['LANE_SELFSERVE_DISABLED']") ->and($washFlow)->toContain("'disabled_lane' => true") ->and($washFlow)->toContain('formatBlockedSessionSummary') ->and($washFlow)->toContain('empty($summary[\'session\'][\'id\'])'); expect($commandTrait)->not->toBeFalse() ->and($commandTrait)->toContain('$this->department_lane->isSelfServeEnabled()') ->and($commandTrait)->toContain('Self-serve is not enabled for this lane.'); }); it('keeps read-only self-serve preview and summary refreshes from touching relay hardware', function (): void { $vehicleConditionsRoute = file_get_contents(app_path('routes/departmentSelfserveVehicleConditionsRoute.php')); expect($vehicleConditionsRoute)->not->toBeFalse(); expect($vehicleConditionsRoute)->toContain('$flow->synchronizeSession($lane_id, $reg, $customer_number, false, $vehicle_type_id, false);'); expect($vehicleConditionsRoute)->toContain('$summary = $flow->synchronizeSession('); expect($vehicleConditionsRoute)->toContain('$summary = $flow->synchronizeSession($lane_id, $reg, $customer_number, false, $vehicle_type_id, false);'); 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_id, $reg, $customer_id, $activate_machine, $vehicle_type_id, $sync_relay_state);'); }); it('classifies self-serve lane command route authorization by customer product behavior', function (): void { $moduleSelfServeRoute = file_get_contents(app_path('routes/moduleSelfServeRoute.php')); $productDocs = file_get_contents(app_path('modules/selfserve/selfserve.md')); $washFlow = file_get_contents(app_path('modules/selfserve/classes/selfserve_wash_flow.php')); expect($moduleSelfServeRoute)->not->toBeFalse(); expect($productDocs)->not->toBeFalse(); expect($washFlow)->not->toBeFalse(); // 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 `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\''); $commandCases = selfserve_lane_command_route_cases($moduleSelfServeRoute); expect($commandCases['START']) ->toContain('requireSelfServeLaneCommandPermission') ->toContain("'modules_selfserve_lane_command_execute_start'") ->toContain("true\n );") ->not->toContain('requireDepartmentAccess') ->not->toContain('requireOperatorLaneCommandPermission'); expect($commandCases['STOP']) ->toContain('requireSelfServeLaneCommandPermission') ->toContain("'modules_selfserve_lane_command_execute_stop'") ->toContain("true,\n true") ->not->toContain('requireDepartmentAccess') ->not->toContain('requireOperatorLaneCommandPermission'); expect($commandCases['OPEN_PROPERTY_ACCESS_GATE']) ->toContain('requirePropertyGateCommandPermission') ->toContain("'modules_selfserve_lane_command_execute_open_property_access_gate'") ->not->toContain('self::requirePermission') ->not->toContain('requireDepartmentAccess'); expect($commandCases['OPEN_PROPERTY_EXIT_GATE']) ->toContain('requirePropertyGateCommandPermission') ->toContain("'modules_selfserve_lane_command_execute_open_property_exit_gate'") ->not->toContain('self::requirePermission') ->not->toContain('requireDepartmentAccess'); foreach (['RESERVE', 'RELEASE', 'RESET'] as $operatorOnlyCommand) { expect($commandCases[$operatorOnlyCommand]) ->toContain('requireOperatorLaneCommandPermission') ->not->toContain('requireSelfServeLaneCommandPermission') ->not->toContain('requirePropertyGateCommandPermission'); } }); /** * @return array */ function selfserve_lane_command_route_cases(string $moduleSelfServeRoute): array { $cases = []; preg_match_all( '/case selfserve_lane_command::([A-Z_]+):(.*?)(?=\n\s*case selfserve_lane_command::|\n\s*}\n\s*\/\/ Execute the command)/s', $moduleSelfServeRoute, $matches, PREG_SET_ORDER ); foreach ($matches as $match) { $cases[$match[1]] = $match[2]; } return $cases; }