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:
@@ -13,6 +13,7 @@ require_once WD . '/objects/department_selfserve_condition_rules_o.php';
|
||||
require_once WD . '/objects/department_selfserve_conditions_o.php';
|
||||
require_once WD . '/objects/department_selfserve_questions_o.php';
|
||||
require_once WD . '/objects/department_selfserve_tasks_o.php';
|
||||
require_once WD . '/objects/department_lanes_o.php';
|
||||
require_once WD . '/objects/selfserve_config_versions_o.php';
|
||||
require_once WD . '/modules/selfserve/classes/selfserve_task_attachment_payloads.php';
|
||||
|
||||
@@ -589,7 +590,13 @@ class selfserve_studio_graph
|
||||
* @param array<string,bool> $permissions
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function projectPathOutcomes(int $departmentId, array $payload, ?int $userId = null, array $permissions = []): array
|
||||
public function projectPathOutcomes(
|
||||
int $departmentId,
|
||||
array $payload,
|
||||
?int $userId = null,
|
||||
array $permissions = [],
|
||||
?callable $progressCallback = null
|
||||
): array
|
||||
{
|
||||
$configSource = strtolower(trim((string)($payload['config_source'] ?? 'draft')));
|
||||
if (!in_array($configSource, ['draft', 'published'], true)) {
|
||||
@@ -669,8 +676,7 @@ class selfserve_studio_graph
|
||||
$vehicleTypeIds[] = null;
|
||||
}
|
||||
|
||||
$maxStates = (int)($payload['max_states'] ?? 2048);
|
||||
$maxStates = max(1, min(2048, $maxStates));
|
||||
$maxStates = $this->pathLimit($payload['max_states'] ?? null);
|
||||
$reg = trim((string)($payload['reg'] ?? $defaults['reg'] ?? 'TEST123'));
|
||||
if ($reg === '') {
|
||||
$reg = 'TEST123';
|
||||
@@ -686,12 +692,13 @@ class selfserve_studio_graph
|
||||
$stateCount = 0;
|
||||
$terminalPathCount = 0;
|
||||
$questionIds = [];
|
||||
$pathSampleLimit = max(1, min(200, (int)($payload['path_sample_limit'] ?? 100)));
|
||||
$pathSampleLimit = $this->pathLimit($payload['path_sample_limit'] ?? null);
|
||||
$paths = [];
|
||||
$scenarioCount = max(1, count($vehicleTypeIds));
|
||||
|
||||
foreach ($vehicleTypeIds as $scenarioVehicleTypeId) {
|
||||
$remainingStates = $maxStates - $stateCount;
|
||||
if ($remainingStates <= 0) {
|
||||
foreach ($vehicleTypeIds as $scenarioIndex => $scenarioVehicleTypeId) {
|
||||
$remainingStates = $maxStates === null ? null : $maxStates - $stateCount;
|
||||
if ($remainingStates !== null && $remainingStates <= 0) {
|
||||
$truncated = true;
|
||||
break;
|
||||
}
|
||||
@@ -746,18 +753,104 @@ class selfserve_studio_graph
|
||||
);
|
||||
};
|
||||
|
||||
$projection = $this->projectPathOutcomesFromSimulator($simulate, [
|
||||
$projectionOptions = [
|
||||
'scope' => $scenarioScope,
|
||||
'max_states' => $remainingStates,
|
||||
'path_sample_limit' => max(0, $pathSampleLimit - count($paths)),
|
||||
]);
|
||||
'path_sample_limit' => $pathSampleLimit === null ? null : max(0, $pathSampleLimit - count($paths)),
|
||||
'progress_callback' => function (array $projection) use (
|
||||
$progressCallback,
|
||||
&$outcomes,
|
||||
&$paths,
|
||||
&$stateCount,
|
||||
&$terminalPathCount,
|
||||
&$questionIds,
|
||||
$maxStates,
|
||||
$pathSampleLimit,
|
||||
$scenarioIndex,
|
||||
$scenarioCount,
|
||||
$departmentId,
|
||||
$lookups,
|
||||
$laneId,
|
||||
$vehicleTypeId,
|
||||
$reg,
|
||||
$customerNumber,
|
||||
$configSource,
|
||||
$versionId,
|
||||
$hardwareMode
|
||||
): void {
|
||||
if ($progressCallback === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$partialOutcomes = array_merge($outcomes, array_values((array)($projection['outcomes'] ?? [])));
|
||||
$partialPaths = array_merge($paths, array_values((array)($projection['paths'] ?? [])));
|
||||
if ($pathSampleLimit !== null && count($partialPaths) > $pathSampleLimit) {
|
||||
$partialPaths = array_slice($partialPaths, 0, $pathSampleLimit);
|
||||
}
|
||||
|
||||
$partialQuestionIds = $questionIds;
|
||||
foreach ((array)($projection['summary']['question_ids'] ?? []) as $questionId) {
|
||||
$partialQuestionIds[(int)$questionId] = true;
|
||||
}
|
||||
|
||||
$projectionProgress = is_array($projection['progress'] ?? null) ? (array)$projection['progress'] : [];
|
||||
$scenarioPercent = (float)($projectionProgress['percent'] ?? 0);
|
||||
$overallPercent = min(99.0, (($scenarioIndex + ($scenarioPercent / 100)) / $scenarioCount) * 100);
|
||||
|
||||
$partialPayload = $this->pathOutcomesPayload(
|
||||
[
|
||||
'department_id' => $departmentId,
|
||||
'department' => $this->labelFor('departments', $departmentId, $lookups),
|
||||
'lane_id' => $laneId,
|
||||
'lane' => $this->labelFor('lanes', $laneId, $lookups),
|
||||
'vehicle_type_id' => $vehicleTypeId,
|
||||
'vehicle_type' => $vehicleTypeId === null ? 'All current vehicle types' : $this->labelFor('vehicle_types', $vehicleTypeId, $lookups),
|
||||
'vehicle_type_count' => $scenarioCount,
|
||||
'registration' => $reg,
|
||||
'customer_number' => $customerNumber,
|
||||
'config_source' => $configSource,
|
||||
'config_version_id' => $versionId,
|
||||
'hardware_mode' => $hardwareMode,
|
||||
'max_states' => $maxStates,
|
||||
],
|
||||
$partialOutcomes,
|
||||
$partialPaths,
|
||||
[],
|
||||
false,
|
||||
$maxStates,
|
||||
$stateCount + (int)($projection['summary']['state_count'] ?? 0),
|
||||
$terminalPathCount + (int)($projection['summary']['terminal_path_count'] ?? 0),
|
||||
$partialQuestionIds,
|
||||
[
|
||||
'complete' => false,
|
||||
'percent' => (int)floor($overallPercent),
|
||||
'state_count' => $stateCount + (int)($projection['summary']['state_count'] ?? 0),
|
||||
'pending_state_count' => (int)($projectionProgress['pending_state_count'] ?? 0),
|
||||
'terminal_path_count' => $terminalPathCount + (int)($projection['summary']['terminal_path_count'] ?? 0),
|
||||
'scenario_index' => $scenarioIndex + 1,
|
||||
'scenario_count' => $scenarioCount,
|
||||
]
|
||||
);
|
||||
$progressCallback($partialPayload);
|
||||
},
|
||||
];
|
||||
if ($remainingStates === null) {
|
||||
unset($projectionOptions['max_states']);
|
||||
}
|
||||
if ($pathSampleLimit === null) {
|
||||
unset($projectionOptions['path_sample_limit']);
|
||||
}
|
||||
$projection = $this->projectPathOutcomesFromSimulator($simulate, $projectionOptions);
|
||||
foreach ((array)($projection['outcomes'] ?? []) as $outcome) {
|
||||
if (is_array($outcome)) {
|
||||
$outcomes[] = $outcome;
|
||||
}
|
||||
}
|
||||
foreach ((array)($projection['paths'] ?? []) as $path) {
|
||||
if (is_array($path) && count($paths) < $pathSampleLimit) {
|
||||
if (!is_array($path)) {
|
||||
continue;
|
||||
}
|
||||
if ($pathSampleLimit === null || count($paths) < $pathSampleLimit) {
|
||||
$paths[] = $path;
|
||||
}
|
||||
}
|
||||
@@ -772,23 +865,12 @@ class selfserve_studio_graph
|
||||
}
|
||||
}
|
||||
|
||||
usort($outcomes, static fn(array $left, array $right): int => ((int)($right['path_count'] ?? 0) <=> (int)($left['path_count'] ?? 0))
|
||||
?: strcmp((string)($left['summary'] ?? ''), (string)($right['summary'] ?? '')));
|
||||
foreach ($outcomes as $index => &$outcome) {
|
||||
$outcome['id'] = 'outcome-' . ($index + 1);
|
||||
}
|
||||
unset($outcome);
|
||||
foreach ($paths as $index => &$path) {
|
||||
$path['id'] = 'path-' . ($index + 1);
|
||||
}
|
||||
unset($path);
|
||||
|
||||
if ($truncated) {
|
||||
$warnings[] = 'Path projection was truncated at ' . $maxStates . ' explored state(s). Narrow the lane or vehicle type filters to inspect more paths.';
|
||||
}
|
||||
|
||||
return [
|
||||
'scope' => [
|
||||
return $this->pathOutcomesPayload(
|
||||
[
|
||||
'department_id' => $departmentId,
|
||||
'department' => $this->labelFor('departments', $departmentId, $lookups),
|
||||
'lane_id' => $laneId,
|
||||
@@ -803,20 +885,24 @@ class selfserve_studio_graph
|
||||
'hardware_mode' => $hardwareMode,
|
||||
'max_states' => $maxStates,
|
||||
],
|
||||
'summary' => [
|
||||
$outcomes,
|
||||
$paths,
|
||||
$warnings,
|
||||
$truncated,
|
||||
$maxStates,
|
||||
$stateCount,
|
||||
$terminalPathCount,
|
||||
$questionIds,
|
||||
[
|
||||
'complete' => true,
|
||||
'percent' => 100,
|
||||
'state_count' => $stateCount,
|
||||
'pending_state_count' => 0,
|
||||
'terminal_path_count' => $terminalPathCount,
|
||||
'outcome_count' => count($outcomes),
|
||||
'question_count' => count($questionIds),
|
||||
'question_ids' => array_values(array_map('intval', array_keys($questionIds))),
|
||||
'max_states' => $maxStates,
|
||||
'path_sample_count' => count($paths),
|
||||
],
|
||||
'outcomes' => array_values($outcomes),
|
||||
'paths' => array_values($paths),
|
||||
'warnings' => array_values(array_unique($warnings)),
|
||||
'truncated' => $truncated,
|
||||
];
|
||||
'scenario_index' => $scenarioCount,
|
||||
'scenario_count' => $scenarioCount,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -826,10 +912,11 @@ class selfserve_studio_graph
|
||||
*/
|
||||
public function projectPathOutcomesFromSimulator(callable $simulate, array $options = []): array
|
||||
{
|
||||
$maxStates = (int)($options['max_states'] ?? 2048);
|
||||
$maxStates = max(1, min(2048, $maxStates));
|
||||
$maxStates = $this->pathLimit($options['max_states'] ?? null);
|
||||
$sampleLimit = max(1, min(10, (int)($options['sample_limit'] ?? 5)));
|
||||
$pathSampleLimit = max(0, min(200, (int)($options['path_sample_limit'] ?? 100)));
|
||||
$pathSampleLimit = $this->pathLimit($options['path_sample_limit'] ?? null);
|
||||
$progressCallback = is_callable($options['progress_callback'] ?? null) ? $options['progress_callback'] : null;
|
||||
$progressIntervalStates = max(1, (int)($options['progress_interval_states'] ?? 128));
|
||||
$scope = is_array($options['scope'] ?? null) ? (array)$options['scope'] : [];
|
||||
$stack = [[
|
||||
'answers' => [],
|
||||
@@ -844,7 +931,7 @@ class selfserve_studio_graph
|
||||
$truncated = false;
|
||||
|
||||
while ($stack !== []) {
|
||||
if ($stateCount >= $maxStates) {
|
||||
if ($maxStates !== null && $stateCount >= $maxStates) {
|
||||
$truncated = true;
|
||||
break;
|
||||
}
|
||||
@@ -883,39 +970,55 @@ class selfserve_studio_graph
|
||||
];
|
||||
}
|
||||
}
|
||||
if ($progressCallback !== null && $stateCount % $progressIntervalStates === 0) {
|
||||
$progressCallback($this->pathOutcomesProjectionPayload(
|
||||
$scope,
|
||||
$groups,
|
||||
$paths,
|
||||
$truncated,
|
||||
$maxStates,
|
||||
$stateCount,
|
||||
$terminalPathCount,
|
||||
$questionIds,
|
||||
count($stack)
|
||||
));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$terminalPathCount++;
|
||||
$chain = is_array($state['chain'] ?? null) ? (array)$state['chain'] : [];
|
||||
$this->addPathOutcomeGroup($groups, $simulation, $chain, $scope, $sampleLimit);
|
||||
if ($pathSampleLimit > 0 && count($paths) < $pathSampleLimit) {
|
||||
if ($pathSampleLimit === null || count($paths) < $pathSampleLimit) {
|
||||
$paths[] = $this->pathResultFromSimulation($simulation, $chain, $scope);
|
||||
}
|
||||
|
||||
if ($progressCallback !== null && $stateCount % $progressIntervalStates === 0) {
|
||||
$progressCallback($this->pathOutcomesProjectionPayload(
|
||||
$scope,
|
||||
$groups,
|
||||
$paths,
|
||||
$truncated,
|
||||
$maxStates,
|
||||
$stateCount,
|
||||
$terminalPathCount,
|
||||
$questionIds,
|
||||
count($stack)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
$outcomes = $this->finalizePathOutcomeGroups($groups);
|
||||
foreach ($paths as $index => &$path) {
|
||||
$path['id'] = 'path-' . ($index + 1);
|
||||
}
|
||||
unset($path);
|
||||
|
||||
return [
|
||||
'scope' => $scope,
|
||||
'summary' => [
|
||||
'state_count' => $stateCount,
|
||||
'terminal_path_count' => $terminalPathCount,
|
||||
'outcome_count' => count($outcomes),
|
||||
'question_count' => count($questionIds),
|
||||
'question_ids' => array_values(array_map('intval', array_keys($questionIds))),
|
||||
'max_states' => $maxStates,
|
||||
'path_sample_count' => count($paths),
|
||||
],
|
||||
'outcomes' => $outcomes,
|
||||
'paths' => array_values($paths),
|
||||
'warnings' => $truncated ? ['Path projection was truncated at ' . $maxStates . ' explored state(s).'] : [],
|
||||
'truncated' => $truncated,
|
||||
];
|
||||
return $this->pathOutcomesProjectionPayload(
|
||||
$scope,
|
||||
$groups,
|
||||
$paths,
|
||||
$truncated,
|
||||
$maxStates,
|
||||
$stateCount,
|
||||
$terminalPathCount,
|
||||
$questionIds,
|
||||
count($stack)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1043,6 +1146,7 @@ class selfserve_studio_graph
|
||||
'relay_machine_cleaner_id',
|
||||
'machine_type_id',
|
||||
'dynamic_image_id',
|
||||
'selfserve_enabled',
|
||||
], ['department' => $departmentId]);
|
||||
$machineTypeRows = $this->fetchRows('selfserve_machine_types', ['id', 'name', 'description'], []);
|
||||
$productRows = $this->fetchRows('products', ['id', 'name', 'description', 'price', 'subscription_allowed', 'category', 'piktogram', 'is_wash', 'order_priority'], []);
|
||||
@@ -2064,6 +2168,15 @@ class selfserve_studio_graph
|
||||
$availableColumns = $this->tableColumns('department_lanes');
|
||||
$updates = [];
|
||||
$params = [':id' => $id];
|
||||
$wasSelfServeEnabled = null;
|
||||
|
||||
if (array_key_exists('selfserve_enabled', $data) && in_array('selfserve_enabled', $availableColumns, true)) {
|
||||
$statement = db::getPDO()->prepare(
|
||||
'SELECT selfserve_enabled FROM department_lanes WHERE id = :id AND deleted_at IS NULL'
|
||||
);
|
||||
$statement->execute([':id' => $id]);
|
||||
$wasSelfServeEnabled = ((int)($statement->fetch(\PDO::FETCH_ASSOC)['selfserve_enabled'] ?? 1)) === 1;
|
||||
}
|
||||
|
||||
$fields = ['name', ...$this->laneOptionalFields()];
|
||||
foreach ($fields as $field) {
|
||||
@@ -2083,6 +2196,14 @@ class selfserve_studio_graph
|
||||
db::getPDO()->prepare(
|
||||
'UPDATE department_lanes SET ' . implode(', ', $updates) . ' WHERE id = :id AND deleted_at IS NULL'
|
||||
)->execute($params);
|
||||
|
||||
if (
|
||||
$wasSelfServeEnabled === true
|
||||
&& array_key_exists(':selfserve_enabled', $params)
|
||||
&& (int)$params[':selfserve_enabled'] === 0
|
||||
) {
|
||||
\objects\department_lanes_o::disableSelfServeRelaysBestEffort($id);
|
||||
}
|
||||
}
|
||||
|
||||
private function softDeleteLane(int $departmentId, int $id): void
|
||||
@@ -2106,11 +2227,16 @@ class selfserve_studio_graph
|
||||
'relay_machine_cleaner_id',
|
||||
'dynamic_image_id',
|
||||
'machine_type_id',
|
||||
'selfserve_enabled',
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeLaneField(string $field, mixed $value): mixed
|
||||
{
|
||||
if ($field === 'selfserve_enabled') {
|
||||
return \objects\department_lanes_o::normalizeSelfServeEnabledValue($value) ? 1 : 0;
|
||||
}
|
||||
|
||||
if (in_array($field, ['dynamic_image_id', 'machine_type_id'], true)) {
|
||||
return $this->nullableInt($value);
|
||||
}
|
||||
@@ -3396,6 +3522,130 @@ class selfserve_studio_graph
|
||||
return null;
|
||||
}
|
||||
|
||||
private function pathLimit(mixed $value): ?int
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parsed = (int)$value;
|
||||
return $parsed > 0 ? $parsed : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $scope
|
||||
* @param array<string,array<string,mixed>> $groups
|
||||
* @param array<int,array<string,mixed>> $paths
|
||||
* @param array<int,bool> $questionIds
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function pathOutcomesProjectionPayload(
|
||||
array $scope,
|
||||
array $groups,
|
||||
array $paths,
|
||||
bool $truncated,
|
||||
?int $maxStates,
|
||||
int $stateCount,
|
||||
int $terminalPathCount,
|
||||
array $questionIds,
|
||||
int $pendingStateCount
|
||||
): array {
|
||||
$warnings = [];
|
||||
if ($truncated && $maxStates !== null) {
|
||||
$warnings[] = 'Path projection was truncated at ' . $maxStates . ' explored state(s).';
|
||||
}
|
||||
|
||||
$knownStateCount = max(1, $stateCount + $pendingStateCount);
|
||||
$complete = !$truncated && $pendingStateCount === 0;
|
||||
$percent = $complete ? 100 : min(99, max(1, (int)floor(($stateCount / $knownStateCount) * 100)));
|
||||
|
||||
return $this->pathOutcomesPayload(
|
||||
$scope,
|
||||
$this->finalizePathOutcomeGroups($groups),
|
||||
$paths,
|
||||
$warnings,
|
||||
$truncated,
|
||||
$maxStates,
|
||||
$stateCount,
|
||||
$terminalPathCount,
|
||||
$questionIds,
|
||||
[
|
||||
'complete' => $complete,
|
||||
'percent' => $percent,
|
||||
'state_count' => $stateCount,
|
||||
'pending_state_count' => $pendingStateCount,
|
||||
'terminal_path_count' => $terminalPathCount,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $scope
|
||||
* @param array<int,array<string,mixed>> $outcomes
|
||||
* @param array<int,array<string,mixed>> $paths
|
||||
* @param array<int,string> $warnings
|
||||
* @param array<int,bool>|array<int,int> $questionIds
|
||||
* @param array<string,mixed> $progress
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function pathOutcomesPayload(
|
||||
array $scope,
|
||||
array $outcomes,
|
||||
array $paths,
|
||||
array $warnings,
|
||||
bool $truncated,
|
||||
?int $maxStates,
|
||||
int $stateCount,
|
||||
int $terminalPathCount,
|
||||
array $questionIds,
|
||||
array $progress = []
|
||||
): array {
|
||||
usort($outcomes, static fn(array $left, array $right): int => ((int)($right['path_count'] ?? 0) <=> (int)($left['path_count'] ?? 0))
|
||||
?: strcmp((string)($left['summary'] ?? ''), (string)($right['summary'] ?? '')));
|
||||
foreach ($outcomes as $index => &$outcome) {
|
||||
$outcome['id'] = 'outcome-' . ($index + 1);
|
||||
}
|
||||
unset($outcome);
|
||||
|
||||
foreach ($paths as $index => &$path) {
|
||||
$path['id'] = 'path-' . ($index + 1);
|
||||
}
|
||||
unset($path);
|
||||
|
||||
$questionIdValues = [];
|
||||
foreach ($questionIds as $key => $value) {
|
||||
$questionIdValues[] = $value === true ? (int)$key : (int)$value;
|
||||
}
|
||||
$questionIdValues = array_values(array_unique(array_filter($questionIdValues, static fn(int $id): bool => $id > 0)));
|
||||
sort($questionIdValues);
|
||||
|
||||
$progress = array_merge([
|
||||
'complete' => !$truncated,
|
||||
'percent' => $truncated ? 99 : 100,
|
||||
'state_count' => $stateCount,
|
||||
'pending_state_count' => 0,
|
||||
'terminal_path_count' => $terminalPathCount,
|
||||
], $progress);
|
||||
|
||||
return [
|
||||
'scope' => $scope,
|
||||
'summary' => [
|
||||
'state_count' => $stateCount,
|
||||
'terminal_path_count' => $terminalPathCount,
|
||||
'outcome_count' => count($outcomes),
|
||||
'question_count' => count($questionIdValues),
|
||||
'question_ids' => $questionIdValues,
|
||||
'max_states' => $maxStates,
|
||||
'path_sample_count' => count($paths),
|
||||
],
|
||||
'outcomes' => array_values($outcomes),
|
||||
'paths' => array_values($paths),
|
||||
'warnings' => array_values(array_unique($warnings)),
|
||||
'truncated' => $truncated,
|
||||
'progress' => $progress,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,array<string,mixed>> $groups
|
||||
* @param array<string,mixed> $simulation
|
||||
|
||||
Reference in New Issue
Block a user