diff --git a/services/nginx/app/modules/selfserve/classes/selfserve_studio_graph.php b/services/nginx/app/modules/selfserve/classes/selfserve_studio_graph.php index 474f605f..0b79db2e 100644 --- a/services/nginx/app/modules/selfserve/classes/selfserve_studio_graph.php +++ b/services/nginx/app/modules/selfserve/classes/selfserve_studio_graph.php @@ -517,21 +517,11 @@ class selfserve_studio_graph throw new \RuntimeException('lane_id is required for studio simulation.'); } - $configSource = strtolower(trim((string)($payload['config_source'] ?? 'draft'))); - if (!in_array($configSource, ['draft', 'published'], true)) { - $configSource = 'draft'; - } - $versioning = new selfserve_config_versioning(); - if ($configSource === 'published') { - $version = $versioning->getPublishedV2Config($departmentId); - $config = is_array($version['config'] ?? null) ? (array)$version['config'] : null; - $versionId = isset($version['version_id']) ? (int)$version['version_id'] : null; - } else { - $version = $versioning->ensureDraftFromLegacy($departmentId, $userId, false); - $config = is_array($version['config'] ?? null) ? (array)$version['config'] : $versioning->snapshotLegacyConfig($departmentId); - $versionId = isset($version['id']) ? (int)$version['id'] : null; - } + $configContext = $this->loadSimulationConfig($departmentId, $payload, $permissions, $versioning); + $configSource = $configContext['config_source']; + $config = $configContext['config']; + $versionId = $configContext['version_id']; $includeHardware = filter_var($payload['include_hardware'] ?? true, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); $includeHardware = $includeHardware !== false; @@ -592,6 +582,70 @@ class selfserve_studio_graph ); } + /** + * @param array $payload + * @param array $permissions + * @return array{config_source:string,config:array|null,version_id:int|null} + */ + private function loadSimulationConfig( + int $departmentId, + array $payload, + array $permissions, + selfserve_config_versioning $versioning + ): array { + $configSource = $this->resolveSimulationConfigSource($payload, $permissions); + + if ($configSource === 'published') { + $version = $versioning->getPublishedV2Config($departmentId); + return [ + 'config_source' => $configSource, + 'config' => is_array($version['config'] ?? null) ? (array)$version['config'] : null, + 'version_id' => isset($version['version_id']) ? (int)$version['version_id'] : null, + ]; + } + + $draft = (new selfserve_config_versions_o())->selectLatestByDepartmentAndStatus( + $departmentId, + selfserve_config_versioning::STATUS_DRAFT + ); + if (!$draft->exists()) { + return [ + 'config_source' => $configSource, + 'config' => $versioning->snapshotLegacyConfig($departmentId), + 'version_id' => null, + ]; + } + + $config = (array)($draft->config_json->value() ?? []); + if (!$versioning->isV2Config($config)) { + $config = $versioning->migrateLegacyConfigToV2($config + ['department_id' => $departmentId]); + } + + return [ + 'config_source' => $configSource, + 'config' => $config, + 'version_id' => (int)$draft->id, + ]; + } + + /** + * @param array $payload + * @param array $permissions + */ + private function resolveSimulationConfigSource(array $payload, array $permissions): string + { + $configSource = strtolower(trim((string)($payload['config_source'] ?? 'draft'))); + if (!in_array($configSource, ['draft', 'published'], true)) { + $configSource = 'draft'; + } + + if ($configSource === 'draft' && !($permissions['can_view'] ?? false)) { + throw new \RuntimeException('Draft studio simulation requires list_department_selfserve_config_versions permission.'); + } + + return $configSource; + } + /** * @param array $payload * @param array $permissions @@ -605,21 +659,11 @@ class selfserve_studio_graph ?callable $progressCallback = null ): array { - $configSource = strtolower(trim((string)($payload['config_source'] ?? 'draft'))); - if (!in_array($configSource, ['draft', 'published'], true)) { - $configSource = 'draft'; - } - $versioning = new selfserve_config_versioning(); - if ($configSource === 'published') { - $version = $versioning->getPublishedV2Config($departmentId); - $config = is_array($version['config'] ?? null) ? (array)$version['config'] : null; - $versionId = isset($version['version_id']) ? (int)$version['version_id'] : null; - } else { - $version = $versioning->ensureDraftFromLegacy($departmentId, $userId, false); - $config = is_array($version['config'] ?? null) ? (array)$version['config'] : $versioning->snapshotLegacyConfig($departmentId); - $versionId = isset($version['id']) ? (int)$version['id'] : null; - } + $configContext = $this->loadSimulationConfig($departmentId, $payload, $permissions, $versioning); + $configSource = $configContext['config_source']; + $config = $configContext['config']; + $versionId = $configContext['version_id']; $includeHardware = filter_var($payload['include_hardware'] ?? true, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); $includeHardware = $includeHardware !== false; diff --git a/services/nginx/app/routes/departmentSelfserveStudioRoute.php b/services/nginx/app/routes/departmentSelfserveStudioRoute.php index adf3e87c..129fd496 100644 --- a/services/nginx/app/routes/departmentSelfserveStudioRoute.php +++ b/services/nginx/app/routes/departmentSelfserveStudioRoute.php @@ -109,7 +109,7 @@ class departmentSelfserveStudioRoute $this->post('/department/selfserve/studio/simulate', function (): void { global $response; - $user = $this->requireStudioUser('list_department_selfserve_vehicle_conditions'); + $user = $this->requireStudioUser('list_department_selfserve_config_versions'); self::requireParameters(['department', 'lane_id', 'reg']); $departmentId = (int)self::getParameter('department'); $laneId = (int)self::getParameter('lane_id'); @@ -129,11 +129,11 @@ class departmentSelfserveStudioRoute $response->error($exception->getMessage(), 422); } }, [ - 'list_department_selfserve_vehicle_conditions' => 'Run the self-serve studio simulator', + 'list_department_selfserve_config_versions' => 'Run the self-serve studio simulator', ]); $this->post('/department/selfserve/studio/path-outcomes/stream', function (): void { - $user = $this->requireStudioUser('list_department_selfserve_vehicle_conditions'); + $user = $this->requireStudioUser('list_department_selfserve_config_versions'); self::requireParameters(['department']); $departmentId = (int)self::getParameter('department'); $this->assertDepartmentAccess($user, $departmentId); @@ -169,12 +169,12 @@ class departmentSelfserveStudioRoute } exit; }, [ - 'list_department_selfserve_vehicle_conditions' => 'Stream grouped self-serve studio question path outcome progress', + 'list_department_selfserve_config_versions' => 'Stream grouped self-serve studio question path outcome progress', ]); $this->post('/department/selfserve/studio/path-outcomes', function (): void { global $response; - $user = $this->requireStudioUser('list_department_selfserve_vehicle_conditions'); + $user = $this->requireStudioUser('list_department_selfserve_config_versions'); self::requireParameters(['department']); $departmentId = (int)self::getParameter('department'); $this->assertDepartmentAccess($user, $departmentId); @@ -192,7 +192,7 @@ class departmentSelfserveStudioRoute $response->error($exception->getMessage(), 422); } }, [ - 'list_department_selfserve_vehicle_conditions' => 'Project grouped self-serve studio question path outcomes', + 'list_department_selfserve_config_versions' => 'Project grouped self-serve studio question path outcomes', ]); $this->post('/department/selfserve/studio/path-confirmations', function (): void { diff --git a/services/nginx/app/tests/Unit/Selfserve/SelfserveRouteWiringTest.php b/services/nginx/app/tests/Unit/Selfserve/SelfserveRouteWiringTest.php index 43fe9278..2bb7a1bd 100644 --- a/services/nginx/app/tests/Unit/Selfserve/SelfserveRouteWiringTest.php +++ b/services/nginx/app/tests/Unit/Selfserve/SelfserveRouteWiringTest.php @@ -120,7 +120,26 @@ it('wires the all-in-one self-serve studio replacement endpoints', function (): 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');