341 lines
15 KiB
PHP
341 lines
15 KiB
PHP
<?php
|
|
|
|
namespace classes;
|
|
|
|
/**
|
|
* Ensures additive self-serve tables and columns exist.
|
|
*
|
|
* This project has no centralized migration runner, so the bootstrap must be
|
|
* idempotent and safe to call from runtime flows.
|
|
*/
|
|
class selfserve_schema_bootstrap
|
|
{
|
|
private static bool $initialized = false;
|
|
|
|
public static function ensureTables(): void
|
|
{
|
|
if (self::$initialized) {
|
|
return;
|
|
}
|
|
|
|
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,
|
|
description VARCHAR(255) 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,
|
|
UNIQUE KEY uniq_selfserve_machine_types_name (name)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
|
|
|
"CREATE TABLE IF NOT EXISTS selfserve_wash_sessions (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
lane_id INT NOT NULL,
|
|
department_id INT NOT NULL,
|
|
machine_type_id INT NULL,
|
|
customer_number INT NULL,
|
|
vehicle_id INT NULL,
|
|
vehicle_type_id INT NULL,
|
|
reg VARCHAR(255) NOT NULL,
|
|
status VARCHAR(64) NOT NULL DEFAULT 'PENDING_QUESTIONS',
|
|
allowed TINYINT(1) NOT NULL DEFAULT 0,
|
|
machine_relay_enabled TINYINT(1) NOT NULL DEFAULT 0,
|
|
machine_relay_enabled_at DATETIME NULL,
|
|
machine_start_triggered TINYINT(1) NOT NULL DEFAULT 0,
|
|
machine_start_triggered_at DATETIME NULL,
|
|
wash_started_at DATETIME NULL,
|
|
order_id INT NULL,
|
|
completed_at DATETIME NULL,
|
|
metadata_json JSON 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_wash_sessions_lane_reg (lane_id, reg),
|
|
INDEX idx_selfserve_wash_sessions_status (status),
|
|
INDEX idx_selfserve_wash_sessions_customer (customer_number),
|
|
INDEX idx_selfserve_wash_sessions_created_at (created_at),
|
|
INDEX idx_selfserve_wash_sessions_department_completed (department_id, completed_at),
|
|
INDEX idx_selfserve_wash_sessions_order (order_id)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
|
|
|
"CREATE TABLE IF NOT EXISTS selfserve_wash_session_answers (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
session_id INT NOT NULL,
|
|
question_id INT NOT NULL,
|
|
question_text VARCHAR(255) NOT NULL,
|
|
answer_value TINYINT(1) NOT NULL,
|
|
answered_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
|
deleted_at TIMESTAMP NULL DEFAULT NULL,
|
|
UNIQUE KEY uniq_selfserve_wash_session_answer (session_id, question_id),
|
|
INDEX idx_selfserve_wash_session_answers_session (session_id)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
|
|
|
"CREATE TABLE IF NOT EXISTS selfserve_wash_session_tasks (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
session_id INT NOT NULL,
|
|
task_id INT NULL,
|
|
task_text VARCHAR(255) NOT NULL,
|
|
description TEXT NULL,
|
|
services JSON NULL,
|
|
buttons JSON NULL,
|
|
dynamic_image_id INT 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_wash_session_tasks_session (session_id)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
|
|
|
"CREATE TABLE IF NOT EXISTS selfserve_wash_session_events (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
session_id INT NOT NULL,
|
|
event_type VARCHAR(64) NOT NULL,
|
|
payload_json JSON NULL,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
INDEX idx_selfserve_wash_session_events_session (session_id),
|
|
INDEX idx_selfserve_wash_session_events_type (event_type)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
|
|
|
"CREATE TABLE IF NOT EXISTS department_selfserve_studio_layouts (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
department_id INT NOT NULL,
|
|
user_id INT NULL,
|
|
layout_json JSON NOT 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_department_selfserve_studio_layouts_department_user (department_id, user_id),
|
|
INDEX idx_department_selfserve_studio_layouts_department_updated (department_id, updated_at)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
|
|
|
"CREATE TABLE IF NOT EXISTS department_selfserve_studio_virtual_hardware (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
department_id INT NOT NULL,
|
|
config_json JSON NOT NULL,
|
|
created_by INT NULL,
|
|
updated_by INT 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,
|
|
UNIQUE KEY uniq_selfserve_vhw_department (department_id),
|
|
INDEX idx_selfserve_vhw_dept_updated (department_id, updated_at)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
|
|
|
"CREATE TABLE IF NOT EXISTS department_selfserve_path_confirmations (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
department_id INT NOT NULL,
|
|
lane_id INT NULL,
|
|
vehicle_type_id INT NULL,
|
|
config_version_id INT NULL,
|
|
config_source VARCHAR(32) NOT NULL DEFAULT 'draft',
|
|
path_signature VARCHAR(128) NOT NULL,
|
|
result_signature VARCHAR(128) NOT NULL,
|
|
answers_json JSON NOT NULL,
|
|
result_json JSON NOT NULL,
|
|
scope_json JSON NULL,
|
|
confirmed_by INT NULL,
|
|
confirmed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
stale_reason VARCHAR(255) 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_path_conf_department_scope (department_id, lane_id, vehicle_type_id, config_version_id),
|
|
INDEX idx_selfserve_path_conf_signature (department_id, config_version_id, path_signature)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
|
|
];
|
|
|
|
foreach ($queries as $sql) {
|
|
$db->query($sql);
|
|
}
|
|
|
|
self::ensureColumn(
|
|
'department_lanes',
|
|
'machine_type_id',
|
|
'ALTER TABLE department_lanes ADD COLUMN machine_type_id INT NULL AFTER dynamic_image_id'
|
|
);
|
|
self::ensureColumn(
|
|
'department_lanes',
|
|
'selfserve_enabled',
|
|
'ALTER TABLE department_lanes ADD COLUMN selfserve_enabled TINYINT(1) NOT NULL DEFAULT 1 AFTER machine_type_id'
|
|
);
|
|
self::ensureColumn(
|
|
'department_selfserve_conditions',
|
|
'machine_type_id',
|
|
'ALTER TABLE department_selfserve_conditions ADD COLUMN machine_type_id INT NULL AFTER product'
|
|
);
|
|
self::ensureColumn(
|
|
'department_selfserve_tasks',
|
|
'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::ensureColumnDataType(
|
|
'department_selfserve_tasks',
|
|
'description',
|
|
['text', 'mediumtext', 'longtext'],
|
|
'ALTER TABLE department_selfserve_tasks MODIFY COLUMN description TEXT NULL AFTER task'
|
|
);
|
|
self::ensureColumnDataType(
|
|
'selfserve_wash_session_tasks',
|
|
'description',
|
|
['text', 'mediumtext', 'longtext'],
|
|
'ALTER TABLE selfserve_wash_session_tasks MODIFY COLUMN description TEXT NULL AFTER task_text'
|
|
);
|
|
self::ensureColumn(
|
|
'selfserve_wash_session_tasks',
|
|
'dynamic_images_vehicle_type',
|
|
'ALTER TABLE selfserve_wash_session_tasks ADD COLUMN dynamic_images_vehicle_type INT NULL AFTER buttons'
|
|
);
|
|
self::ensureColumn(
|
|
'selfserve_wash_sessions',
|
|
'wash_started_at',
|
|
'ALTER TABLE selfserve_wash_sessions ADD COLUMN wash_started_at DATETIME NULL AFTER machine_start_triggered_at'
|
|
);
|
|
self::ensureIndex(
|
|
'selfserve_wash_sessions',
|
|
'idx_selfserve_wash_sessions_department_completed',
|
|
'ALTER TABLE selfserve_wash_sessions ADD INDEX idx_selfserve_wash_sessions_department_completed (department_id, completed_at)'
|
|
);
|
|
self::ensureIndex(
|
|
'selfserve_wash_sessions',
|
|
'idx_selfserve_wash_sessions_order',
|
|
'ALTER TABLE selfserve_wash_sessions ADD INDEX idx_selfserve_wash_sessions_order (order_id)'
|
|
);
|
|
|
|
self::$initialized = true;
|
|
}
|
|
|
|
public static function tableHasColumn(string $table, string $column): bool
|
|
{
|
|
global $db;
|
|
$table = $db->escape_string($table);
|
|
$column = $db->escape_string($column);
|
|
$database = $db->escape_string($db->getDatabase());
|
|
|
|
$sql = "SELECT COUNT(*) AS c
|
|
FROM information_schema.COLUMNS
|
|
WHERE TABLE_SCHEMA = '$database'
|
|
AND TABLE_NAME = '$table'
|
|
AND COLUMN_NAME = '$column'";
|
|
$result = $db->query($sql);
|
|
if (!$result) {
|
|
return false;
|
|
}
|
|
$row = $result->fetch_assoc();
|
|
return ((int)($row['c'] ?? 0)) > 0;
|
|
}
|
|
|
|
public static function ensureColumn(string $table, string $column, string $alterSql): void
|
|
{
|
|
global $db;
|
|
if (self::tableHasColumn($table, $column)) {
|
|
return;
|
|
}
|
|
$db->query($alterSql);
|
|
}
|
|
|
|
public static function tableHasIndex(string $table, string $index): bool
|
|
{
|
|
global $db;
|
|
$table = $db->escape_string($table);
|
|
$index = $db->escape_string($index);
|
|
$database = $db->escape_string($db->getDatabase());
|
|
|
|
$sql = "SELECT COUNT(*) AS c
|
|
FROM information_schema.STATISTICS
|
|
WHERE TABLE_SCHEMA = '$database'
|
|
AND TABLE_NAME = '$table'
|
|
AND INDEX_NAME = '$index'";
|
|
$result = $db->query($sql);
|
|
if (!$result) {
|
|
return false;
|
|
}
|
|
$row = $result->fetch_assoc();
|
|
return ((int)($row['c'] ?? 0)) > 0;
|
|
}
|
|
|
|
public static function ensureIndex(string $table, string $index, string $alterSql): void
|
|
{
|
|
global $db;
|
|
if (self::tableHasIndex($table, $index)) {
|
|
return;
|
|
}
|
|
$db->query($alterSql);
|
|
}
|
|
|
|
/**
|
|
* @param array<int,string> $acceptedDataTypes
|
|
*/
|
|
public static function ensureColumnDataType(string $table, string $column, array $acceptedDataTypes, string $alterSql): void
|
|
{
|
|
global $db;
|
|
|
|
$columnInfo = self::columnInfo($table, $column);
|
|
if ($columnInfo === null) {
|
|
return;
|
|
}
|
|
|
|
$dataType = strtolower((string)($columnInfo['DATA_TYPE'] ?? ''));
|
|
$acceptedDataTypes = array_map(static fn(string $type): string => strtolower($type), $acceptedDataTypes);
|
|
if (in_array($dataType, $acceptedDataTypes, true)) {
|
|
return;
|
|
}
|
|
|
|
$db->query($alterSql);
|
|
}
|
|
|
|
/**
|
|
* @return array<string,mixed>|null
|
|
*/
|
|
public static function columnInfo(string $table, string $column): ?array
|
|
{
|
|
global $db;
|
|
$table = $db->escape_string($table);
|
|
$column = $db->escape_string($column);
|
|
$database = $db->escape_string($db->getDatabase());
|
|
|
|
$sql = "SELECT DATA_TYPE, COLUMN_TYPE, IS_NULLABLE, CHARACTER_MAXIMUM_LENGTH
|
|
FROM information_schema.COLUMNS
|
|
WHERE TABLE_SCHEMA = '$database'
|
|
AND TABLE_NAME = '$table'
|
|
AND COLUMN_NAME = '$column'
|
|
LIMIT 1";
|
|
$result = $db->query($sql);
|
|
if (!$result) {
|
|
return null;
|
|
}
|
|
$row = $result->fetch_assoc();
|
|
return is_array($row) ? $row : null;
|
|
}
|
|
}
|