Add support for selfserve_enabled lanes and synchronize behavior across tasks, sessions, and projections

- Introduced `selfserve_enabled` property for `department_lanes` with schema update, object properties, and associated methods/tests.
- Enhanced `selfserve_wash_flow` and session logic to respect lane self-serve settings, including block handling and task filtering.
- Updated API routes and Studio Graph projections to include `selfserve_enabled` in payloads and progress callbacks.
- Added unit tests for session statuses, lane configuration, and blocking behavior due to disabled self-serve settings.
This commit is contained in:
Jeppe Bundgaard
2026-04-29 17:28:42 +02:00
parent 5875371d13
commit 8bf957b273
18 changed files with 924 additions and 101 deletions
@@ -103,6 +103,12 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
$snapshot = $this->buildEligibilitySnapshot($laneId, $reg, $customerNumber, $vehicleTypeIdOverride, $options);
$session = $this->findLatestOpenSession($laneId, $snapshot['reg'], $snapshot['customer_number']);
if (($snapshot['evaluation_trace']['disabled_lane'] ?? false) === true) {
return $session->exists()
? $this->getSessionSummary((int)$session->id)
: $this->formatBlockedSessionSummary($snapshot);
}
if (!$session->exists()) {
$session = (new selfserve_wash_sessions_o())->add(
$laneId,
@@ -155,6 +161,9 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
throw new \RuntimeException('No active self-serve wash session found for the lane.');
}
$summary = $this->synchronizeSession($laneId, $normalizedReg, null, false, null, false);
if (empty($summary['session']['id'])) {
throw new \RuntimeException((string)($summary['blocked_reason'] ?? 'Self-serve is disabled for this lane.'));
}
$session = (new selfserve_wash_sessions_o())->select((int)$summary['session']['id']);
}
@@ -324,6 +333,24 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
$answerRows = (new selfserve_wash_session_answers_o())->listBySession($sessionId);
$answers = $this->buildSessionQuestions($session, $answerRows);
$metadata = is_array($session->metadata_json->value()) ? $session->metadata_json->value() : [];
$allowedServices = $this->normalizeServiceNames(
is_array($metadata['allowed_services'] ?? null) ? (array)$metadata['allowed_services'] : []
);
$machineAvailable = array_key_exists('machine_available', $metadata)
? (bool)$metadata['machine_available']
: ($lane->exists() && !empty($lane->relay_machine_id->value()));
$allVisibleQuestionsAnswered = array_key_exists('all_visible_questions_answered', $metadata)
? (bool)$metadata['all_visible_questions_answered']
: true;
if (!array_key_exists('all_visible_questions_answered', $metadata)) {
foreach ($answers as $answer) {
if (($answer['answer'] ?? null) === null) {
$allVisibleQuestionsAnswered = false;
break;
}
}
}
$tasks = array_map(function (array $row): array {
return [
@@ -335,6 +362,9 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
'dynamic_images_vehicle_type' => $row['dynamic_images_vehicle_type'] === null ? null : (int)$row['dynamic_images_vehicle_type']
];
}, (new selfserve_wash_session_tasks_o())->listBySession($sessionId));
if (array_key_exists('allowed_services', $metadata) || (bool)$session->allowed->value() === false) {
$tasks = $this->filterTasksForAllowedServices($tasks, $allowedServices);
}
$tasks = (new selfserve_task_attachment_payloads())->attachToTasks($tasks);
$events = array_map(function (array $row): array {
@@ -353,8 +383,12 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
'questions' => $answers,
'tasks' => $tasks,
'events' => $events,
'config_version_id' => is_array($session->metadata_json->value()) ? ($session->metadata_json->value()['config_version_id'] ?? null) : null,
'evaluation_trace' => is_array($session->metadata_json->value()) ? ($session->metadata_json->value()['evaluation_trace'] ?? null) : null,
'allowed_services' => $allowedServices,
'machine_available' => $machineAvailable,
'all_visible_questions_answered' => $allVisibleQuestionsAnswered,
'allowed' => (bool)$session->allowed->value(),
'config_version_id' => $metadata['config_version_id'] ?? null,
'evaluation_trace' => $metadata['evaluation_trace'] ?? null,
];
}
@@ -453,6 +487,56 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
$departmentId = (int)$lane->department->value();
$machineTypeId = $lane->machine_type_id->value() === null ? null : (int)$lane->machine_type_id->value();
if (!$lane->isSelfServeEnabled()) {
$vehicle = $this->findVehicleByRegistration($normalizedReg);
$vehicleData = $vehicle?->asArray();
$vehicleTypeId = $this->resolveVehicleTypeId($vehicle, $vehicleTypeIdOverride);
$resolvedCustomerNumber = $customerNumber ?? ($vehicle !== null ? (int)$vehicle->customer_id->value() : null);
return [
'lane' => $lane->asArray(),
'machine_type' => null,
'vehicle' => $vehicleData,
'reg' => $normalizedReg,
'customer_number' => $resolvedCustomerNumber,
'vehicle_type_id' => $vehicleTypeId,
'answers' => [],
'persisted_answers' => [],
'persisted_answer_customer_number' => null,
'answer_overrides' => [],
'answer_sources' => [],
'questions' => [],
'conditions' => [],
'tasks' => [],
'allowed_services' => [],
'machine_available' => false,
'all_visible_questions_answered' => false,
'allowed' => false,
'blocked_reason' => 'Self-serve is disabled for this lane.',
'config_version_id' => null,
'config_source' => (string)($options['config_source'] ?? 'published'),
'evaluation_trace' => [
'blocked' => true,
'blocking_reasons' => ['LANE_SELFSERVE_DISABLED'],
'disabled_lane' => true,
'message' => 'Self-serve is disabled for this lane.',
'visibility_condition_results' => [],
'condition_results' => [],
'visibility_expression_traces' => [],
'condition_expression_traces' => [],
'task_gates' => [],
'visible_question_ids' => [],
],
'debug_candidates' => [
'questions' => [],
'conditions' => [],
'rules' => [],
'tasks' => [],
'actions' => [],
'visible_answers' => [],
],
];
}
$configSource = (string)($options['config_source'] ?? 'published');
$publishedConfigVersionId = $options['config_version_id'] ?? null;
$publishedConfigPayload = is_array($options['config_payload'] ?? null) ? (array)$options['config_payload'] : null;
@@ -585,6 +669,7 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
$machineAllowed = $allVisibleQuestionsAnswered
&& $machineAvailable
&& in_array(selfserve_lane_services::MACHINE->name, $allowedServices, true);
$visibleTasks = $this->filterTasksForAllowedServices($activeTasks, $allowedServices);
$machineType = null;
if ($machineTypeId !== null) {
@@ -608,7 +693,7 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
'answer_sources' => $answerSources,
'questions' => $visibleQuestions,
'conditions' => $serviceConditionResults,
'tasks' => $activeTasks,
'tasks' => $visibleTasks,
'allowed_services' => $allowedServices,
'machine_available' => $machineAvailable,
'all_visible_questions_answered' => $allVisibleQuestionsAnswered,
@@ -713,6 +798,7 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
'machine_available' => $snapshot['machine_available'],
'all_visible_questions_answered' => $snapshot['all_visible_questions_answered'],
'allowed' => $snapshot['allowed'],
'blocked_reason' => $snapshot['blocked_reason'] ?? null,
'session' => $session,
'config_version_id' => $snapshot['config_version_id'] ?? null,
'config_source' => $snapshot['config_source'] ?? null,
@@ -720,6 +806,24 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
];
}
protected function formatBlockedSessionSummary(array $snapshot): array
{
return [
'session' => null,
'lane' => $snapshot['lane'],
'machine_type' => $snapshot['machine_type'],
'questions' => [],
'tasks' => [],
'events' => [],
'allowed' => false,
'allowed_services' => [],
'machine_available' => false,
'blocked_reason' => $snapshot['blocked_reason'] ?? 'Self-serve is disabled for this lane.',
'config_version_id' => $snapshot['config_version_id'] ?? null,
'evaluation_trace' => $snapshot['evaluation_trace'] ?? null,
];
}
/**
* @param array<string,mixed> $snapshot
* @param array<string,mixed> $options
@@ -2789,8 +2893,12 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
'deleted_at' => null,
...($customerNumber !== null ? ['customer_number' => $customerNumber] : []),
],
['id']
['id', 'status']
);
$rows = array_values(array_filter(
$rows,
static fn(array $row): bool => !selfserve_wash_sessions_o::isTerminalStatus($row['status'] ?? null)
));
if ($rows === []) {
return new selfserve_wash_sessions_o();
}
@@ -2900,6 +3008,37 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
}
}
/**
* @param array<int,array<string,mixed>> $tasks
* @param array<int,string> $allowedServices
* @return array<int,array<string,mixed>>
*/
protected function filterTasksForAllowedServices(array $tasks, array $allowedServices): array
{
if (in_array(selfserve_lane_services::MACHINE->name, $allowedServices, true)) {
return array_values($tasks);
}
return array_values(array_filter(
$tasks,
fn(array $task): bool => !$this->taskUsesMachineControls($task)
));
}
protected function taskUsesMachineControls(array $task): bool
{
if (in_array(selfserve_lane_services::MACHINE->name, $this->normalizeServiceNames($this->normalizeJsonArray($task['services'] ?? null)), true)) {
return true;
}
if ($this->normalizeButtonList($task['buttons'] ?? null) !== []) {
return true;
}
$dynamicImagesVehicleType = $task['dynamic_images_vehicle_type'] ?? null;
return $dynamicImagesVehicleType !== null && $dynamicImagesVehicleType !== '';
}
protected function normalizeServiceNames(array $services): array
{
$normalized = [];