Add department self-serve config versioning routes with lifecycle actions (list, validate, publish, rollback) and associated logic for managing config versions.
This commit is contained in:
@@ -4075,6 +4075,20 @@ paths:
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: vehicle_type_id
|
||||
in: query
|
||||
required: false
|
||||
description: Optional vehicle type override used when no vehicle is found by registration plate.
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
- name: vehicle_type
|
||||
in: query
|
||||
required: false
|
||||
description: Backward-compatible alias of `vehicle_type_id`.
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully evaluated self-serve eligibility
|
||||
@@ -4109,6 +4123,20 @@ paths:
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: vehicle_type_id
|
||||
in: query
|
||||
required: false
|
||||
description: Optional vehicle type override used to refresh summary data for unknown or reassigned plates.
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
- name: vehicle_type
|
||||
in: query
|
||||
required: false
|
||||
description: Backward-compatible alias of `vehicle_type_id`.
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
responses:
|
||||
'200':
|
||||
description: Successfully retrieved self-serve wash summary
|
||||
@@ -7334,14 +7362,25 @@ paths:
|
||||
vehicle_type_id:
|
||||
type: integer
|
||||
nullable: true
|
||||
included_minutes:
|
||||
type: integer
|
||||
nullable: true
|
||||
machine_type_id:
|
||||
type: integer
|
||||
nullable: true
|
||||
machine_relay_enabled:
|
||||
type: boolean
|
||||
machine_relay_enabled_at:
|
||||
type: string
|
||||
nullable: true
|
||||
machine_start_triggered:
|
||||
type: boolean
|
||||
machine_start_triggered_at:
|
||||
type: string
|
||||
nullable: true
|
||||
wash_started_at:
|
||||
type: string
|
||||
nullable: true
|
||||
created_at:
|
||||
type: string
|
||||
updated_at:
|
||||
@@ -12850,6 +12889,9 @@ components:
|
||||
customer_number:
|
||||
type: integer
|
||||
nullable: true
|
||||
vehicle_type_id:
|
||||
type: integer
|
||||
nullable: true
|
||||
questions:
|
||||
type: array
|
||||
items:
|
||||
|
||||
@@ -21,6 +21,23 @@ class selfserve_schema_bootstrap
|
||||
global $db;
|
||||
|
||||
$queries = [
|
||||
"CREATE TABLE IF NOT EXISTS selfserve_config_versions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
department_id INT NOT NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'DRAFT',
|
||||
version_number INT NOT NULL,
|
||||
config_json JSON NOT NULL,
|
||||
validation_result_json JSON NULL,
|
||||
source_version_id INT NULL,
|
||||
created_by INT NULL,
|
||||
published_at DATETIME NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP NULL DEFAULT NULL,
|
||||
INDEX idx_selfserve_config_versions_department_status (department_id, status),
|
||||
INDEX idx_selfserve_config_versions_department_version (department_id, version_number)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
||||
|
||||
"CREATE TABLE IF NOT EXISTS selfserve_machine_types (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
@@ -116,6 +133,16 @@ class selfserve_schema_bootstrap
|
||||
'machine_type_id',
|
||||
'ALTER TABLE department_selfserve_tasks ADD COLUMN machine_type_id INT NULL AFTER product'
|
||||
);
|
||||
self::ensureColumn(
|
||||
'department_selfserve_tasks',
|
||||
'gate_type',
|
||||
"ALTER TABLE department_selfserve_tasks ADD COLUMN gate_type VARCHAR(16) NULL DEFAULT 'ALWAYS' AFTER condition_id"
|
||||
);
|
||||
self::ensureColumn(
|
||||
'department_selfserve_tasks',
|
||||
'gate_ref_id',
|
||||
'ALTER TABLE department_selfserve_tasks ADD COLUMN gate_ref_id INT NULL AFTER gate_type'
|
||||
);
|
||||
|
||||
self::$initialized = true;
|
||||
}
|
||||
|
||||
@@ -5,9 +5,11 @@ namespace modules\selfserve\classes;
|
||||
require_once WD . '/modules/selfserve/interfaces/selfserve_condition_evaluator_i.php';
|
||||
require_once WD . '/modules/selfserve/helpers/selfserve_condition_rule_object_type.php';
|
||||
require_once WD . '/modules/selfserve/helpers/selfserve_condition_rule_type.php';
|
||||
require_once WD . '/modules/selfserve/helpers/selfserve_task_gate_type.php';
|
||||
|
||||
use modules\selfserve\helpers\selfserve_condition_rule_object_type;
|
||||
use modules\selfserve\helpers\selfserve_condition_rule_type;
|
||||
use modules\selfserve\helpers\selfserve_task_gate_type;
|
||||
use modules\selfserve\interfaces\selfserve_condition_evaluator_i;
|
||||
|
||||
class selfserve_condition_evaluator implements selfserve_condition_evaluator_i
|
||||
@@ -98,6 +100,20 @@ class selfserve_condition_evaluator implements selfserve_condition_evaluator_i
|
||||
return ($answers[$gateId] ?? null) === true;
|
||||
}
|
||||
|
||||
public function taskGateSatisfiedTyped(string $gateType, ?int $gateRefId, array $conditionResults, array $answers): bool
|
||||
{
|
||||
$typed = selfserve_task_gate_type::tryFrom(strtoupper(trim($gateType)));
|
||||
if ($typed === null) {
|
||||
return $this->taskGateSatisfied($gateRefId, $conditionResults, $answers);
|
||||
}
|
||||
|
||||
return match ($typed) {
|
||||
selfserve_task_gate_type::ALWAYS => true,
|
||||
selfserve_task_gate_type::CONDITION => ($gateRefId !== null && $gateRefId > 0) ? (($conditionResults[$gateRefId] ?? false) === true) : false,
|
||||
selfserve_task_gate_type::QUESTION => ($gateRefId !== null && $gateRefId > 0) ? (($answers[$gateRefId] ?? null) === true) : false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $rule
|
||||
* @param array<int,bool|null> $answers
|
||||
|
||||
@@ -0,0 +1,424 @@
|
||||
<?php
|
||||
|
||||
namespace modules\selfserve\classes;
|
||||
|
||||
require_once WD . '/classes/selfserve_schema_bootstrap.php';
|
||||
require_once WD . '/objects/departments_o.php';
|
||||
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/selfserve_config_versions_o.php';
|
||||
require_once WD . '/modules/selfserve/helpers/selfserve_task_gate_type.php';
|
||||
|
||||
use classes\selfserve_schema_bootstrap;
|
||||
use modules\selfserve\helpers\selfserve_task_gate_type;
|
||||
use objects\departments_o;
|
||||
use objects\department_selfserve_condition_rules_o;
|
||||
use objects\department_selfserve_conditions_o;
|
||||
use objects\department_selfserve_questions_o;
|
||||
use objects\department_selfserve_tasks_o;
|
||||
use objects\selfserve_config_versions_o;
|
||||
|
||||
class selfserve_config_versioning
|
||||
{
|
||||
public const STATUS_DRAFT = 'DRAFT';
|
||||
public const STATUS_PUBLISHED = 'PUBLISHED';
|
||||
public const STATUS_ARCHIVED = 'ARCHIVED';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
selfserve_schema_bootstrap::ensureTables();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{version_id:int,config:array<string,mixed>}|null
|
||||
*/
|
||||
public function getPublishedConfig(int $departmentId): ?array
|
||||
{
|
||||
$version = (new selfserve_config_versions_o())->selectLatestByDepartmentAndStatus($departmentId, self::STATUS_PUBLISHED);
|
||||
if (!$version->exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'version_id' => (int)$version->id,
|
||||
'config' => (array)($version->config_json->value() ?? []),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function ensureDraftFromLegacy(int $departmentId, ?int $createdBy = null, bool $forceRefresh = false): array
|
||||
{
|
||||
$versionObject = (new selfserve_config_versions_o())->selectLatestByDepartmentAndStatus($departmentId, self::STATUS_DRAFT);
|
||||
$config = $this->snapshotLegacyConfig($departmentId);
|
||||
$validation = $this->validateConfig($config);
|
||||
|
||||
if ($versionObject->exists()) {
|
||||
if ($forceRefresh) {
|
||||
$versionObject->config_json->set($config);
|
||||
$versionObject->validation_result_json->set($validation);
|
||||
}
|
||||
return $versionObject->asArray();
|
||||
}
|
||||
|
||||
$latestVersionNumber = $this->getLatestVersionNumber($departmentId);
|
||||
$newVersion = (new selfserve_config_versions_o())->add(
|
||||
$departmentId,
|
||||
self::STATUS_DRAFT,
|
||||
$latestVersionNumber + 1,
|
||||
$config,
|
||||
$validation,
|
||||
null,
|
||||
$createdBy,
|
||||
null,
|
||||
);
|
||||
return $newVersion->asArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
public function listVersions(int $departmentId): array
|
||||
{
|
||||
return (new selfserve_config_versions_o())->listByDepartment($departmentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function validateDraft(int $departmentId): array
|
||||
{
|
||||
$draft = (new selfserve_config_versions_o())->selectLatestByDepartmentAndStatus($departmentId, self::STATUS_DRAFT);
|
||||
if (!$draft->exists()) {
|
||||
$created = $this->ensureDraftFromLegacy($departmentId);
|
||||
$draft = (new selfserve_config_versions_o())->select((int)$created['id']);
|
||||
}
|
||||
|
||||
$validation = $this->validateConfig((array)($draft->config_json->value() ?? []));
|
||||
$draft->validation_result_json->set($validation);
|
||||
|
||||
return [
|
||||
'version' => $draft->asArray(),
|
||||
'validation' => $validation,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function publishDraft(int $departmentId, ?int $publishedBy = null): array
|
||||
{
|
||||
$draft = (new selfserve_config_versions_o())->selectLatestByDepartmentAndStatus($departmentId, self::STATUS_DRAFT);
|
||||
if (!$draft->exists()) {
|
||||
$created = $this->ensureDraftFromLegacy($departmentId, $publishedBy);
|
||||
$draft = (new selfserve_config_versions_o())->select((int)$created['id']);
|
||||
}
|
||||
|
||||
$validation = $this->validateConfig((array)($draft->config_json->value() ?? []));
|
||||
$draft->validation_result_json->set($validation);
|
||||
if (($validation['valid'] ?? false) !== true) {
|
||||
throw new \RuntimeException('Draft validation failed. Resolve errors before publishing.');
|
||||
}
|
||||
|
||||
$this->archivePublishedVersions($departmentId);
|
||||
$draft->status->set(self::STATUS_PUBLISHED);
|
||||
$draft->published_at->set(date('Y-m-d H:i:s'));
|
||||
if ($publishedBy !== null) {
|
||||
$draft->created_by->set($publishedBy);
|
||||
}
|
||||
|
||||
// Keep editing path open by creating a new draft cloned from newly published version.
|
||||
$publishedArray = $draft->asArray();
|
||||
$this->createDraftFromConfig($departmentId, (array)$draft->config_json->value(), (int)$draft->id, $publishedBy);
|
||||
|
||||
return $publishedArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function rollbackToVersion(int $departmentId, int $targetVersionId, ?int $createdBy = null): array
|
||||
{
|
||||
$target = (new selfserve_config_versions_o())->select($targetVersionId);
|
||||
if (!$target->exists() || (int)$target->department_id->value() !== $departmentId) {
|
||||
throw new \RuntimeException('Target version not found for department.');
|
||||
}
|
||||
|
||||
$config = (array)($target->config_json->value() ?? []);
|
||||
$validation = $this->validateConfig($config);
|
||||
if (($validation['valid'] ?? false) !== true) {
|
||||
throw new \RuntimeException('Target version cannot be rolled back because validation fails.');
|
||||
}
|
||||
|
||||
$this->archivePublishedVersions($departmentId);
|
||||
$latestVersionNumber = $this->getLatestVersionNumber($departmentId);
|
||||
$rollbackVersion = (new selfserve_config_versions_o())->add(
|
||||
$departmentId,
|
||||
self::STATUS_PUBLISHED,
|
||||
$latestVersionNumber + 1,
|
||||
$config,
|
||||
$validation,
|
||||
$targetVersionId,
|
||||
$createdBy,
|
||||
date('Y-m-d H:i:s'),
|
||||
);
|
||||
|
||||
$this->createDraftFromConfig($departmentId, $config, (int)$rollbackVersion->id, $createdBy);
|
||||
|
||||
return $rollbackVersion->asArray();
|
||||
}
|
||||
|
||||
public function syncDraftFromLegacyForDepartment(int $departmentId): void
|
||||
{
|
||||
if ($departmentId > 0) {
|
||||
$this->ensureDraftFromLegacy($departmentId, null, true);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->getKnownDepartmentIdsForSync() as $id) {
|
||||
$this->ensureDraftFromLegacy($id, null, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function snapshotLegacyConfig(int $departmentId): array
|
||||
{
|
||||
$questionsObject = new department_selfserve_questions_o();
|
||||
$conditionsObject = new department_selfserve_conditions_o();
|
||||
$tasksObject = new department_selfserve_tasks_o();
|
||||
$rulesObject = new department_selfserve_condition_rules_o();
|
||||
|
||||
$departmentFilter = [0, $departmentId];
|
||||
|
||||
$questions = $questionsObject->getFieldsWhereIn([
|
||||
'department' => $departmentFilter,
|
||||
'deleted_at' => null,
|
||||
], ['id', 'department', 'lane', 'product', 'condition_id', 'question', 'description', 'order_priority', 'created_at', 'updated_at']);
|
||||
|
||||
$conditions = $conditionsObject->getFieldsWhereIn([
|
||||
'department' => $departmentFilter,
|
||||
'deleted_at' => null,
|
||||
], ['id', 'department', 'lane', 'product', 'machine_type_id', 'condition_id', 'name', 'description', 'created_at', 'updated_at']);
|
||||
|
||||
$tasks = $tasksObject->getFieldsWhereIn([
|
||||
'department' => $departmentFilter,
|
||||
'deleted_at' => null,
|
||||
], ['id', 'department', 'lane', 'product', 'machine_type_id', 'condition_id', 'gate_type', 'gate_ref_id', 'task', 'description', 'order_priority', 'services', 'buttons', 'dynamic_images_vehicle_type', 'created_at', 'updated_at']);
|
||||
|
||||
$conditionIds = array_map(static fn(array $condition): int => (int)$condition['id'], $conditions);
|
||||
$rules = $conditionIds === []
|
||||
? []
|
||||
: $rulesObject->getFieldsWhereIn([
|
||||
'condition_id' => $conditionIds,
|
||||
'deleted_at' => null,
|
||||
], ['id', 'condition_id', 'type', 'object_type', 'object_id', 'name', 'description']);
|
||||
|
||||
$tasks = array_map(function (array $task): array {
|
||||
$gateType = selfserve_task_gate_type::tryFrom((string)($task['gate_type'] ?? ''));
|
||||
if ($gateType === null) {
|
||||
$legacyGateId = $this->nullableInt($task['condition_id'] ?? null);
|
||||
$task['gate_type'] = $legacyGateId === null
|
||||
? selfserve_task_gate_type::ALWAYS->value
|
||||
: selfserve_task_gate_type::CONDITION->value;
|
||||
$task['gate_ref_id'] = $legacyGateId;
|
||||
}
|
||||
return $task;
|
||||
}, $tasks);
|
||||
|
||||
usort($questions, static fn(array $a, array $b): int => (int)$a['id'] <=> (int)$b['id']);
|
||||
usort($conditions, static fn(array $a, array $b): int => (int)$a['id'] <=> (int)$b['id']);
|
||||
usort($rules, static fn(array $a, array $b): int => (int)$a['id'] <=> (int)$b['id']);
|
||||
usort($tasks, static fn(array $a, array $b): int => (int)$a['id'] <=> (int)$b['id']);
|
||||
|
||||
return [
|
||||
'department_id' => $departmentId,
|
||||
'questions' => $questions,
|
||||
'conditions' => $conditions,
|
||||
'rules' => $rules,
|
||||
'tasks' => $tasks,
|
||||
'snapshot_meta' => [
|
||||
'captured_at' => date('c'),
|
||||
'source' => 'legacy_tables',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $config
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function validateConfig(array $config): array
|
||||
{
|
||||
$errors = [];
|
||||
$warnings = [];
|
||||
|
||||
$questions = is_array($config['questions'] ?? null) ? $config['questions'] : [];
|
||||
$conditions = is_array($config['conditions'] ?? null) ? $config['conditions'] : [];
|
||||
$rules = is_array($config['rules'] ?? null) ? $config['rules'] : [];
|
||||
$tasks = is_array($config['tasks'] ?? null) ? $config['tasks'] : [];
|
||||
|
||||
$questionIds = [];
|
||||
foreach ($questions as $question) {
|
||||
$id = (int)($question['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
$errors[] = 'Question without valid id.';
|
||||
continue;
|
||||
}
|
||||
$questionIds[$id] = true;
|
||||
}
|
||||
|
||||
$conditionIds = [];
|
||||
foreach ($conditions as $condition) {
|
||||
$id = (int)($condition['id'] ?? 0);
|
||||
if ($id <= 0) {
|
||||
$errors[] = 'Condition without valid id.';
|
||||
continue;
|
||||
}
|
||||
$conditionIds[$id] = true;
|
||||
$parentId = $this->nullableInt($condition['condition_id'] ?? null);
|
||||
if ($parentId !== null && !isset($conditionIds[$parentId])) {
|
||||
$warnings[] = 'Condition ' . $id . ' references parent condition ' . $parentId . ' that may be defined later or missing.';
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($rules as $rule) {
|
||||
$conditionId = (int)($rule['condition_id'] ?? 0);
|
||||
if ($conditionId <= 0 || !isset($conditionIds[$conditionId])) {
|
||||
$errors[] = 'Rule references unknown condition_id: ' . $conditionId;
|
||||
}
|
||||
$objectType = (string)($rule['object_type'] ?? '');
|
||||
$objectId = (int)($rule['object_id'] ?? 0);
|
||||
if ($objectType === 'question' && !isset($questionIds[$objectId])) {
|
||||
$errors[] = 'Rule references unknown question object_id: ' . $objectId;
|
||||
}
|
||||
if ($objectType === 'condition' && !isset($conditionIds[$objectId])) {
|
||||
$errors[] = 'Rule references unknown condition object_id: ' . $objectId;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($tasks as $task) {
|
||||
$gateTypeRaw = (string)($task['gate_type'] ?? '');
|
||||
$gateType = selfserve_task_gate_type::tryFrom($gateTypeRaw);
|
||||
if ($gateType === null) {
|
||||
$warnings[] = 'Task ' . (int)($task['id'] ?? 0) . ' has invalid gate_type `' . $gateTypeRaw . '`, falling back to legacy handling.';
|
||||
continue;
|
||||
}
|
||||
|
||||
$gateRefId = $this->nullableInt($task['gate_ref_id'] ?? null);
|
||||
if ($gateType === selfserve_task_gate_type::ALWAYS) {
|
||||
continue;
|
||||
}
|
||||
if ($gateRefId === null) {
|
||||
$errors[] = 'Task ' . (int)($task['id'] ?? 0) . ' requires gate_ref_id for gate_type ' . $gateType->value;
|
||||
continue;
|
||||
}
|
||||
if ($gateType === selfserve_task_gate_type::CONDITION && !isset($conditionIds[$gateRefId])) {
|
||||
$errors[] = 'Task ' . (int)($task['id'] ?? 0) . ' references unknown condition gate_ref_id ' . $gateRefId;
|
||||
}
|
||||
if ($gateType === selfserve_task_gate_type::QUESTION && !isset($questionIds[$gateRefId])) {
|
||||
$errors[] = 'Task ' . (int)($task['id'] ?? 0) . ' references unknown question gate_ref_id ' . $gateRefId;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'valid' => $errors === [],
|
||||
'errors' => $errors,
|
||||
'warnings' => $warnings,
|
||||
'stats' => [
|
||||
'questions' => count($questions),
|
||||
'conditions' => count($conditions),
|
||||
'rules' => count($rules),
|
||||
'tasks' => count($tasks),
|
||||
],
|
||||
'validated_at' => date('c'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function createDraftFromConfig(int $departmentId, array $config, ?int $sourceVersionId, ?int $createdBy): void
|
||||
{
|
||||
// Remove stale drafts first.
|
||||
$this->deleteAllDrafts($departmentId);
|
||||
|
||||
$validation = $this->validateConfig($config);
|
||||
$latestVersionNumber = $this->getLatestVersionNumber($departmentId);
|
||||
(new selfserve_config_versions_o())->add(
|
||||
$departmentId,
|
||||
self::STATUS_DRAFT,
|
||||
$latestVersionNumber + 1,
|
||||
$config,
|
||||
$validation,
|
||||
$sourceVersionId,
|
||||
$createdBy,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
protected function archivePublishedVersions(int $departmentId): void
|
||||
{
|
||||
global $db;
|
||||
$departmentId = (int)$departmentId;
|
||||
$sql = "UPDATE selfserve_config_versions
|
||||
SET status = '" . self::STATUS_ARCHIVED . "'
|
||||
WHERE department_id = $departmentId
|
||||
AND status = '" . self::STATUS_PUBLISHED . "'
|
||||
AND deleted_at IS NULL";
|
||||
$db->query($sql);
|
||||
}
|
||||
|
||||
protected function deleteAllDrafts(int $departmentId): void
|
||||
{
|
||||
global $db;
|
||||
$departmentId = (int)$departmentId;
|
||||
$sql = "UPDATE selfserve_config_versions
|
||||
SET deleted_at = NOW()
|
||||
WHERE department_id = $departmentId
|
||||
AND status = '" . self::STATUS_DRAFT . "'
|
||||
AND deleted_at IS NULL";
|
||||
$db->query($sql);
|
||||
}
|
||||
|
||||
protected function getLatestVersionNumber(int $departmentId): int
|
||||
{
|
||||
global $db;
|
||||
$departmentId = (int)$departmentId;
|
||||
$sql = "SELECT MAX(version_number) AS latest_version
|
||||
FROM selfserve_config_versions
|
||||
WHERE department_id = $departmentId
|
||||
AND deleted_at IS NULL";
|
||||
$result = $db->query($sql);
|
||||
$row = $db->fetch_assoc($result);
|
||||
return (int)($row['latest_version'] ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int>
|
||||
*/
|
||||
protected function getKnownDepartmentIdsForSync(): array
|
||||
{
|
||||
$departmentRows = (new departments_o())->getFieldsWhere([
|
||||
'deleted_at' => null,
|
||||
], ['id']);
|
||||
$ids = array_map(static fn(array $row): int => (int)$row['id'], $departmentRows);
|
||||
|
||||
if ($ids === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(array_unique(array_filter($ids, static fn(int $id): bool => $id > 0)));
|
||||
}
|
||||
|
||||
protected function nullableInt(mixed $value): ?int
|
||||
{
|
||||
if ($value === null || $value === '' || $value === 'null') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$intValue = (int)$value;
|
||||
return $intValue <= 0 ? null : $intValue;
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,12 @@ namespace modules\selfserve\classes;
|
||||
require_once WD . '/classes/selfserve.php';
|
||||
require_once WD . '/classes/selfserve_schema_bootstrap.php';
|
||||
require_once WD . '/modules/selfserve/classes/selfserve_condition_evaluator.php';
|
||||
require_once WD . '/modules/selfserve/classes/selfserve_config_versioning.php';
|
||||
require_once WD . '/modules/selfserve/helpers/selfserve_lane_relay.php';
|
||||
require_once WD . '/modules/selfserve/helpers/selfserve_lane_services.php';
|
||||
require_once WD . '/modules/selfserve/helpers/selfserve_lane_state.php';
|
||||
require_once WD . '/modules/selfserve/helpers/selfserve_lane_status.php';
|
||||
require_once WD . '/modules/selfserve/helpers/selfserve_task_gate_type.php';
|
||||
require_once WD . '/modules/selfserve/helpers/selfserve_wash_event_type.php';
|
||||
require_once WD . '/modules/selfserve/helpers/selfserve_wash_session_status.php';
|
||||
require_once WD . '/modules/selfserve/interfaces/selfserve_condition_evaluator_i.php';
|
||||
@@ -28,6 +30,8 @@ require_once WD . '/objects/selfserve_wash_sessions_o.php';
|
||||
|
||||
use classes\selfserve;
|
||||
use classes\selfserve_schema_bootstrap;
|
||||
use modules\selfserve\classes\selfserve_config_versioning;
|
||||
use modules\selfserve\helpers\selfserve_task_gate_type;
|
||||
use modules\selfserve\helpers\selfserve_lane_relay;
|
||||
use modules\selfserve\helpers\selfserve_lane_services;
|
||||
use modules\selfserve\helpers\selfserve_lane_state;
|
||||
@@ -58,17 +62,17 @@ 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): array
|
||||
public function previewVehicleEligibility(int $laneId, string $reg, ?int $customerNumber = null, ?int $vehicleTypeIdOverride = null): array
|
||||
{
|
||||
$snapshot = $this->buildEligibilitySnapshot($laneId, $reg, $customerNumber);
|
||||
$snapshot = $this->buildEligibilitySnapshot($laneId, $reg, $customerNumber, $vehicleTypeIdOverride);
|
||||
$session = $this->findLatestOpenSession($laneId, $snapshot['reg'], $snapshot['customer_number']);
|
||||
|
||||
return $this->formatSnapshotResponse($snapshot, $session->exists() ? $session->asArray() : null);
|
||||
}
|
||||
|
||||
public function synchronizeSession(int $laneId, string $reg, ?int $customerNumber = null, bool $activateMachine = true): array
|
||||
public function synchronizeSession(int $laneId, string $reg, ?int $customerNumber = null, bool $activateMachine = true, ?int $vehicleTypeIdOverride = null): array
|
||||
{
|
||||
$snapshot = $this->buildEligibilitySnapshot($laneId, $reg, $customerNumber);
|
||||
$snapshot = $this->buildEligibilitySnapshot($laneId, $reg, $customerNumber, $vehicleTypeIdOverride);
|
||||
$session = $this->findLatestOpenSession($laneId, $snapshot['reg'], $snapshot['customer_number']);
|
||||
|
||||
if (!$session->exists()) {
|
||||
@@ -85,6 +89,11 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
|
||||
$this->buildSessionMetadata($snapshot),
|
||||
);
|
||||
} else {
|
||||
$session->machine_type_id->set($snapshot['machine_type']['id'] ?? null);
|
||||
$session->customer_number->set($snapshot['customer_number']);
|
||||
$session->vehicle_id->set($snapshot['vehicle']['id'] ?? null);
|
||||
$session->vehicle_type_id->set($snapshot['vehicle_type_id']);
|
||||
$session->reg->set($snapshot['reg']);
|
||||
$session->allowed->set((bool)$snapshot['allowed']);
|
||||
$session->metadata_json->set($this->buildSessionMetadata($snapshot));
|
||||
$session->updateStatus($this->deriveCurrentStatus($snapshot, $session));
|
||||
@@ -172,19 +181,54 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
|
||||
{
|
||||
try {
|
||||
$lane = (new selfserve())->lane($laneId);
|
||||
if (
|
||||
empty($lane->department_lane)
|
||||
|| empty($lane->department_lane->relay_machine_id)
|
||||
|| trim((string)$lane->department_lane->relay_machine_id->value()) === ''
|
||||
) {
|
||||
if (empty($lane->department_lane)) {
|
||||
return;
|
||||
}
|
||||
$lane->setMachineRelayStatusHard(false);
|
||||
|
||||
$this->turnOffRelayIfConfiguredAndOn($lane, selfserve_lane_relay::MACHINE);
|
||||
$this->turnOffRelayIfConfiguredAndOn($lane, selfserve_lane_relay::MACHINE_CLEANER);
|
||||
} catch (\Throwable) {
|
||||
// Best effort only; session completion flow must continue.
|
||||
}
|
||||
}
|
||||
|
||||
protected function turnOffRelayIfConfiguredAndOn(selfserve_lane $lane, selfserve_lane_relay $relay): void
|
||||
{
|
||||
if (!$this->isRelayConfiguredForLane($lane, $relay)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$status = $lane->getRelayStatus($relay);
|
||||
if ((bool)($status['on'] ?? false) !== true) {
|
||||
return;
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// If relay status can't be read, still attempt turn-off as best effort.
|
||||
}
|
||||
|
||||
try {
|
||||
$lane->setRelayStatusHard($relay, false);
|
||||
} catch (\Throwable) {
|
||||
// Best effort only; session completion flow must continue.
|
||||
}
|
||||
}
|
||||
|
||||
protected function isRelayConfiguredForLane(selfserve_lane $lane, selfserve_lane_relay $relay): bool
|
||||
{
|
||||
if (empty($lane->department_lane)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$relayId = match ($relay) {
|
||||
selfserve_lane_relay::MACHINE => (string)$lane->department_lane->relay_machine_id->value(),
|
||||
selfserve_lane_relay::MACHINE_PROGRAM_PICKER => (string)$lane->department_lane->relay_machine_program_picker_id->value(),
|
||||
selfserve_lane_relay::MACHINE_CLEANER => (string)$lane->department_lane->relay_machine_cleaner_id->value(),
|
||||
};
|
||||
|
||||
return trim($relayId) !== '';
|
||||
}
|
||||
|
||||
public function getSessionSummary(int $sessionId): array
|
||||
{
|
||||
$session = (new selfserve_wash_sessions_o())->select($sessionId);
|
||||
@@ -230,6 +274,8 @@ 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,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -265,7 +311,7 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
|
||||
return $this->getSessionSummary((int)$session->id);
|
||||
}
|
||||
|
||||
protected function buildEligibilitySnapshot(int $laneId, string $reg, ?int $customerNumber = null): array
|
||||
protected function buildEligibilitySnapshot(int $laneId, string $reg, ?int $customerNumber = null, ?int $vehicleTypeIdOverride = null): array
|
||||
{
|
||||
$normalizedReg = selfserve::standardize_registration($reg);
|
||||
$lane = (new department_lanes_o())->select($laneId);
|
||||
@@ -275,14 +321,17 @@ 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();
|
||||
$publishedConfig = (new selfserve_config_versioning())->getPublishedConfig($departmentId);
|
||||
$publishedConfigVersionId = $publishedConfig['version_id'] ?? null;
|
||||
$publishedConfigPayload = is_array($publishedConfig['config'] ?? null) ? $publishedConfig['config'] : null;
|
||||
$vehicle = $this->findVehicleByRegistration($normalizedReg);
|
||||
$vehicleData = $vehicle?->asArray();
|
||||
$vehicleTypeId = $vehicle !== null ? (int)$vehicle->type->value() : null;
|
||||
$vehicleTypeId = $this->resolveVehicleTypeId($vehicle, $vehicleTypeIdOverride);
|
||||
$resolvedCustomerNumber = $customerNumber ?? ($vehicle !== null ? (int)$vehicle->customer_id->value() : null);
|
||||
|
||||
$questions = $this->loadQuestions($departmentId, $laneId, $vehicleTypeId);
|
||||
$conditions = $this->loadConditions($departmentId, $laneId, $vehicleTypeId, $machineTypeId);
|
||||
$rules = $this->loadConditionRules($conditions);
|
||||
$questions = $this->loadQuestions($departmentId, $laneId, $vehicleTypeId, $publishedConfigPayload);
|
||||
$conditions = $this->loadConditions($departmentId, $laneId, $vehicleTypeId, $machineTypeId, $publishedConfigPayload);
|
||||
$rules = $this->loadConditionRules($conditions, $publishedConfigPayload);
|
||||
$answers = (new department_selfserve_vehicle_conditions_o())->getAnswerMapForVehicle($departmentId, $laneId, $normalizedReg);
|
||||
$visibilityConditionResults = $this->conditionEvaluator->evaluate($conditions, $rules, $answers);
|
||||
|
||||
@@ -309,11 +358,44 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
|
||||
$visibleAnswers = $this->filterAnswersToVisibleQuestions($answers, $visibleQuestionIds);
|
||||
$serviceConditionResults = $this->conditionEvaluator->evaluate($conditions, $rules, $visibleAnswers);
|
||||
|
||||
$tasks = $this->loadTasks($departmentId, $laneId, $vehicleTypeId, $machineTypeId);
|
||||
$tasks = $this->loadTasks($departmentId, $laneId, $vehicleTypeId, $machineTypeId, $publishedConfigPayload);
|
||||
$activeTasks = [];
|
||||
$taskGateTrace = [];
|
||||
$conditionIds = array_map(static fn(array $condition): int => (int)($condition['id'] ?? 0), $conditions);
|
||||
foreach ($tasks as $task) {
|
||||
$gateId = $this->nullableInt($task['condition_id'] ?? null);
|
||||
if (!$this->conditionEvaluator->taskGateSatisfied($gateId, $serviceConditionResults, $visibleAnswers)) {
|
||||
$typedGateType = selfserve_task_gate_type::tryFrom((string)($task['gate_type'] ?? ''));
|
||||
$typedGateRefId = $this->nullableInt($task['gate_ref_id'] ?? null);
|
||||
|
||||
if ($typedGateType === null) {
|
||||
if ($gateId === null) {
|
||||
$typedGateType = selfserve_task_gate_type::ALWAYS;
|
||||
$typedGateRefId = null;
|
||||
} elseif (in_array($gateId, $conditionIds, true)) {
|
||||
$typedGateType = selfserve_task_gate_type::CONDITION;
|
||||
$typedGateRefId = $gateId;
|
||||
} else {
|
||||
$typedGateType = selfserve_task_gate_type::QUESTION;
|
||||
$typedGateRefId = $gateId;
|
||||
}
|
||||
}
|
||||
|
||||
$gateSatisfied = $this->conditionEvaluator->taskGateSatisfiedTyped(
|
||||
$typedGateType->value,
|
||||
$typedGateRefId,
|
||||
$serviceConditionResults,
|
||||
$visibleAnswers
|
||||
);
|
||||
|
||||
$taskGateTrace[] = [
|
||||
'task_id' => (int)$task['id'],
|
||||
'legacy_gate_id' => $gateId,
|
||||
'gate_type' => $typedGateType->value,
|
||||
'gate_ref_id' => $typedGateRefId,
|
||||
'satisfied' => $gateSatisfied,
|
||||
];
|
||||
|
||||
if (!$gateSatisfied) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -322,6 +404,8 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
|
||||
'task' => (string)$task['task'],
|
||||
'description' => (string)($task['description'] ?? ''),
|
||||
'condition_id' => $gateId,
|
||||
'gate_type' => $typedGateType->value,
|
||||
'gate_ref_id' => $typedGateRefId,
|
||||
'order_priority' => (int)($task['order_priority'] ?? 0),
|
||||
'services' => $this->normalizeServiceNames($this->normalizeJsonArray($task['services'] ?? null)),
|
||||
'buttons' => $this->normalizeIntArray($this->normalizeJsonArray($task['buttons'] ?? null)),
|
||||
@@ -374,6 +458,12 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
|
||||
'machine_available' => $machineAvailable,
|
||||
'all_visible_questions_answered' => $allVisibleQuestionsAnswered,
|
||||
'allowed' => $machineAllowed,
|
||||
'config_version_id' => $publishedConfigVersionId === null ? null : (int)$publishedConfigVersionId,
|
||||
'evaluation_trace' => [
|
||||
'condition_results' => $serviceConditionResults,
|
||||
'task_gates' => $taskGateTrace,
|
||||
'visible_question_ids' => $visibleQuestionIds,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -387,6 +477,7 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
|
||||
$lane = (new selfserve())->lane($laneId);
|
||||
$lane->setLaneCache($laneId, $lane::CACHE_SELFSERVE_LANE_KEY_ALLOWED_SERVICES, $snapshot['allowed_services']);
|
||||
$lane->turnOnRelay(selfserve_lane_relay::MACHINE);
|
||||
$this->enableCleanerRelayForStartedWash($lane);
|
||||
|
||||
$session->markRelayEnabled();
|
||||
$this->logSessionEvent((int)$session->id, selfserve_wash_event_type::MACHINE_RELAY_ENABLED, [
|
||||
@@ -430,6 +521,7 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
|
||||
'vehicle' => $snapshot['vehicle'],
|
||||
'reg' => $snapshot['reg'],
|
||||
'customer_number' => $snapshot['customer_number'],
|
||||
'vehicle_type_id' => $snapshot['vehicle_type_id'],
|
||||
'questions' => $snapshot['questions'],
|
||||
'tasks' => $snapshot['tasks'],
|
||||
'allowed_services' => $snapshot['allowed_services'],
|
||||
@@ -437,11 +529,41 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
|
||||
'all_visible_questions_answered' => $snapshot['all_visible_questions_answered'],
|
||||
'allowed' => $snapshot['allowed'],
|
||||
'session' => $session,
|
||||
'config_version_id' => $snapshot['config_version_id'] ?? null,
|
||||
'evaluation_trace' => $snapshot['evaluation_trace'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
protected function loadQuestions(int $departmentId, int $laneId, ?int $vehicleTypeId): array
|
||||
/**
|
||||
* @param array<string,mixed>|null $publishedConfig
|
||||
*/
|
||||
protected function loadQuestions(int $departmentId, int $laneId, ?int $vehicleTypeId, ?array $publishedConfig = null): array
|
||||
{
|
||||
if (is_array($publishedConfig) && isset($publishedConfig['questions']) && is_array($publishedConfig['questions'])) {
|
||||
$questions = array_values(array_filter($publishedConfig['questions'], static function (array $question) use ($departmentId): bool {
|
||||
return ((int)($question['department'] ?? 0) === 0) || ((int)($question['department'] ?? 0) === $departmentId);
|
||||
}));
|
||||
|
||||
$sharedQuestions = array_values(array_filter($questions, static function (array $question): bool {
|
||||
return (int)($question['department'] ?? 0) === 0
|
||||
&& (int)($question['lane'] ?? 0) === 0
|
||||
&& (int)($question['product'] ?? 0) === 0;
|
||||
}));
|
||||
if ($sharedQuestions !== []) {
|
||||
return $sharedQuestions;
|
||||
}
|
||||
|
||||
if ($vehicleTypeId === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(array_filter($questions, static function (array $question) use ($departmentId, $laneId, $vehicleTypeId): bool {
|
||||
return (int)($question['department'] ?? 0) === $departmentId
|
||||
&& (int)($question['lane'] ?? 0) === $laneId
|
||||
&& (int)($question['product'] ?? 0) === $vehicleTypeId;
|
||||
}));
|
||||
}
|
||||
|
||||
$questionsObject = new department_selfserve_questions_o();
|
||||
$sharedQuestions = $questionsObject->getSharedQuestions();
|
||||
if ($sharedQuestions !== []) {
|
||||
@@ -454,8 +576,37 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
|
||||
return $questionsObject->getLegacyQuestionsForLaneProduct($departmentId, $laneId, $vehicleTypeId);
|
||||
}
|
||||
|
||||
protected function loadConditions(int $departmentId, int $laneId, ?int $vehicleTypeId, ?int $machineTypeId): array
|
||||
/**
|
||||
* @param array<string,mixed>|null $publishedConfig
|
||||
*/
|
||||
protected function loadConditions(int $departmentId, int $laneId, ?int $vehicleTypeId, ?int $machineTypeId, ?array $publishedConfig = null): array
|
||||
{
|
||||
if (is_array($publishedConfig) && isset($publishedConfig['conditions']) && is_array($publishedConfig['conditions'])) {
|
||||
$conditions = array_values(array_filter($publishedConfig['conditions'], static function (array $condition) use ($departmentId): bool {
|
||||
return ((int)($condition['department'] ?? 0) === 0) || ((int)($condition['department'] ?? 0) === $departmentId);
|
||||
}));
|
||||
|
||||
if ($machineTypeId !== null) {
|
||||
$machineTypeConditions = array_values(array_filter($conditions, static function (array $condition) use ($machineTypeId): bool {
|
||||
return (int)($condition['machine_type_id'] ?? 0) === $machineTypeId;
|
||||
}));
|
||||
if ($machineTypeConditions !== []) {
|
||||
return $machineTypeConditions;
|
||||
}
|
||||
}
|
||||
|
||||
if ($vehicleTypeId === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(array_filter($conditions, static function (array $condition) use ($departmentId, $laneId, $vehicleTypeId): bool {
|
||||
return (int)($condition['machine_type_id'] ?? 0) === 0
|
||||
&& (int)($condition['department'] ?? 0) === $departmentId
|
||||
&& (int)($condition['lane'] ?? 0) === $laneId
|
||||
&& (int)($condition['product'] ?? 0) === $vehicleTypeId;
|
||||
}));
|
||||
}
|
||||
|
||||
$conditionsObject = new department_selfserve_conditions_o();
|
||||
if ($machineTypeId !== null) {
|
||||
$machineTypeConditions = $conditionsObject->getConditionsForMachineType($machineTypeId);
|
||||
@@ -470,8 +621,36 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
|
||||
return $conditionsObject->getLegacyConditionsForLaneProduct($departmentId, $laneId, $vehicleTypeId);
|
||||
}
|
||||
|
||||
protected function loadTasks(int $departmentId, int $laneId, ?int $vehicleTypeId, ?int $machineTypeId): array
|
||||
/**
|
||||
* @param array<string,mixed>|null $publishedConfig
|
||||
*/
|
||||
protected function loadTasks(int $departmentId, int $laneId, ?int $vehicleTypeId, ?int $machineTypeId, ?array $publishedConfig = null): array
|
||||
{
|
||||
if (is_array($publishedConfig) && isset($publishedConfig['tasks']) && is_array($publishedConfig['tasks'])) {
|
||||
$tasks = array_values(array_filter($publishedConfig['tasks'], static function (array $task) use ($departmentId): bool {
|
||||
return ((int)($task['department'] ?? 0) === 0) || ((int)($task['department'] ?? 0) === $departmentId);
|
||||
}));
|
||||
|
||||
if ($machineTypeId !== null) {
|
||||
$machineTypeTasks = array_values(array_filter($tasks, static function (array $task) use ($machineTypeId): bool {
|
||||
return (int)($task['machine_type_id'] ?? 0) === $machineTypeId;
|
||||
}));
|
||||
if ($machineTypeTasks !== []) {
|
||||
return $machineTypeTasks;
|
||||
}
|
||||
}
|
||||
if ($vehicleTypeId === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(array_filter($tasks, static function (array $task) use ($departmentId, $laneId, $vehicleTypeId): bool {
|
||||
return (int)($task['machine_type_id'] ?? 0) === 0
|
||||
&& (int)($task['department'] ?? 0) === $departmentId
|
||||
&& (int)($task['lane'] ?? 0) === $laneId
|
||||
&& (int)($task['product'] ?? 0) === $vehicleTypeId;
|
||||
}));
|
||||
}
|
||||
|
||||
$tasksObject = new department_selfserve_tasks_o();
|
||||
if ($machineTypeId !== null) {
|
||||
$machineTypeTasks = $tasksObject->getTasksForMachineType($machineTypeId);
|
||||
@@ -486,7 +665,10 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
|
||||
return $tasksObject->getLegacyTasksForLaneProduct($departmentId, $laneId, $vehicleTypeId);
|
||||
}
|
||||
|
||||
protected function loadConditionRules(array $conditions): array
|
||||
/**
|
||||
* @param array<string,mixed>|null $publishedConfig
|
||||
*/
|
||||
protected function loadConditionRules(array $conditions, ?array $publishedConfig = null): array
|
||||
{
|
||||
if ($conditions === []) {
|
||||
return [];
|
||||
@@ -494,6 +676,12 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
|
||||
|
||||
$conditionIds = array_map(static fn(array $condition): int => (int)$condition['id'], $conditions);
|
||||
|
||||
if (is_array($publishedConfig) && isset($publishedConfig['rules']) && is_array($publishedConfig['rules'])) {
|
||||
return array_values(array_filter($publishedConfig['rules'], static function (array $rule) use ($conditionIds): bool {
|
||||
return in_array((int)($rule['condition_id'] ?? 0), $conditionIds, true);
|
||||
}));
|
||||
}
|
||||
|
||||
return (new department_selfserve_condition_rules_o())->getFieldsWhereIn([
|
||||
'condition_id' => $conditionIds,
|
||||
'deleted_at' => null,
|
||||
@@ -547,6 +735,8 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
|
||||
'allowed_services' => $snapshot['allowed_services'],
|
||||
'machine_available' => (bool)$snapshot['machine_available'],
|
||||
'all_visible_questions_answered' => (bool)$snapshot['all_visible_questions_answered'],
|
||||
'config_version_id' => $snapshot['config_version_id'] ?? null,
|
||||
'evaluation_trace' => $snapshot['evaluation_trace'] ?? null,
|
||||
'visible_question_ids' => array_map(static fn(array $question): int => (int)$question['id'], $snapshot['questions']),
|
||||
'visible_questions' => array_map(static fn(array $question): array => [
|
||||
'id' => (int)$question['id'],
|
||||
@@ -681,6 +871,19 @@ class selfserve_wash_flow implements selfserve_wash_flow_i
|
||||
return $vehicle->exists() ? $vehicle : null;
|
||||
}
|
||||
|
||||
protected function resolveVehicleTypeId(?customer_vehicles_o $vehicle, ?int $vehicleTypeIdOverride = null): ?int
|
||||
{
|
||||
if ($vehicleTypeIdOverride !== null && $vehicleTypeIdOverride > 0) {
|
||||
return $vehicleTypeIdOverride;
|
||||
}
|
||||
if ($vehicle === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$vehicleTypeId = (int)$vehicle->type->value();
|
||||
return $vehicleTypeId > 0 ? $vehicleTypeId : null;
|
||||
}
|
||||
|
||||
protected function findLatestOpenSession(int $laneId, string $reg, ?int $customerNumber = null): selfserve_wash_sessions_o
|
||||
{
|
||||
$session = new selfserve_wash_sessions_o();
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace modules\selfserve\helpers;
|
||||
|
||||
enum selfserve_task_gate_type: string
|
||||
{
|
||||
case ALWAYS = 'ALWAYS';
|
||||
case CONDITION = 'CONDITION';
|
||||
case QUESTION = 'QUESTION';
|
||||
}
|
||||
@@ -19,4 +19,13 @@ interface selfserve_condition_evaluator_i
|
||||
* @return bool
|
||||
*/
|
||||
public function taskGateSatisfied(?int $gateId, array $conditionResults, array $answers): bool;
|
||||
|
||||
/**
|
||||
* @param string $gateType ALWAYS|CONDITION|QUESTION
|
||||
* @param int|null $gateRefId
|
||||
* @param array<int,bool> $conditionResults
|
||||
* @param array<int,bool|null> $answers
|
||||
* @return bool
|
||||
*/
|
||||
public function taskGateSatisfiedTyped(string $gateType, ?int $gateRefId, array $conditionResults, array $answers): bool;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ namespace modules\selfserve\interfaces;
|
||||
|
||||
interface selfserve_wash_flow_i
|
||||
{
|
||||
public function previewVehicleEligibility(int $laneId, string $reg, ?int $customerNumber = null): array;
|
||||
public function previewVehicleEligibility(int $laneId, string $reg, ?int $customerNumber = null, ?int $vehicleTypeIdOverride = null): array;
|
||||
|
||||
public function synchronizeSession(int $laneId, string $reg, ?int $customerNumber = null, bool $activateMachine = true): array;
|
||||
public function synchronizeSession(int $laneId, string $reg, ?int $customerNumber = null, bool $activateMachine = true, ?int $vehicleTypeIdOverride = null): array;
|
||||
|
||||
public function recordMachineStartWebhook(int $laneId, ?string $reg = null, array $payload = []): array;
|
||||
|
||||
|
||||
@@ -9,9 +9,7 @@ use traits\db_object_t;
|
||||
|
||||
class department_selfserve_questions_o extends db
|
||||
{
|
||||
use db_object_t {
|
||||
delete as trait_delete;
|
||||
}
|
||||
use db_object_t;
|
||||
|
||||
public object_property $department; // The department id
|
||||
public object_property $lane; // The lane id
|
||||
@@ -107,7 +105,13 @@ class department_selfserve_questions_o extends db
|
||||
// `department_selfserve_tasks.condition_id` link (which semantically stores question id).
|
||||
$sql = "UPDATE department_selfserve_tasks SET condition_id = NULL WHERE condition_id = $id";
|
||||
$db->query($sql);
|
||||
$this->trait_delete();
|
||||
|
||||
if (self::columnsExist(['deleted_at'])) {
|
||||
self::update(['deleted_at' => date('Y-m-d H:i:s')]);
|
||||
return;
|
||||
}
|
||||
|
||||
self::deletePermanently();
|
||||
}
|
||||
|
||||
public function asArray(): array
|
||||
|
||||
@@ -6,8 +6,8 @@ use classes\db;
|
||||
use classes\object_property;
|
||||
use classes\selfserve_schema_bootstrap;
|
||||
use Exception;
|
||||
use modules\selfserve\helpers\selfserve_lane_command;
|
||||
use modules\selfserve\helpers\selfserve_lane_services;
|
||||
use modules\selfserve\helpers\selfserve_task_gate_type;
|
||||
use traits\db_object_t;
|
||||
|
||||
class department_selfserve_tasks_o extends db
|
||||
@@ -27,6 +27,8 @@ class department_selfserve_tasks_o extends db
|
||||
public object_property $product; // The product id
|
||||
public object_property $machine_type_id; // The reusable machine type id (nullable, preferred over department/lane/product)
|
||||
public object_property $condition_id; // Legacy column name: stores question id that gates this task (nullable)
|
||||
public object_property $gate_type; // Canonical gate type: ALWAYS|CONDITION|QUESTION
|
||||
public object_property $gate_ref_id; // Canonical gate reference id (nullable)
|
||||
public object_property $task; // The task
|
||||
public object_property $description; // The task description
|
||||
public object_property $order_priority; // The order priority of the task (lower numbers are shown first)
|
||||
@@ -75,7 +77,21 @@ class department_selfserve_tasks_o extends db
|
||||
* @return department_selfserve_tasks_o
|
||||
* @throws Exception If the object was not created successfully
|
||||
*/
|
||||
public function add(int $department, int $lane, int $product, int|null $question_id, string $task, string $description, int $order_priority = 0, ?array $services = null, array|string|null $buttons = null, int|null $dynamic_images_vehicle_type = null, ?int $machine_type_id = null): self
|
||||
public function add(
|
||||
int $department,
|
||||
int $lane,
|
||||
int $product,
|
||||
int|null $question_id,
|
||||
string $task,
|
||||
string $description,
|
||||
int $order_priority = 0,
|
||||
?array $services = null,
|
||||
array|string|null $buttons = null,
|
||||
int|null $dynamic_images_vehicle_type = null,
|
||||
?int $machine_type_id = null,
|
||||
?selfserve_task_gate_type $gate_type = null,
|
||||
?int $gate_ref_id = null,
|
||||
): self
|
||||
{
|
||||
global /** @var db $db */
|
||||
$db;
|
||||
@@ -89,6 +105,9 @@ class department_selfserve_tasks_o extends db
|
||||
if (!is_null($machine_type_id)) {
|
||||
$machine_type_id = (int)$machine_type_id;
|
||||
}
|
||||
if (!is_null($gate_ref_id)) {
|
||||
$gate_ref_id = (int)$gate_ref_id;
|
||||
}
|
||||
$task = $db->escape_string($task);
|
||||
$description = $db->escape_string($description);
|
||||
$order_priority = (int)$order_priority;
|
||||
@@ -133,6 +152,16 @@ class department_selfserve_tasks_o extends db
|
||||
}
|
||||
}
|
||||
|
||||
if ($gate_type === null) {
|
||||
$resolvedGate = self::resolveLegacyGateDefinition($question_id);
|
||||
$gate_type = $resolvedGate['gate_type'];
|
||||
$gate_ref_id = $resolvedGate['gate_ref_id'];
|
||||
} elseif ($gate_type === selfserve_task_gate_type::ALWAYS) {
|
||||
$gate_ref_id = null;
|
||||
} elseif ($gate_ref_id === null || $gate_ref_id <= 0) {
|
||||
throw new Exception('gate_ref_id must be provided for CONDITION and QUESTION task gate types');
|
||||
}
|
||||
|
||||
// Add the object
|
||||
$tmp_id = self::add_object([
|
||||
'department' => $department,
|
||||
@@ -140,6 +169,8 @@ class department_selfserve_tasks_o extends db
|
||||
'product' => $product,
|
||||
...(!is_null($machine_type_id) ? ['machine_type_id' => $machine_type_id] : []),
|
||||
...(!is_null($question_id) ? ['condition_id' => $question_id] : []), // Legacy column name; contains the gating question id
|
||||
'gate_type' => $gate_type->value,
|
||||
'gate_ref_id' => $gate_ref_id,
|
||||
'task' => $task,
|
||||
'description' => $description,
|
||||
'order_priority' => $order_priority,
|
||||
@@ -161,6 +192,8 @@ class department_selfserve_tasks_o extends db
|
||||
$this->product = new object_property($this->table, $this->id, 'product', 'int', false);
|
||||
$this->machine_type_id = new object_property($this->table, $this->id, 'machine_type_id', 'int', false);
|
||||
$this->condition_id = new object_property($this->table, $this->id, 'condition_id', 'int', true);
|
||||
$this->gate_type = new object_property($this->table, $this->id, 'gate_type', 'string', false);
|
||||
$this->gate_ref_id = new object_property($this->table, $this->id, 'gate_ref_id', 'int', true);
|
||||
$this->task = new object_property($this->table, $this->id, 'task', 'string', false);
|
||||
$this->description = new object_property($this->table, $this->id, 'description', 'string', false);
|
||||
$this->order_priority = new object_property($this->table, $this->id, 'order_priority', 'int', false);
|
||||
@@ -186,6 +219,8 @@ class department_selfserve_tasks_o extends db
|
||||
'product' => (int)$this->product->value(),
|
||||
'machine_type_id' => is_null($this->machine_type_id->value()) ? null : (int)$this->machine_type_id->value(),
|
||||
'condition_id' => is_null($this->condition_id->value()) ? null : (int)$this->condition_id->value(),
|
||||
'gate_type' => (string)($this->gate_type->value() ?? selfserve_task_gate_type::ALWAYS->value),
|
||||
'gate_ref_id' => is_null($this->gate_ref_id->value()) ? null : (int)$this->gate_ref_id->value(),
|
||||
'task' => (string)$this->task->value(),
|
||||
'description' => (string)$this->description->value(),
|
||||
'order_priority' => (int)$this->order_priority->value(),
|
||||
@@ -216,6 +251,9 @@ class department_selfserve_tasks_o extends db
|
||||
public function setQuestionId(?int $question_id): void
|
||||
{
|
||||
$this->condition_id->set(is_null($question_id) ? null : (int)$question_id);
|
||||
$resolvedGate = self::resolveLegacyGateDefinition($question_id);
|
||||
$this->gate_type->set($resolvedGate['gate_type']->value);
|
||||
$this->gate_ref_id->set($resolvedGate['gate_ref_id']);
|
||||
}
|
||||
|
||||
public function getTasksForMachineType(int $machineTypeId): array
|
||||
@@ -223,7 +261,7 @@ class department_selfserve_tasks_o extends db
|
||||
return self::getFieldsWhere([
|
||||
'machine_type_id' => $machineTypeId,
|
||||
'deleted_at' => null,
|
||||
], ['id', 'department', 'lane', 'product', 'machine_type_id', 'condition_id', 'task', 'description', 'order_priority', 'services', 'buttons', 'dynamic_images_vehicle_type', 'created_at', 'updated_at']);
|
||||
], ['id', 'department', 'lane', 'product', 'machine_type_id', 'condition_id', 'gate_type', 'gate_ref_id', 'task', 'description', 'order_priority', 'services', 'buttons', 'dynamic_images_vehicle_type', 'created_at', 'updated_at']);
|
||||
}
|
||||
|
||||
public function getLegacyTasksForLaneProduct(int $departmentId, int $laneId, int $productId): array
|
||||
@@ -233,7 +271,56 @@ class department_selfserve_tasks_o extends db
|
||||
'lane' => $laneId,
|
||||
'product' => $productId,
|
||||
'deleted_at' => null,
|
||||
], ['id', 'department', 'lane', 'product', 'machine_type_id', 'condition_id', 'task', 'description', 'order_priority', 'services', 'buttons', 'dynamic_images_vehicle_type', 'created_at', 'updated_at']);
|
||||
], ['id', 'department', 'lane', 'product', 'machine_type_id', 'condition_id', 'gate_type', 'gate_ref_id', 'task', 'description', 'order_priority', 'services', 'buttons', 'dynamic_images_vehicle_type', 'created_at', 'updated_at']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize external gate_type inputs.
|
||||
* @param mixed $input
|
||||
* @return selfserve_task_gate_type
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function normalizeGateTypeInput(mixed $input): selfserve_task_gate_type
|
||||
{
|
||||
if ($input instanceof selfserve_task_gate_type) {
|
||||
return $input;
|
||||
}
|
||||
if (is_string($input)) {
|
||||
$normalized = strtoupper(trim($input));
|
||||
$gateType = selfserve_task_gate_type::tryFrom($normalized);
|
||||
if ($gateType !== null) {
|
||||
return $gateType;
|
||||
}
|
||||
}
|
||||
throw new Exception('Invalid gate_type. Expected ALWAYS, CONDITION, or QUESTION.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a typed gate from legacy `condition_id` input.
|
||||
* @param int|null $legacyGateId
|
||||
* @return array{gate_type:selfserve_task_gate_type,gate_ref_id:int|null}
|
||||
*/
|
||||
public static function resolveLegacyGateDefinition(?int $legacyGateId): array
|
||||
{
|
||||
if ($legacyGateId === null || $legacyGateId <= 0) {
|
||||
return [
|
||||
'gate_type' => selfserve_task_gate_type::ALWAYS,
|
||||
'gate_ref_id' => null,
|
||||
];
|
||||
}
|
||||
|
||||
$condition = (new department_selfserve_conditions_o())->select((int)$legacyGateId);
|
||||
if ($condition->exists()) {
|
||||
return [
|
||||
'gate_type' => selfserve_task_gate_type::CONDITION,
|
||||
'gate_ref_id' => (int)$legacyGateId,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'gate_type' => selfserve_task_gate_type::QUESTION,
|
||||
'gate_ref_id' => (int)$legacyGateId,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace objects;
|
||||
|
||||
use classes\db;
|
||||
use classes\object_property;
|
||||
use classes\selfserve_schema_bootstrap;
|
||||
use traits\db_object_t;
|
||||
|
||||
class selfserve_config_versions_o extends db
|
||||
{
|
||||
use db_object_t;
|
||||
|
||||
public object_property $department_id;
|
||||
public object_property $status;
|
||||
public object_property $version_number;
|
||||
public object_property $config_json;
|
||||
public object_property $validation_result_json;
|
||||
public object_property $source_version_id;
|
||||
public object_property $created_by;
|
||||
public object_property $published_at;
|
||||
public object_property $created_at;
|
||||
public object_property $updated_at;
|
||||
public object_property $deleted_at;
|
||||
|
||||
public function structure(): void
|
||||
{
|
||||
selfserve_schema_bootstrap::ensureTables();
|
||||
$this->setTable('selfserve_config_versions');
|
||||
}
|
||||
|
||||
public function getObjectProperties(): void
|
||||
{
|
||||
$this->department_id = new object_property($this->table, $this->id, 'department_id', 'int', false);
|
||||
$this->status = new object_property($this->table, $this->id, 'status', 'string', false);
|
||||
$this->version_number = new object_property($this->table, $this->id, 'version_number', 'int', false);
|
||||
$this->config_json = new object_property($this->table, $this->id, 'config_json', 'json', false);
|
||||
$this->validation_result_json = new object_property($this->table, $this->id, 'validation_result_json', 'json', false);
|
||||
$this->source_version_id = new object_property($this->table, $this->id, 'source_version_id', 'int', false);
|
||||
$this->created_by = new object_property($this->table, $this->id, 'created_by', 'int', false);
|
||||
$this->published_at = new object_property($this->table, $this->id, 'published_at', 'datetime', false);
|
||||
$this->created_at = new object_property($this->table, $this->id, 'created_at', 'timestamp', false);
|
||||
$this->updated_at = new object_property($this->table, $this->id, 'updated_at', 'timestamp', false);
|
||||
$this->deleted_at = new object_property($this->table, $this->id, 'deleted_at', 'timestamp', false);
|
||||
}
|
||||
|
||||
public function objectChanged(): void
|
||||
{
|
||||
// No-op for now.
|
||||
}
|
||||
|
||||
public function add(
|
||||
int $departmentId,
|
||||
string $status,
|
||||
int $versionNumber,
|
||||
array $config,
|
||||
?array $validationResult = null,
|
||||
?int $sourceVersionId = null,
|
||||
?int $createdBy = null,
|
||||
?string $publishedAt = null,
|
||||
): self {
|
||||
$configJson = json_encode($config, JSON_UNESCAPED_UNICODE);
|
||||
if ($configJson === false) {
|
||||
throw new \RuntimeException('Failed to encode self-serve config JSON: ' . json_last_error_msg());
|
||||
}
|
||||
|
||||
$validationJson = null;
|
||||
if ($validationResult !== null) {
|
||||
$validationJson = json_encode($validationResult, JSON_UNESCAPED_UNICODE);
|
||||
if ($validationJson === false) {
|
||||
throw new \RuntimeException('Failed to encode self-serve validation JSON: ' . json_last_error_msg());
|
||||
}
|
||||
}
|
||||
|
||||
$this->id = $this->add_object([
|
||||
'department_id' => $departmentId,
|
||||
'status' => $status,
|
||||
'version_number' => $versionNumber,
|
||||
// Pass JSON as escaped strings to avoid SQL quoting issues inside generic add_object().
|
||||
'config_json' => $configJson,
|
||||
'validation_result_json' => $validationJson,
|
||||
'source_version_id' => $sourceVersionId,
|
||||
'created_by' => $createdBy,
|
||||
'published_at' => $publishedAt,
|
||||
]);
|
||||
$this->getObjectProperties();
|
||||
$this->objectChanged();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function selectLatestByDepartmentAndStatus(int $departmentId, string $status): self
|
||||
{
|
||||
$rows = $this->getFieldsWhere([
|
||||
'department_id' => $departmentId,
|
||||
'status' => $status,
|
||||
'deleted_at' => null,
|
||||
], ['id']);
|
||||
if ($rows === []) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
usort($rows, static fn(array $a, array $b): int => (int)$b['id'] <=> (int)$a['id']);
|
||||
$this->select((int)$rows[0]['id']);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function listByDepartment(int $departmentId): array
|
||||
{
|
||||
$rows = $this->getFieldsWhere([
|
||||
'department_id' => $departmentId,
|
||||
'deleted_at' => null,
|
||||
], ['id']);
|
||||
if ($rows === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
usort($rows, static fn(array $a, array $b): int => (int)$b['id'] <=> (int)$a['id']);
|
||||
return array_map(function (array $row): array {
|
||||
return (new selfserve_config_versions_o())->select((int)$row['id'])->asArray();
|
||||
}, $rows);
|
||||
}
|
||||
|
||||
public function asArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => (int)$this->id,
|
||||
'department_id' => (int)$this->department_id->value(),
|
||||
'status' => (string)$this->status->value(),
|
||||
'version_number' => (int)$this->version_number->value(),
|
||||
'config' => (array)($this->config_json->value() ?? []),
|
||||
'validation_result' => (array)($this->validation_result_json->value() ?? []),
|
||||
'source_version_id' => $this->source_version_id->value() === null ? null : (int)$this->source_version_id->value(),
|
||||
'created_by' => $this->created_by->value() === null ? null : (int)$this->created_by->value(),
|
||||
'published_at' => $this->published_at->value() === null ? null : (string)$this->published_at->value(),
|
||||
'created_at' => (string)$this->created_at->value(),
|
||||
'updated_at' => $this->updated_at->value() === null ? null : (string)$this->updated_at->value(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
namespace routes;
|
||||
|
||||
use classes\authentication;
|
||||
use modules\selfserve\classes\selfserve_config_versioning;
|
||||
use classes\response;
|
||||
use objects\department_selfserve_conditions_o;
|
||||
use objects\department_selfserve_condition_rules_o;
|
||||
@@ -131,6 +132,7 @@ class departmentSelfserveConditionRulesRoute
|
||||
$name,
|
||||
$description
|
||||
);
|
||||
(new selfserve_config_versioning())->syncDraftFromLegacyForDepartment((int)$condition_o->department->value());
|
||||
(new logs_o())->add('department_selfserve_condition_rules', 'global', 1, $user->id, 'ADD_RULE', 'User added department self-serve condition rule ' . $rule_o->id);
|
||||
$response->success($rule_o->asArray());
|
||||
} catch (\Exception $e) {
|
||||
@@ -169,6 +171,9 @@ class departmentSelfserveConditionRulesRoute
|
||||
if ($condition_o->exists() && !$this->canAccessDepartment($authorized_department_ids, (int)$condition_o->department->value())) {
|
||||
$this->forbidDepartmentAccess((int)$condition_o->department->value());
|
||||
}
|
||||
$originalDepartment = $condition_o->exists()
|
||||
? (int)$condition_o->department->value()
|
||||
: 0;
|
||||
|
||||
if ($response->isRequestParameterSet('condition_id')) {
|
||||
$new_condition_id = (int)$response->getRequestParameter('condition_id');
|
||||
@@ -198,6 +203,11 @@ class departmentSelfserveConditionRulesRoute
|
||||
$rule_o->description->update((string)$response->getRequestParameter('description'));
|
||||
}
|
||||
|
||||
(new selfserve_config_versioning())->syncDraftFromLegacyForDepartment($originalDepartment);
|
||||
$updatedCondition = (new department_selfserve_conditions_o())->select((int)$rule_o->condition_id->value());
|
||||
if ($updatedCondition->exists()) {
|
||||
(new selfserve_config_versioning())->syncDraftFromLegacyForDepartment((int)$updatedCondition->department->value());
|
||||
}
|
||||
(new logs_o())->add('department_selfserve_condition_rules', 'global', 1, $user->id, 'UPDATE_RULE', 'User updated department self-serve condition rule ' . $id);
|
||||
$response->success($rule_o->asArray());
|
||||
} else {
|
||||
@@ -235,6 +245,9 @@ class departmentSelfserveConditionRulesRoute
|
||||
}
|
||||
|
||||
$rule_o->delete();
|
||||
if ($condition_o->exists()) {
|
||||
(new selfserve_config_versioning())->syncDraftFromLegacyForDepartment((int)$condition_o->department->value());
|
||||
}
|
||||
(new logs_o())->add('department_selfserve_condition_rules', 'global', 1, $user->id, 'DELETE_RULE', 'User deleted department self-serve condition rule ' . $id);
|
||||
$response->success('Rule deleted');
|
||||
} else {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
namespace routes;
|
||||
|
||||
use classes\authentication;
|
||||
use modules\selfserve\classes\selfserve_config_versioning;
|
||||
use classes\response;
|
||||
use objects\department_selfserve_conditions_o;
|
||||
use objects\logs_o;
|
||||
@@ -138,6 +139,7 @@ class departmentSelfserveConditionsRoute
|
||||
$condition_id,
|
||||
$machine_type_id
|
||||
);
|
||||
(new selfserve_config_versioning())->syncDraftFromLegacyForDepartment($department);
|
||||
(new logs_o())->add('department_selfserve_conditions', 'global', 1, $user->id, 'ADD_CONDITION', 'User added department self-serve condition ' . $condition_o->id);
|
||||
$response->success($condition_o->asArray());
|
||||
} catch (\Exception $e) {
|
||||
@@ -168,6 +170,7 @@ class departmentSelfserveConditionsRoute
|
||||
if (!$condition_o->exists()) {
|
||||
$response->error('Condition not found', 404);
|
||||
}
|
||||
$originalDepartment = (int)$condition_o->department->value();
|
||||
|
||||
$authorized_department_ids = $user->getGroup()->getDepartments();
|
||||
if (!$this->canAccessDepartment($authorized_department_ids, (int)$condition_o->department->value())) {
|
||||
@@ -202,6 +205,8 @@ class departmentSelfserveConditionsRoute
|
||||
$condition_o->description->update((string)$response->getRequestParameter('description'));
|
||||
}
|
||||
|
||||
(new selfserve_config_versioning())->syncDraftFromLegacyForDepartment($originalDepartment);
|
||||
(new selfserve_config_versioning())->syncDraftFromLegacyForDepartment((int)$condition_o->department->value());
|
||||
(new logs_o())->add('department_selfserve_conditions', 'global', 1, $user->id, 'UPDATE_CONDITION', 'User updated department self-serve condition ' . $id);
|
||||
$response->success($condition_o->asArray());
|
||||
} else {
|
||||
@@ -236,6 +241,7 @@ class departmentSelfserveConditionsRoute
|
||||
}
|
||||
|
||||
$condition_o->delete();
|
||||
(new selfserve_config_versioning())->syncDraftFromLegacyForDepartment((int)$condition_o->department->value());
|
||||
(new logs_o())->add('department_selfserve_conditions', 'global', 1, $user->id, 'DELETE_CONDITION', 'User deleted department self-serve condition ' . $id);
|
||||
$response->success('Condition deleted');
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
/**
|
||||
* Route for self-serve config draft/publish lifecycle
|
||||
*/
|
||||
|
||||
namespace routes;
|
||||
|
||||
use classes\authentication;
|
||||
use modules\selfserve\classes\selfserve_config_versioning;
|
||||
use objects\logs_o;
|
||||
use traits\route_t;
|
||||
|
||||
class departmentSelfserveConfigVersionsRoute
|
||||
{
|
||||
use route_t;
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$this->get('/department/selfserve/config/versions', function () {
|
||||
global $response;
|
||||
$this->requirePermission('list_department_selfserve_config_versions');
|
||||
$user = (new authentication())->get_user();
|
||||
if (!$user) {
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
|
||||
self::requireParameters(['department']);
|
||||
$departmentId = (int)self::getParameter('department');
|
||||
$authorizedDepartmentIds = $user->getGroup()->getDepartments();
|
||||
if (!in_array($departmentId, $authorizedDepartmentIds, true) && !$this->hasPermission('view_all_department_selfserve_config_versions')) {
|
||||
$this->forbidDepartmentAccess($departmentId, ['view_all_department_selfserve_config_versions']);
|
||||
}
|
||||
|
||||
$service = new selfserve_config_versioning();
|
||||
$versions = $service->listVersions($departmentId);
|
||||
(new logs_o())->add('selfserve_config_versions', $departmentId, 1, $user->id, 'LIST_CONFIG_VERSIONS', 'User listed self-serve config versions');
|
||||
$response->success($versions);
|
||||
}, [
|
||||
'list_department_selfserve_config_versions' => 'List self-serve config versions for a department',
|
||||
'view_all_department_selfserve_config_versions' => 'List self-serve config versions across all departments',
|
||||
]);
|
||||
|
||||
$this->get('/department/selfserve/config/history', function () {
|
||||
global $response;
|
||||
$this->requirePermission('list_department_selfserve_config_versions');
|
||||
$user = (new authentication())->get_user();
|
||||
if (!$user) {
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
|
||||
self::requireParameters(['department']);
|
||||
$departmentId = (int)self::getParameter('department');
|
||||
$authorizedDepartmentIds = $user->getGroup()->getDepartments();
|
||||
if (!in_array($departmentId, $authorizedDepartmentIds, true) && !$this->hasPermission('view_all_department_selfserve_config_versions')) {
|
||||
$this->forbidDepartmentAccess($departmentId, ['view_all_department_selfserve_config_versions']);
|
||||
}
|
||||
|
||||
$service = new selfserve_config_versioning();
|
||||
$response->success($service->listVersions($departmentId));
|
||||
}, [
|
||||
'list_department_selfserve_config_versions' => 'List self-serve config history for a department',
|
||||
'view_all_department_selfserve_config_versions' => 'List self-serve config history across all departments',
|
||||
]);
|
||||
|
||||
$this->get('/department/selfserve/config/active', function () {
|
||||
global $response;
|
||||
$this->requirePermission('list_department_selfserve_config_versions');
|
||||
$user = (new authentication())->get_user();
|
||||
if (!$user) {
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
|
||||
self::requireParameters(['department']);
|
||||
$departmentId = (int)self::getParameter('department');
|
||||
$authorizedDepartmentIds = $user->getGroup()->getDepartments();
|
||||
if (!in_array($departmentId, $authorizedDepartmentIds, true) && !$this->hasPermission('view_all_department_selfserve_config_versions')) {
|
||||
$this->forbidDepartmentAccess($departmentId, ['view_all_department_selfserve_config_versions']);
|
||||
}
|
||||
|
||||
$service = new selfserve_config_versioning();
|
||||
$published = $service->getPublishedConfig($departmentId);
|
||||
if ($published === null) {
|
||||
$response->success([
|
||||
'version_id' => null,
|
||||
'config' => null,
|
||||
'source' => 'legacy',
|
||||
]);
|
||||
}
|
||||
|
||||
$response->success([
|
||||
'version_id' => (int)$published['version_id'],
|
||||
'config' => (array)$published['config'],
|
||||
'source' => 'published',
|
||||
]);
|
||||
}, [
|
||||
'list_department_selfserve_config_versions' => 'View active self-serve config version for a department',
|
||||
'view_all_department_selfserve_config_versions' => 'View active self-serve config version across all departments',
|
||||
]);
|
||||
|
||||
$this->post('/department/selfserve/config/draft', function () {
|
||||
global $response;
|
||||
$this->requirePermission('edit_department_selfserve_config_versions');
|
||||
$user = (new authentication())->get_user();
|
||||
if (!$user) {
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
|
||||
self::requireParameters(['department']);
|
||||
$departmentId = (int)self::getParameter('department');
|
||||
$this->assertDepartmentAccess($user, $departmentId);
|
||||
|
||||
$forceRefresh = self::isParametersSet(['force'])
|
||||
? filter_var(self::getParameter('force'), FILTER_VALIDATE_BOOLEAN)
|
||||
: false;
|
||||
|
||||
$service = new selfserve_config_versioning();
|
||||
$draft = $service->ensureDraftFromLegacy($departmentId, (int)$user->id, $forceRefresh === true);
|
||||
(new logs_o())->add('selfserve_config_versions', $departmentId, 1, $user->id, 'UPSERT_CONFIG_DRAFT', 'User upserted self-serve config draft');
|
||||
$response->success($draft);
|
||||
}, [
|
||||
'edit_department_selfserve_config_versions' => 'Create or update a self-serve config draft for a department',
|
||||
]);
|
||||
|
||||
$this->post('/department/selfserve/config/validate', function () {
|
||||
global $response;
|
||||
$this->requirePermission('edit_department_selfserve_config_versions');
|
||||
$user = (new authentication())->get_user();
|
||||
if (!$user) {
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
|
||||
self::requireParameters(['department']);
|
||||
$departmentId = (int)self::getParameter('department');
|
||||
$this->assertDepartmentAccess($user, $departmentId);
|
||||
|
||||
$service = new selfserve_config_versioning();
|
||||
$validation = $service->validateDraft($departmentId);
|
||||
(new logs_o())->add('selfserve_config_versions', $departmentId, 1, $user->id, 'VALIDATE_CONFIG_DRAFT', 'User validated self-serve config draft');
|
||||
$response->success($validation);
|
||||
}, [
|
||||
'edit_department_selfserve_config_versions' => 'Validate a self-serve config draft for a department',
|
||||
]);
|
||||
|
||||
$this->post('/department/selfserve/config/publish', function () {
|
||||
global $response;
|
||||
$this->requirePermission('publish_department_selfserve_config_versions');
|
||||
$user = (new authentication())->get_user();
|
||||
if (!$user) {
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
|
||||
self::requireParameters(['department']);
|
||||
$departmentId = (int)self::getParameter('department');
|
||||
$this->assertDepartmentAccess($user, $departmentId);
|
||||
|
||||
$service = new selfserve_config_versioning();
|
||||
try {
|
||||
$published = $service->publishDraft($departmentId, (int)$user->id);
|
||||
(new logs_o())->add('selfserve_config_versions', $departmentId, 1, $user->id, 'PUBLISH_CONFIG_DRAFT', 'User published self-serve config draft');
|
||||
$response->success($published);
|
||||
} catch (\RuntimeException $exception) {
|
||||
$response->error($exception->getMessage(), 422);
|
||||
}
|
||||
}, [
|
||||
'publish_department_selfserve_config_versions' => 'Publish self-serve config draft for a department',
|
||||
]);
|
||||
|
||||
$this->post('/department/selfserve/config/rollback', function () {
|
||||
global $response;
|
||||
$this->requirePermission('rollback_department_selfserve_config_versions');
|
||||
$user = (new authentication())->get_user();
|
||||
if (!$user) {
|
||||
$response->error('Invalid session', 400);
|
||||
}
|
||||
|
||||
self::requireParameters(['department', 'target_version_id']);
|
||||
$departmentId = (int)self::getParameter('department');
|
||||
$targetVersionId = (int)self::getParameter('target_version_id');
|
||||
if ($targetVersionId <= 0) {
|
||||
$response->error('Invalid target_version_id', 400);
|
||||
}
|
||||
|
||||
$this->assertDepartmentAccess($user, $departmentId);
|
||||
|
||||
$service = new selfserve_config_versioning();
|
||||
try {
|
||||
$rolledBack = $service->rollbackToVersion($departmentId, $targetVersionId, (int)$user->id);
|
||||
(new logs_o())->add('selfserve_config_versions', $departmentId, 1, $user->id, 'ROLLBACK_CONFIG_VERSION', 'User rolled back self-serve config to version ' . $targetVersionId);
|
||||
$response->success($rolledBack);
|
||||
} catch (\RuntimeException $exception) {
|
||||
$response->error($exception->getMessage(), 422);
|
||||
}
|
||||
}, [
|
||||
'rollback_department_selfserve_config_versions' => 'Rollback self-serve config to a previous version for a department',
|
||||
]);
|
||||
}
|
||||
|
||||
private function assertDepartmentAccess(object $user, int $departmentId): void
|
||||
{
|
||||
$authorizedDepartmentIds = $user->getGroup()->getDepartments();
|
||||
if (!in_array($departmentId, $authorizedDepartmentIds, true)) {
|
||||
$this->forbidDepartmentAccess($departmentId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
namespace routes;
|
||||
|
||||
use classes\authentication;
|
||||
use modules\selfserve\classes\selfserve_config_versioning;
|
||||
use classes\response;
|
||||
use objects\department_selfserve_questions_o;
|
||||
use objects\logs_o;
|
||||
@@ -129,6 +130,7 @@ class departmentSelfserveQuestionsRoute
|
||||
$condition_id,
|
||||
$order_priority
|
||||
);
|
||||
(new selfserve_config_versioning())->syncDraftFromLegacyForDepartment($department);
|
||||
(new logs_o())->add('department_selfserve_questions', 'global', 1, $user->id, 'ADD_QUESTION', 'User added a department self-serve question: ' . $question);
|
||||
$response->success($question_o->asArray());
|
||||
} catch (\Exception $e) {
|
||||
@@ -157,6 +159,7 @@ class departmentSelfserveQuestionsRoute
|
||||
if (!$question_o->exists()) {
|
||||
$response->error('Question not found', 404);
|
||||
}
|
||||
$originalDepartment = (int)$question_o->department->value();
|
||||
|
||||
$authorized_department_ids = $user->getGroup()->getDepartments();
|
||||
if (!$this->canAccessDepartment($authorized_department_ids, (int)$question_o->department->value())) {
|
||||
@@ -190,6 +193,8 @@ class departmentSelfserveQuestionsRoute
|
||||
$question_o->order_priority->set((int)self::getParameter('order_priority'));
|
||||
}
|
||||
|
||||
(new selfserve_config_versioning())->syncDraftFromLegacyForDepartment($originalDepartment);
|
||||
(new selfserve_config_versioning())->syncDraftFromLegacyForDepartment((int)$question_o->department->value());
|
||||
(new logs_o())->add('department_selfserve_questions', 'global', 1, $user->id, 'EDIT_QUESTION', 'User updated department self-serve question ID: ' . $id);
|
||||
$response->success($question_o->asArray());
|
||||
} else {
|
||||
@@ -222,6 +227,7 @@ class departmentSelfserveQuestionsRoute
|
||||
}
|
||||
|
||||
$question_o->delete();
|
||||
(new selfserve_config_versioning())->syncDraftFromLegacyForDepartment((int)$question_o->department->value());
|
||||
(new logs_o())->add('department_selfserve_questions', 'global', 1, $user->id, 'DELETE_QUESTION', 'User deleted department self-serve question ID: ' . $id);
|
||||
$response->success('Question deleted');
|
||||
} else {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
namespace routes;
|
||||
|
||||
use classes\authentication;
|
||||
use modules\selfserve\classes\selfserve_config_versioning;
|
||||
use classes\response;
|
||||
use objects\department_selfserve_tasks_o;
|
||||
use objects\logs_o;
|
||||
@@ -13,6 +14,7 @@ use attachments\helpers\attachment_content;
|
||||
use classes\attachment_store;
|
||||
use classes\attachments;
|
||||
use modules\selfserve\helpers\selfserve_lane_services;
|
||||
use modules\selfserve\helpers\selfserve_task_gate_type;
|
||||
use traits\route_t;
|
||||
|
||||
class departmentSelfserveTasksRoute
|
||||
@@ -79,13 +81,19 @@ class departmentSelfserveTasksRoute
|
||||
if (self::isParametersSet(['condition_id'])) {
|
||||
$filters['condition_id'] = (int)self::getParameter('condition_id');
|
||||
}
|
||||
if (self::isParametersSet(['gate_type'])) {
|
||||
$filters['gate_type'] = (string)self::getParameter('gate_type');
|
||||
}
|
||||
if (self::isParametersSet(['gate_ref_id'])) {
|
||||
$filters['gate_ref_id'] = (int)self::getParameter('gate_ref_id');
|
||||
}
|
||||
|
||||
if (self::isParametersSet(['machine_type_id'])) {
|
||||
$filters['machine_type_id'] = (int)self::getParameter('machine_type_id');
|
||||
}
|
||||
|
||||
$response->success(
|
||||
$tasks_o->setSearchableFields(['id', 'department', 'lane', 'product', 'machine_type_id', 'condition_id', 'task', 'description', 'deleted_at'])
|
||||
$tasks_o->setSearchableFields(['id', 'department', 'lane', 'product', 'machine_type_id', 'condition_id', 'gate_type', 'gate_ref_id', 'task', 'description', 'deleted_at'])
|
||||
->listObjectsWithPaginationIfSet(function ($task) {
|
||||
$t = new department_selfserve_tasks_o();
|
||||
$t->select((int)$task['id']);
|
||||
@@ -125,6 +133,38 @@ class departmentSelfserveTasksRoute
|
||||
$question_id = (int)$condition_id_param;
|
||||
}
|
||||
}
|
||||
$gate_type = null;
|
||||
if ($response->isRequestParameterSet('gate_type')) {
|
||||
try {
|
||||
$gate_type = department_selfserve_tasks_o::normalizeGateTypeInput($response->getRequestParameter('gate_type'));
|
||||
} catch (\Exception $e) {
|
||||
$response->error($e->getMessage(), 400);
|
||||
}
|
||||
}
|
||||
$gate_ref_id = null;
|
||||
if ($response->isRequestParameterSet('gate_ref_id')) {
|
||||
$gate_ref_param = $response->getRequestParameter('gate_ref_id');
|
||||
if (!is_null($gate_ref_param) && $gate_ref_param !== '' && $gate_ref_param !== 'null') {
|
||||
$gate_ref_id = (int)$gate_ref_param;
|
||||
}
|
||||
}
|
||||
if ($gate_type !== null) {
|
||||
if ($gate_type === selfserve_task_gate_type::ALWAYS) {
|
||||
$gate_ref_id = null;
|
||||
$question_id = null;
|
||||
} else {
|
||||
if ($gate_ref_id === null) {
|
||||
$gate_ref_id = $question_id;
|
||||
}
|
||||
if ($gate_ref_id === null || $gate_ref_id <= 0) {
|
||||
$response->error('gate_ref_id is required when gate_type is CONDITION or QUESTION', 400);
|
||||
}
|
||||
// Keep legacy contract field in sync.
|
||||
$question_id = $gate_ref_id;
|
||||
}
|
||||
} elseif ($gate_ref_id !== null && $gate_ref_id > 0) {
|
||||
$question_id = $gate_ref_id;
|
||||
}
|
||||
$task = (string)$response->getRequestParameter('task');
|
||||
$description = (string)$response->getRequestParameter('description');
|
||||
$order_priority = (int)($response->getRequestParameter('order_priority') ?? 0);
|
||||
@@ -211,7 +251,10 @@ class departmentSelfserveTasksRoute
|
||||
$buttons_ids,
|
||||
$vehicle_type,
|
||||
$machine_type_id,
|
||||
$gate_type,
|
||||
$gate_ref_id,
|
||||
);
|
||||
(new selfserve_config_versioning())->syncDraftFromLegacyForDepartment($department);
|
||||
(new logs_o())->add('department_selfserve_tasks', 'global', 1, $user->id, 'ADD_TASK', 'User added a department self-serve task: ' . $task);
|
||||
$response->success($task_o->asArray());
|
||||
} catch (\Exception $e) {
|
||||
@@ -240,6 +283,7 @@ class departmentSelfserveTasksRoute
|
||||
if (!$task_o->exists()) {
|
||||
$response->error('Task not found', 404);
|
||||
}
|
||||
$originalDepartment = (int)$task_o->department->value();
|
||||
|
||||
$authorized_department_ids = $user->getGroup()->getDepartments();
|
||||
if (!$this->canAccessDepartment($authorized_department_ids, (int)$task_o->department->value())) {
|
||||
@@ -267,6 +311,40 @@ class departmentSelfserveTasksRoute
|
||||
$param = self::getParameter('condition_id');
|
||||
$task_o->setQuestionId($param === null || $param === '' || $param === 'null' ? null : (int)$param);
|
||||
}
|
||||
if (self::isParametersSet(['gate_type']) || self::isParametersSet(['gate_ref_id'])) {
|
||||
try {
|
||||
$effectiveGateType = self::isParametersSet(['gate_type'])
|
||||
? department_selfserve_tasks_o::normalizeGateTypeInput(self::getParameter('gate_type'))
|
||||
: department_selfserve_tasks_o::normalizeGateTypeInput((string)($task_o->gate_type->value() ?? selfserve_task_gate_type::ALWAYS->value));
|
||||
$effectiveGateRefId = self::isParametersSet(['gate_ref_id'])
|
||||
? (
|
||||
(self::getParameter('gate_ref_id') === null || self::getParameter('gate_ref_id') === '' || self::getParameter('gate_ref_id') === 'null')
|
||||
? null
|
||||
: (int)self::getParameter('gate_ref_id')
|
||||
)
|
||||
: ($task_o->gate_ref_id->value() === null ? null : (int)$task_o->gate_ref_id->value());
|
||||
|
||||
if (!self::isParametersSet(['gate_ref_id']) && self::isParametersSet(['condition_id']) && $effectiveGateType !== selfserve_task_gate_type::ALWAYS) {
|
||||
$conditionParam = self::getParameter('condition_id');
|
||||
$effectiveGateRefId = ($conditionParam === null || $conditionParam === '' || $conditionParam === 'null')
|
||||
? null
|
||||
: (int)$conditionParam;
|
||||
}
|
||||
|
||||
if ($effectiveGateType === selfserve_task_gate_type::ALWAYS) {
|
||||
$effectiveGateRefId = null;
|
||||
} elseif ($effectiveGateRefId === null || $effectiveGateRefId <= 0) {
|
||||
$response->error('gate_ref_id is required when gate_type is CONDITION or QUESTION', 400);
|
||||
}
|
||||
|
||||
$task_o->gate_type->set($effectiveGateType->value);
|
||||
$task_o->gate_ref_id->set($effectiveGateRefId);
|
||||
// Preserve legacy contract field.
|
||||
$task_o->condition_id->set($effectiveGateRefId);
|
||||
} catch (\Exception $e) {
|
||||
$response->error($e->getMessage(), 400);
|
||||
}
|
||||
}
|
||||
if (self::isParametersSet(['task'])) {
|
||||
$task_o->task->set((string)self::getParameter('task'));
|
||||
}
|
||||
@@ -344,6 +422,8 @@ class departmentSelfserveTasksRoute
|
||||
}
|
||||
}
|
||||
|
||||
(new selfserve_config_versioning())->syncDraftFromLegacyForDepartment($originalDepartment);
|
||||
(new selfserve_config_versioning())->syncDraftFromLegacyForDepartment((int)$task_o->department->value());
|
||||
(new logs_o())->add('department_selfserve_tasks', 'global', 1, $user->id, 'EDIT_TASK', 'User updated department self-serve task ID: ' . $id);
|
||||
$response->success($task_o->asArray());
|
||||
} else {
|
||||
@@ -376,6 +456,7 @@ class departmentSelfserveTasksRoute
|
||||
}
|
||||
|
||||
$task_o->delete();
|
||||
(new selfserve_config_versioning())->syncDraftFromLegacyForDepartment((int)$task_o->department->value());
|
||||
(new logs_o())->add('department_selfserve_tasks', 'global', 1, $user->id, 'DELETE_TASK', 'User deleted department self-serve task ID: ' . $id);
|
||||
$response->success('Task deleted');
|
||||
} else {
|
||||
|
||||
@@ -11,6 +11,7 @@ use classes\selfserve;
|
||||
use modules\selfserve\classes\selfserve_wash_flow;
|
||||
use objects\customer_vehicles_o;
|
||||
use objects\department_lanes_o;
|
||||
use objects\department_selfserve_tasks_o;
|
||||
use objects\department_selfserve_vehicle_conditions_o;
|
||||
use objects\logs_o;
|
||||
use traits\route_t;
|
||||
@@ -136,9 +137,15 @@ class departmentSelfserveVehicleConditionsRoute
|
||||
$vehicle = $this->assertOwnVehicle($user, $reg, 'list_department_selfserve_vehicle_conditions');
|
||||
$customer_number = (int)$vehicle->customer_id->value();
|
||||
}
|
||||
$vehicle_type_id = $this->resolveVehicleTypeIdFromQuery();
|
||||
$flow = $this->getWashFlow();
|
||||
|
||||
if ($vehicle_type_id !== null) {
|
||||
$flow->synchronizeSession($lane_id, $reg, $customer_number, false, $vehicle_type_id);
|
||||
}
|
||||
|
||||
(new logs_o())->add('department_selfserve_vehicle_conditions', (int)$lane->department->value(), 1, $user->id, 'CHECK_VEHICLE_ALLOWED', 'User checked self-serve eligibility for lane ' . $lane_id . ' and vehicle ' . $reg);
|
||||
$response->success($this->getWashFlow()->previewVehicleEligibility($lane_id, $reg, $customer_number));
|
||||
$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'
|
||||
@@ -161,10 +168,24 @@ class departmentSelfserveVehicleConditionsRoute
|
||||
}
|
||||
|
||||
$flow = $this->getWashFlow();
|
||||
$vehicle_type_id = $this->resolveVehicleTypeIdFromQuery();
|
||||
try {
|
||||
if (self::isParametersSet(['session_id'])) {
|
||||
$summary = $flow->getSessionSummary((int)self::getParameter('session_id'));
|
||||
$this->assertSummaryAccess($user, $summary, $has_global, 'list_department_selfserve_vehicle_conditions');
|
||||
|
||||
if ($this->shouldRefreshSummaryForVehicleType($summary, $vehicle_type_id)) {
|
||||
$summary = $flow->synchronizeSession(
|
||||
(int)($summary['session']['lane_id'] ?? 0),
|
||||
(string)($summary['session']['reg'] ?? ''),
|
||||
isset($summary['session']['customer_number']) && $summary['session']['customer_number'] !== null
|
||||
? (int)$summary['session']['customer_number']
|
||||
: null,
|
||||
false,
|
||||
$vehicle_type_id
|
||||
);
|
||||
}
|
||||
|
||||
$response->success($summary);
|
||||
}
|
||||
|
||||
@@ -173,11 +194,17 @@ class departmentSelfserveVehicleConditionsRoute
|
||||
$reg = selfserve::standardize_registration((string)self::getParameter('reg'));
|
||||
|
||||
$this->assertLaneAccess($user, $lane_id, $has_global);
|
||||
$customer_number = null;
|
||||
if (!$has_global && $has_own) {
|
||||
$this->assertOwnVehicle($user, $reg, 'list_department_selfserve_vehicle_conditions');
|
||||
$vehicle = $this->assertOwnVehicle($user, $reg, 'list_department_selfserve_vehicle_conditions');
|
||||
$customer_number = (int)$vehicle->customer_id->value();
|
||||
}
|
||||
|
||||
$summary = $flow->getLatestSessionSummary($lane_id, $reg);
|
||||
if ($vehicle_type_id !== null) {
|
||||
$summary = $flow->synchronizeSession($lane_id, $reg, $customer_number, false, $vehicle_type_id);
|
||||
} else {
|
||||
$summary = $flow->getLatestSessionSummary($lane_id, $reg);
|
||||
}
|
||||
$this->assertSummaryAccess($user, $summary, $has_global, 'list_department_selfserve_vehicle_conditions');
|
||||
$response->success($summary);
|
||||
} catch (\RuntimeException $e) {
|
||||
@@ -214,6 +241,7 @@ class departmentSelfserveVehicleConditionsRoute
|
||||
if (!$department || !$lane || !$reg || !$question) {
|
||||
$response->error('Missing required fields', 400);
|
||||
}
|
||||
$vehicle_type_id = $this->resolveVehicleTypeIdFromRequest();
|
||||
|
||||
if ($has_global) {
|
||||
$customer_id = $response->isRequestParameterSet('customer_id') ? (int)$response->getRequestParameter('customer_id') : null;
|
||||
@@ -230,7 +258,7 @@ class departmentSelfserveVehicleConditionsRoute
|
||||
try {
|
||||
$condition_o = new department_selfserve_vehicle_conditions_o();
|
||||
$condition_o->add($department, $lane, $reg, $question, $value, $customer_id);
|
||||
$summary = $this->getWashFlow()->synchronizeSession($lane, $reg, $customer_id);
|
||||
$summary = $this->getWashFlow()->synchronizeSession($lane, $reg, $customer_id, true, $vehicle_type_id);
|
||||
(new logs_o())->add('department_selfserve_vehicle_conditions', 'global', 1, $user->id, 'ADD_VEHICLE_CONDITION', 'User added department self-serve vehicle condition ' . $condition_o->id);
|
||||
$response->success([
|
||||
'condition' => $condition_o->asArray(),
|
||||
@@ -321,12 +349,15 @@ class departmentSelfserveVehicleConditionsRoute
|
||||
}
|
||||
$condition_o->customer_id->update($new_customer_id);
|
||||
}
|
||||
$vehicle_type_id = $this->resolveVehicleTypeIdFromRequest();
|
||||
|
||||
try {
|
||||
$summary = $this->getWashFlow()->synchronizeSession(
|
||||
(int)$condition_o->lane->value(),
|
||||
(string)$condition_o->reg->value(),
|
||||
$condition_o->customer_id->value() === null ? null : (int)$condition_o->customer_id->value()
|
||||
$condition_o->customer_id->value() === null ? null : (int)$condition_o->customer_id->value(),
|
||||
true,
|
||||
$vehicle_type_id
|
||||
);
|
||||
(new logs_o())->add('department_selfserve_vehicle_conditions', 'global', 1, $user->id, 'UPDATE_VEHICLE_CONDITION', 'User updated department self-serve vehicle condition ' . $id);
|
||||
$response->success([
|
||||
@@ -383,11 +414,12 @@ class departmentSelfserveVehicleConditionsRoute
|
||||
$lane_id = (int)$condition_o->lane->value();
|
||||
$reg = (string)$condition_o->reg->value();
|
||||
$customer_id = $condition_o->customer_id->value() === null ? null : (int)$condition_o->customer_id->value();
|
||||
$vehicle_type_id = $this->resolveVehicleTypeIdFromRequest();
|
||||
|
||||
$condition_o->delete();
|
||||
|
||||
try {
|
||||
$summary = $this->getWashFlow()->synchronizeSession($lane_id, $reg, $customer_id);
|
||||
$summary = $this->getWashFlow()->synchronizeSession($lane_id, $reg, $customer_id, true, $vehicle_type_id);
|
||||
} catch (\Throwable) {
|
||||
$summary = null;
|
||||
}
|
||||
@@ -408,6 +440,70 @@ class departmentSelfserveVehicleConditionsRoute
|
||||
return new selfserve_wash_flow();
|
||||
}
|
||||
|
||||
private function resolveVehicleTypeIdFromQuery(): ?int
|
||||
{
|
||||
$rawVehicleType = null;
|
||||
if (self::isParametersSet(['vehicle_type_id'])) {
|
||||
$rawVehicleType = self::getParameter('vehicle_type_id');
|
||||
} elseif (self::isParametersSet(['vehicle_type'])) {
|
||||
$rawVehicleType = self::getParameter('vehicle_type');
|
||||
}
|
||||
|
||||
return $this->normalizeVehicleTypeOverride($rawVehicleType);
|
||||
}
|
||||
|
||||
private function resolveVehicleTypeIdFromRequest(): ?int
|
||||
{
|
||||
global $response;
|
||||
|
||||
$rawVehicleType = null;
|
||||
if ($response->isRequestParameterSet('vehicle_type_id')) {
|
||||
$rawVehicleType = $response->getRequestParameter('vehicle_type_id');
|
||||
} elseif ($response->isRequestParameterSet('vehicle_type')) {
|
||||
$rawVehicleType = $response->getRequestParameter('vehicle_type');
|
||||
} elseif (self::isParametersSet(['vehicle_type_id'])) {
|
||||
$rawVehicleType = self::getParameter('vehicle_type_id');
|
||||
} elseif (self::isParametersSet(['vehicle_type'])) {
|
||||
$rawVehicleType = self::getParameter('vehicle_type');
|
||||
}
|
||||
|
||||
return $this->normalizeVehicleTypeOverride($rawVehicleType);
|
||||
}
|
||||
|
||||
private function normalizeVehicleTypeOverride(mixed $rawVehicleType): ?int
|
||||
{
|
||||
global $response;
|
||||
|
||||
if ($rawVehicleType === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return department_selfserve_tasks_o::normalizeVehicleTypeInput($rawVehicleType);
|
||||
} catch (\Exception $e) {
|
||||
$response->error('Invalid vehicle_type_id parameter: ' . $e->getMessage(), 400);
|
||||
}
|
||||
}
|
||||
|
||||
private function shouldRefreshSummaryForVehicleType(array $summary, ?int $vehicleTypeIdOverride): bool
|
||||
{
|
||||
if ($vehicleTypeIdOverride === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$session = is_array($summary['session'] ?? null) ? $summary['session'] : [];
|
||||
$sessionVehicleTypeId = isset($session['vehicle_type_id']) && $session['vehicle_type_id'] !== null
|
||||
? (int)$session['vehicle_type_id']
|
||||
: null;
|
||||
if ($sessionVehicleTypeId !== $vehicleTypeIdOverride) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$questions = is_array($summary['questions'] ?? null) ? $summary['questions'] : [];
|
||||
$tasks = is_array($summary['tasks'] ?? null) ? $summary['tasks'] : [];
|
||||
return $questions === [] && $tasks === [];
|
||||
}
|
||||
|
||||
private function assertLaneAccess(object $user, int $laneId, bool $hasGlobalPermission): department_lanes_o
|
||||
{
|
||||
global $response;
|
||||
|
||||
@@ -117,6 +117,15 @@ class moduleSelfServeRoute
|
||||
];
|
||||
};
|
||||
|
||||
$format_wash_started_at = static function (?int $wash_start_time): ?string {
|
||||
if ($wash_start_time === null || $wash_start_time <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return date('Y-m-d H:i:s', $wash_start_time);
|
||||
};
|
||||
$included_minutes_when_machine_enabled = 20;
|
||||
|
||||
$rows = (new selfserve_wash_sessions_o())->getFieldsWhere([
|
||||
'lane_id' => $lane_id,
|
||||
'completed_at' => null,
|
||||
@@ -136,6 +145,15 @@ class moduleSelfServeRoute
|
||||
$lane = $selfserve->lane($lane_id);
|
||||
$lane_status = $lane->getLaneStatus();
|
||||
$lane_state = $lane->getLaneState();
|
||||
$wash_started_at = $format_wash_started_at($lane->getWashStartTime());
|
||||
$machine_start_triggered = $wash_started_at !== null;
|
||||
$machine_relay_enabled = $machine_start_triggered
|
||||
|| $lane_state->equals(\modules\selfserve\helpers\selfserve_lane_state::MACHINE_RELAY_ON_QUEUED)
|
||||
|| $lane_state->equals(\modules\selfserve\helpers\selfserve_lane_state::MACHINE_RELAY_ON)
|
||||
|| $lane_state->equals(\modules\selfserve\helpers\selfserve_lane_state::IN_WASH)
|
||||
|| $lane_state->equals(\modules\selfserve\helpers\selfserve_lane_state::EXIT_PORT_OPEN_QUEUED)
|
||||
|| $lane_state->equals(\modules\selfserve\helpers\selfserve_lane_state::EXIT_PORT_OPEN);
|
||||
$included_minutes = $machine_relay_enabled ? $included_minutes_when_machine_enabled : null;
|
||||
$in_progress = $lane_status->equals(\modules\selfserve\helpers\selfserve_lane_status::OCCUPIED)
|
||||
|| $lane_state->equals(\modules\selfserve\helpers\selfserve_lane_state::IN_WASH);
|
||||
|
||||
@@ -171,9 +189,13 @@ class moduleSelfServeRoute
|
||||
'customer_number' => $runtime_customer_number,
|
||||
'vehicle_id' => $vehicle['id'] ?? null,
|
||||
'vehicle_type_id' => $vehicle['type'] ?? null,
|
||||
'included_minutes' => $included_minutes,
|
||||
'machine_type_id' => null,
|
||||
'machine_start_triggered' => $lane_state->equals(\modules\selfserve\helpers\selfserve_lane_state::IN_WASH),
|
||||
'machine_start_triggered_at' => null,
|
||||
'machine_relay_enabled' => $machine_relay_enabled,
|
||||
'machine_relay_enabled_at' => $machine_relay_enabled ? $wash_started_at : null,
|
||||
'machine_start_triggered' => $machine_start_triggered,
|
||||
'machine_start_triggered_at' => $wash_started_at,
|
||||
'wash_started_at' => $wash_started_at,
|
||||
'created_at' => null,
|
||||
'updated_at' => null,
|
||||
],
|
||||
@@ -191,10 +213,14 @@ class moduleSelfServeRoute
|
||||
}
|
||||
$customer = $build_customer($customer_number);
|
||||
$vehicle = $build_vehicle($vehicle_id, $session_reg);
|
||||
$machine_start_triggered_at = $session->machine_start_triggered_at->value() === null ? null : (string)$session->machine_start_triggered_at->value();
|
||||
$machine_relay_enabled = (bool)$session->machine_relay_enabled->value();
|
||||
$included_minutes = $machine_relay_enabled ? $included_minutes_when_machine_enabled : null;
|
||||
|
||||
$response->success([
|
||||
'lane_id' => $lane_id,
|
||||
'in_progress' => true,
|
||||
'elapsed_minutes' => 0, // TODO: Start when the session was created.
|
||||
'session' => [
|
||||
'id' => (int)$session->id,
|
||||
'status' => (string)$session->status->value(),
|
||||
@@ -202,9 +228,13 @@ class moduleSelfServeRoute
|
||||
'customer_number' => $customer_number,
|
||||
'vehicle_id' => $vehicle_id,
|
||||
'vehicle_type_id' => $session->vehicle_type_id->value() === null ? null : (int)$session->vehicle_type_id->value(),
|
||||
'included_minutes' => $included_minutes,
|
||||
'machine_type_id' => $session->machine_type_id->value() === null ? null : (int)$session->machine_type_id->value(),
|
||||
'machine_relay_enabled' => $machine_relay_enabled,
|
||||
'machine_relay_enabled_at' => $session->machine_relay_enabled_at->value() === null ? null : (string)$session->machine_relay_enabled_at->value(),
|
||||
'machine_start_triggered' => (bool)$session->machine_start_triggered->value(),
|
||||
'machine_start_triggered_at' => $session->machine_start_triggered_at->value() === null ? null : (string)$session->machine_start_triggered_at->value(),
|
||||
'machine_start_triggered_at' => $machine_start_triggered_at,
|
||||
'wash_started_at' => $machine_start_triggered_at,
|
||||
'created_at' => (string)$session->created_at->value(),
|
||||
'updated_at' => $session->updated_at->value() === null ? null : (string)$session->updated_at->value(),
|
||||
],
|
||||
@@ -643,6 +673,16 @@ class moduleSelfServeRoute
|
||||
$lane = $selfserve->lane($lane_id);
|
||||
try {
|
||||
$lane->turnOnRelay(selfserve_lane_relay::MACHINE, $duration);
|
||||
// A started wash should also turn on cleaner when configured.
|
||||
try {
|
||||
if (
|
||||
!empty($lane->department_lane)
|
||||
&& !empty($lane->department_lane->relay_machine_cleaner_id)
|
||||
&& trim((string)$lane->department_lane->relay_machine_cleaner_id->value()) !== ''
|
||||
) {
|
||||
$lane->setMachineCleanerRelayStatusHard(true);
|
||||
}
|
||||
} catch (\Throwable $ignored) {}
|
||||
$response->success(['lane_id' => $lane_id, 'relay' => 'MACHINE', 'enabled' => true, 'duration' => $duration]);
|
||||
} catch (\Exception $e) {
|
||||
$msg = $e->getMessage();
|
||||
|
||||
@@ -5,6 +5,7 @@ app_require('modules/selfserve/classes/selfserve_condition_evaluator.php');
|
||||
use modules\selfserve\classes\selfserve_condition_evaluator;
|
||||
use modules\selfserve\helpers\selfserve_condition_rule_object_type;
|
||||
use modules\selfserve\helpers\selfserve_condition_rule_type;
|
||||
use modules\selfserve\helpers\selfserve_task_gate_type;
|
||||
|
||||
it('evaluates self-serve conditions with combined AND and OR semantics', function (): void {
|
||||
$evaluator = new selfserve_condition_evaluator();
|
||||
@@ -67,3 +68,13 @@ it('prefers condition results over question answers when evaluating task gates',
|
||||
expect($evaluator->taskGateSatisfied(15, [], [15 => true]))->toBeTrue();
|
||||
expect($evaluator->taskGateSatisfied(null, [], []))->toBeTrue();
|
||||
});
|
||||
|
||||
it('evaluates typed task gates with strict semantics', function (): void {
|
||||
$evaluator = new selfserve_condition_evaluator();
|
||||
|
||||
expect($evaluator->taskGateSatisfiedTyped(selfserve_task_gate_type::ALWAYS->value, null, [], []))->toBeTrue();
|
||||
expect($evaluator->taskGateSatisfiedTyped(selfserve_task_gate_type::CONDITION->value, 31, [31 => true], [31 => false]))->toBeTrue();
|
||||
expect($evaluator->taskGateSatisfiedTyped(selfserve_task_gate_type::CONDITION->value, 31, [31 => false], [31 => true]))->toBeFalse();
|
||||
expect($evaluator->taskGateSatisfiedTyped(selfserve_task_gate_type::QUESTION->value, 8, [], [8 => true]))->toBeTrue();
|
||||
expect($evaluator->taskGateSatisfiedTyped(selfserve_task_gate_type::QUESTION->value, 8, [8 => true], [8 => false]))->toBeFalse();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
app_require('modules/selfserve/classes/selfserve_config_versioning.php');
|
||||
app_require('objects/selfserve_config_versions_o.php');
|
||||
|
||||
use modules\selfserve\classes\selfserve_config_versioning;
|
||||
use objects\selfserve_config_versions_o;
|
||||
|
||||
function selfserve_config_versioning_without_constructor(): selfserve_config_versioning
|
||||
{
|
||||
$reflection = new ReflectionClass(selfserve_config_versioning::class);
|
||||
return $reflection->newInstanceWithoutConstructor();
|
||||
}
|
||||
|
||||
it('validates typed task gates for known references', function (): void {
|
||||
$service = selfserve_config_versioning_without_constructor();
|
||||
|
||||
$validation = $service->validateConfig([
|
||||
'questions' => [
|
||||
['id' => 10],
|
||||
],
|
||||
'conditions' => [
|
||||
['id' => 20],
|
||||
],
|
||||
'rules' => [],
|
||||
'tasks' => [
|
||||
[
|
||||
'id' => 100,
|
||||
'gate_type' => 'ALWAYS',
|
||||
'gate_ref_id' => null,
|
||||
],
|
||||
[
|
||||
'id' => 101,
|
||||
'gate_type' => 'CONDITION',
|
||||
'gate_ref_id' => 20,
|
||||
],
|
||||
[
|
||||
'id' => 102,
|
||||
'gate_type' => 'QUESTION',
|
||||
'gate_ref_id' => 10,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
expect($validation['valid'])->toBeTrue();
|
||||
expect($validation['errors'])->toBe([]);
|
||||
});
|
||||
|
||||
it('fails validation when typed task gates reference unknown entities', function (): void {
|
||||
$service = selfserve_config_versioning_without_constructor();
|
||||
|
||||
$validation = $service->validateConfig([
|
||||
'questions' => [
|
||||
['id' => 1],
|
||||
],
|
||||
'conditions' => [
|
||||
['id' => 2],
|
||||
],
|
||||
'rules' => [],
|
||||
'tasks' => [
|
||||
[
|
||||
'id' => 200,
|
||||
'gate_type' => 'CONDITION',
|
||||
'gate_ref_id' => 999,
|
||||
],
|
||||
[
|
||||
'id' => 201,
|
||||
'gate_type' => 'QUESTION',
|
||||
'gate_ref_id' => 998,
|
||||
],
|
||||
[
|
||||
'id' => 202,
|
||||
'gate_type' => 'QUESTION',
|
||||
'gate_ref_id' => null,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
expect($validation['valid'])->toBeFalse();
|
||||
expect(implode("\n", $validation['errors']))->toContain('unknown condition gate_ref_id 999');
|
||||
expect(implode("\n", $validation['errors']))->toContain('unknown question gate_ref_id 998');
|
||||
expect(implode("\n", $validation['errors']))->toContain('requires gate_ref_id');
|
||||
});
|
||||
|
||||
it('encodes config json payloads with apostrophes before persistence', function (): void {
|
||||
$version = new class extends selfserve_config_versions_o {
|
||||
/** @var array<string,mixed> */
|
||||
public array $capturedPayload = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// Skip db bootstrap for this unit test.
|
||||
}
|
||||
|
||||
public function add_object(array $data): int
|
||||
{
|
||||
$this->capturedPayload = $data;
|
||||
return 123;
|
||||
}
|
||||
|
||||
public function getObjectProperties(): void
|
||||
{
|
||||
// No-op for this unit test.
|
||||
}
|
||||
|
||||
public function objectChanged(): void
|
||||
{
|
||||
// No-op for this unit test.
|
||||
}
|
||||
};
|
||||
|
||||
$version->add(
|
||||
42,
|
||||
selfserve_config_versioning::STATUS_DRAFT,
|
||||
1,
|
||||
[
|
||||
'questions' => [
|
||||
[
|
||||
'id' => 1,
|
||||
'question' => "Driver's side check",
|
||||
],
|
||||
],
|
||||
'conditions' => [],
|
||||
'rules' => [],
|
||||
'tasks' => [],
|
||||
],
|
||||
[
|
||||
'valid' => true,
|
||||
'errors' => [],
|
||||
'warnings' => [],
|
||||
],
|
||||
);
|
||||
|
||||
$configJson = $version->capturedPayload['config_json'] ?? null;
|
||||
$validationJson = $version->capturedPayload['validation_result_json'] ?? null;
|
||||
|
||||
expect($configJson)->toBeString();
|
||||
expect($validationJson)->toBeString();
|
||||
expect(json_decode($configJson, true)['questions'][0]['question'] ?? null)->toBe("Driver's side check");
|
||||
expect(json_decode($validationJson, true)['valid'] ?? null)->toBeTrue();
|
||||
});
|
||||
@@ -24,8 +24,27 @@ function selfserve_openapi_content_or_skip(): string
|
||||
test()->markTestSkipped('openapi.yaml is not available in this runtime environment.');
|
||||
}
|
||||
|
||||
function selfserve_openapi_path_block_or_fail(string $content, string $path): string
|
||||
{
|
||||
$path_marker = ' ' . $path . ':';
|
||||
$start = strpos($content, $path_marker);
|
||||
if ($start === false) {
|
||||
throw new RuntimeException("OpenAPI path block not found: {$path}");
|
||||
}
|
||||
|
||||
$rest = substr($content, $start + strlen($path_marker));
|
||||
$next_path = strpos($rest, "\n /");
|
||||
if ($next_path === false) {
|
||||
return substr($content, $start);
|
||||
}
|
||||
|
||||
return substr($content, $start, strlen($path_marker) + $next_path);
|
||||
}
|
||||
|
||||
it('documents self-serve machine type, eligibility, summary, and webhook endpoints', function (): void {
|
||||
$content = selfserve_openapi_content_or_skip();
|
||||
$allowedPathBlock = selfserve_openapi_path_block_or_fail($content, '/department/selfserve/vehicle/allowed');
|
||||
$summaryPathBlock = selfserve_openapi_path_block_or_fail($content, '/department/selfserve/washes/summary');
|
||||
|
||||
expect($content)->toContain('/department/selfserve/machine-types:');
|
||||
expect($content)->toContain('/department/selfserve/vehicle/allowed:');
|
||||
@@ -39,6 +58,10 @@ it('documents self-serve machine type, eligibility, summary, and webhook endpoin
|
||||
expect($content)->toContain('/modules/self-serve/lane/relay/machine_program_picker/set:');
|
||||
expect($content)->toContain('/modules/self-serve/lane/relay/machine_cleaner/status:');
|
||||
expect($content)->toContain('/modules/self-serve/lane/relay/machine_cleaner/set:');
|
||||
expect($allowedPathBlock)->toContain('vehicle_type_id:');
|
||||
expect($allowedPathBlock)->toContain('vehicle_type:');
|
||||
expect($summaryPathBlock)->toContain('vehicle_type_id:');
|
||||
expect($summaryPathBlock)->toContain('vehicle_type:');
|
||||
});
|
||||
|
||||
it('defines reusable self-serve wash and machine type schemas', function (): void {
|
||||
@@ -52,3 +75,14 @@ it('defines reusable self-serve wash and machine type schemas', function (): voi
|
||||
expect($content)->toContain('machine_type_id:');
|
||||
expect($content)->toContain('SelfServeLaneMachineRelayStatus:');
|
||||
});
|
||||
|
||||
it('documents in-progress self-serve wash start and machine relay fields', function (): void {
|
||||
$content = selfserve_openapi_content_or_skip();
|
||||
$inProgressPathBlock = selfserve_openapi_path_block_or_fail($content, '/modules/self-serve/lane/wash/in-progress');
|
||||
|
||||
expect($inProgressPathBlock)->toContain('included_minutes:');
|
||||
expect($inProgressPathBlock)->toContain('machine_relay_enabled:');
|
||||
expect($inProgressPathBlock)->toContain('machine_relay_enabled_at:');
|
||||
expect($inProgressPathBlock)->toContain('machine_start_triggered_at:');
|
||||
expect($inProgressPathBlock)->toContain('wash_started_at:');
|
||||
});
|
||||
|
||||
@@ -32,6 +32,33 @@ it('keeps machine type support wired into lanes, tasks, and conditions routes',
|
||||
->toContain('product');
|
||||
});
|
||||
|
||||
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('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'));
|
||||
|
||||
@@ -72,4 +99,31 @@ it('wires in-progress self-serve wash details endpoint', function (): void {
|
||||
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("'included_minutes' =>");
|
||||
expect($moduleSelfServeRoute)->toContain("'machine_relay_enabled' =>");
|
||||
expect($moduleSelfServeRoute)->toContain("'machine_relay_enabled_at' =>");
|
||||
expect($moduleSelfServeRoute)->toContain("'wash_started_at' =>");
|
||||
});
|
||||
|
||||
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)');
|
||||
expect($vehicleConditionsRoute)->toContain('synchronizeSession($lane, $reg, $customer_id, true, $vehicle_type_id)');
|
||||
|
||||
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);");
|
||||
});
|
||||
|
||||
@@ -12,3 +12,20 @@ it('enables cleaner relay on wash start command and webhook flow', function ():
|
||||
expect($washFlow)->toContain('enableCleanerRelayForStartedWash(');
|
||||
expect($washFlow)->toContain('setMachineCleanerRelayStatusHard(true)');
|
||||
});
|
||||
|
||||
it('keeps cleaner relay enable wired into machine relay start paths', function (): void {
|
||||
$washFlow = file_get_contents(app_path('modules/selfserve/classes/selfserve_wash_flow.php'));
|
||||
$moduleRoute = file_get_contents(app_path('routes/moduleSelfServeRoute.php'));
|
||||
|
||||
expect($washFlow)->not->toBeFalse();
|
||||
$enableMachineRelayMethodOffset = strpos($washFlow, 'protected function enableMachineRelayIfAllowed');
|
||||
expect($enableMachineRelayMethodOffset)->not->toBeFalse();
|
||||
$enableMachineRelayMethod = substr($washFlow, (int)$enableMachineRelayMethodOffset, 1200);
|
||||
expect($enableMachineRelayMethod)->toContain('$this->enableCleanerRelayForStartedWash($lane);');
|
||||
|
||||
expect($moduleRoute)->not->toBeFalse();
|
||||
$machineEnableRouteOffset = strpos($moduleRoute, '/modules/self-serve/lane/relay/machine/enable');
|
||||
expect($machineEnableRouteOffset)->not->toBeFalse();
|
||||
$machineEnableRoute = substr($moduleRoute, (int)$machineEnableRouteOffset, 1800);
|
||||
expect($machineEnableRoute)->toContain('$lane->setMachineCleanerRelayStatusHard(true);');
|
||||
});
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
<?php
|
||||
|
||||
it('forces machine relay off when a self-serve wash session is completed', function (): void {
|
||||
it('forces machine and cleaner relays off when a self-serve wash session is completed', function (): void {
|
||||
$washFlow = file_get_contents(app_path('modules/selfserve/classes/selfserve_wash_flow.php'));
|
||||
|
||||
expect($washFlow)->not->toBeFalse();
|
||||
expect($washFlow)->toContain('$this->disableMachineRelayForCompletedWash($laneId);');
|
||||
expect($washFlow)->toContain('setMachineRelayStatusHard(false)');
|
||||
|
||||
$methodOffset = strpos($washFlow, 'protected function disableMachineRelayForCompletedWash');
|
||||
expect($methodOffset)->not->toBeFalse();
|
||||
$methodBody = substr($washFlow, (int)$methodOffset, 1500);
|
||||
|
||||
expect($methodBody)->toContain('$this->turnOffRelayIfConfiguredAndOn($lane, selfserve_lane_relay::MACHINE);');
|
||||
expect($methodBody)->toContain('$this->turnOffRelayIfConfiguredAndOn($lane, selfserve_lane_relay::MACHINE_CLEANER);');
|
||||
|
||||
$helperOffset = strpos($washFlow, 'protected function turnOffRelayIfConfiguredAndOn');
|
||||
expect($helperOffset)->not->toBeFalse();
|
||||
$helperBody = substr($washFlow, (int)$helperOffset, 1500);
|
||||
|
||||
expect($helperBody)->toContain('$status = $lane->getRelayStatus($relay);');
|
||||
expect($helperBody)->toContain("if ((bool)(\$status['on'] ?? false) !== true)");
|
||||
expect($helperBody)->toContain('$lane->setRelayStatusHard($relay, false);');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user