Files
api/services/nginx/app/modules/selfserve/classes/selfserve_wash_flow.php
T

1013 lines
43 KiB
PHP

<?php
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';
require_once WD . '/modules/selfserve/interfaces/selfserve_wash_flow_i.php';
require_once WD . '/objects/customer_vehicles_o.php';
require_once WD . '/objects/department_lanes_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/department_selfserve_vehicle_conditions_o.php';
require_once WD . '/objects/selfserve_machine_types_o.php';
require_once WD . '/objects/selfserve_wash_session_answers_o.php';
require_once WD . '/objects/selfserve_wash_session_events_o.php';
require_once WD . '/objects/selfserve_wash_session_tasks_o.php';
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;
use modules\selfserve\helpers\selfserve_lane_status;
use modules\selfserve\helpers\selfserve_wash_event_type;
use modules\selfserve\helpers\selfserve_wash_session_status;
use modules\selfserve\interfaces\selfserve_condition_evaluator_i;
use modules\selfserve\interfaces\selfserve_wash_flow_i;
use objects\customer_vehicles_o;
use objects\department_lanes_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\department_selfserve_vehicle_conditions_o;
use objects\selfserve_machine_types_o;
use objects\selfserve_wash_session_answers_o;
use objects\selfserve_wash_session_events_o;
use objects\selfserve_wash_session_tasks_o;
use objects\selfserve_wash_sessions_o;
class selfserve_wash_flow implements selfserve_wash_flow_i
{
public function __construct(
protected ?selfserve_condition_evaluator_i $conditionEvaluator = null,
) {
selfserve_schema_bootstrap::ensureTables();
$this->conditionEvaluator ??= new selfserve_condition_evaluator();
}
public function previewVehicleEligibility(int $laneId, string $reg, ?int $customerNumber = null, ?int $vehicleTypeIdOverride = null): array
{
$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, ?int $vehicleTypeIdOverride = null): array
{
$snapshot = $this->buildEligibilitySnapshot($laneId, $reg, $customerNumber, $vehicleTypeIdOverride);
$session = $this->findLatestOpenSession($laneId, $snapshot['reg'], $snapshot['customer_number']);
if (!$session->exists()) {
$session = (new selfserve_wash_sessions_o())->add(
$laneId,
(int)$snapshot['lane']['department'],
$snapshot['machine_type']['id'] ?? null,
$snapshot['customer_number'],
$snapshot['reg'],
$snapshot['vehicle']['id'] ?? null,
$snapshot['vehicle']['type'] ?? null,
$this->deriveBaseStatus($snapshot),
(bool)$snapshot['allowed'],
$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));
}
$this->syncSessionAnswers((int)$session->id, $snapshot['questions']);
$this->syncSessionTasks((int)$session->id, $snapshot['tasks']);
$this->logSessionEvent((int)$session->id, selfserve_wash_event_type::SESSION_SYNCED, [
'allowed' => (bool)$snapshot['allowed'],
'all_visible_questions_answered' => (bool)$snapshot['all_visible_questions_answered'],
'allowed_services' => $snapshot['allowed_services'],
'task_ids' => array_map(static fn(array $task): int => (int)$task['id'], $snapshot['tasks']),
]);
$this->syncMachineRelayFromVisibleServices($snapshot, $session, $activateMachine);
return $this->getSessionSummary((int)$session->id);
}
public function recordMachineStartWebhook(int $laneId, ?string $reg = null, array $payload = []): array
{
$normalizedReg = $reg === null ? null : selfserve::standardize_registration($reg);
$session = $normalizedReg !== null
? $this->findLatestOpenSession($laneId, $normalizedReg)
: $this->findLatestOpenSessionByLane($laneId);
if (!$session->exists()) {
if ($normalizedReg === null) {
throw new \RuntimeException('No active self-serve wash session found for the lane.');
}
$summary = $this->synchronizeSession($laneId, $normalizedReg, null, false);
$session = (new selfserve_wash_sessions_o())->select((int)$summary['session']['id']);
}
$lane = (new selfserve())->lane($laneId);
$effectiveReg = $normalizedReg ?? (string)$session->reg->value();
$customerNumber = $session->customer_number->value() === null ? null : (int)$session->customer_number->value();
if ($lane->getLaneStatus()->equals(selfserve_lane_status::AVAILABLE)) {
$lane->setLaneStatus(selfserve_lane_status::OCCUPIED);
}
if (!$lane->getLaneState()->equals(selfserve_lane_state::IN_WASH)) {
$lane->setLaneState(selfserve_lane_state::IN_WASH);
}
if ($effectiveReg !== '') {
$lane->setLicensePlate($effectiveReg);
}
if ($customerNumber !== null && $customerNumber > 0) {
$lane->setCustomerNumber($customerNumber);
}
if ((int)$lane->getWashStartTime() <= 0) {
$lane->setWashStartTime(time());
}
$this->enableCleanerRelayForStartedWash($lane);
$session->markMachineStartTriggered();
$this->logSessionEvent((int)$session->id, selfserve_wash_event_type::MACHINE_START_TRIGGERED, $payload + [
'lane_id' => $laneId,
'reg' => $effectiveReg,
'customer_number' => $customerNumber,
]);
return $this->getSessionSummary((int)$session->id);
}
protected function enableCleanerRelayForStartedWash(selfserve_lane $lane): void
{
try {
if (
empty($lane->department_lane)
|| empty($lane->department_lane->relay_machine_cleaner_id)
|| trim((string)$lane->department_lane->relay_machine_cleaner_id->value()) === ''
) {
return;
}
$lane->setMachineCleanerRelayStatusHard(true);
} catch (\Throwable) {
// Best effort only; webhook start flow must continue.
}
}
protected function disableMachineRelayForCompletedWash(int $laneId): void
{
try {
$lane = (new selfserve())->lane($laneId);
if (empty($lane->department_lane)) {
return;
}
$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);
if (!$session->exists()) {
throw new \RuntimeException('Self-serve wash session not found.');
}
$lane = (new department_lanes_o())->select((int)$session->lane_id->value());
$machineType = null;
if ($session->machine_type_id->value() !== null) {
$machineTypeObject = (new selfserve_machine_types_o())->select((int)$session->machine_type_id->value());
if ($machineTypeObject->exists()) {
$machineType = $machineTypeObject->asArray();
}
}
$answerRows = (new selfserve_wash_session_answers_o())->listBySession($sessionId);
$answers = $this->buildSessionQuestions($session, $answerRows);
$tasks = array_map(function (array $row): array {
return [
'task_id' => $row['task_id'] === null ? null : (int)$row['task_id'],
'task' => (string)$row['task_text'],
'description' => $row['description'] === null ? null : (string)$row['description'],
'services' => $this->normalizeJsonArray($row['services'] ?? null),
'buttons' => $this->normalizeJsonArray($row['buttons'] ?? null),
];
}, (new selfserve_wash_session_tasks_o())->listBySession($sessionId));
$events = array_map(function (array $row): array {
return [
'id' => (int)$row['id'],
'type' => (string)$row['event_type'],
'payload' => $this->normalizeJsonValue($row['payload_json'] ?? null),
'created_at' => (string)$row['created_at'],
];
}, (new selfserve_wash_session_events_o())->listBySession($sessionId));
return [
'session' => $session->asArray(),
'lane' => $lane->exists() ? $lane->asArray() : null,
'machine_type' => $machineType,
'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,
];
}
public function getLatestSessionSummary(int $laneId, string $reg): array
{
$session = (new selfserve_wash_sessions_o())->selectLatestByLaneAndReg($laneId, selfserve::standardize_registration($reg));
if (!$session->exists()) {
throw new \RuntimeException('No self-serve wash session found for the lane and vehicle.');
}
return $this->getSessionSummary((int)$session->id);
}
public function completeLatestSessionForLane(int $laneId, ?string $reg = null, ?int $customerNumber = null, ?int $orderId = null): ?array
{
$session = $reg !== null
? $this->findLatestOpenSession($laneId, selfserve::standardize_registration($reg), $customerNumber)
: $this->findLatestOpenSessionByLane($laneId, $customerNumber);
if (!$session->exists()) {
return null;
}
$session->markCompleted($orderId);
$this->disableMachineRelayForCompletedWash($laneId);
$this->logSessionEvent((int)$session->id, selfserve_wash_event_type::SESSION_COMPLETED, [
'lane_id' => $laneId,
'reg' => $reg === null ? (string)$session->reg->value() : selfserve::standardize_registration($reg),
'customer_number' => $customerNumber ?? ($session->customer_number->value() === null ? null : (int)$session->customer_number->value()),
'order_id' => $orderId,
]);
return $this->getSessionSummary((int)$session->id);
}
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);
if (!$lane->exists()) {
throw new \RuntimeException('Department lane not found.');
}
$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 = $this->resolveVehicleTypeId($vehicle, $vehicleTypeIdOverride);
$resolvedCustomerNumber = $customerNumber ?? ($vehicle !== null ? (int)$vehicle->customer_id->value() : null);
$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);
$visibleQuestions = [];
$visibleQuestionIds = [];
foreach ($questions as $question) {
$gateId = $this->nullableInt($question['condition_id'] ?? null);
if ($gateId !== null && (($visibilityConditionResults[$gateId] ?? false) !== true)) {
continue;
}
$questionId = (int)$question['id'];
$visibleQuestionIds[] = $questionId;
$visibleQuestions[] = [
'id' => $questionId,
'question' => (string)$question['question'],
'description' => (string)($question['description'] ?? ''),
'condition_id' => $gateId,
'order_priority' => (int)($question['order_priority'] ?? 0),
'answer' => array_key_exists($questionId, $answers) ? (bool)$answers[$questionId] : null,
];
}
usort($visibleQuestions, static fn(array $a, array $b): int => $a['order_priority'] <=> $b['order_priority']);
$visibleAnswers = $this->filterAnswersToVisibleQuestions($answers, $visibleQuestionIds);
$serviceConditionResults = $this->conditionEvaluator->evaluate($conditions, $rules, $visibleAnswers);
$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);
$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;
}
$activeTasks[] = [
'id' => (int)$task['id'],
'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)),
];
}
usort($activeTasks, static fn(array $a, array $b): int => $a['order_priority'] <=> $b['order_priority']);
$allowedServices = [];
foreach ($activeTasks as $task) {
foreach ($task['services'] as $service) {
if (!in_array($service, $allowedServices, true)) {
$allowedServices[] = $service;
}
}
}
$machineAvailable = !empty($lane->relay_machine_id->value());
$allVisibleQuestionsAnswered = true;
foreach ($visibleQuestions as $question) {
if ($question['answer'] === null) {
$allVisibleQuestionsAnswered = false;
break;
}
}
$machineAllowed = $allVisibleQuestionsAnswered
&& $machineAvailable
&& in_array(selfserve_lane_services::MACHINE->name, $allowedServices, true);
$machineType = null;
if ($machineTypeId !== null) {
$machineTypeObject = (new selfserve_machine_types_o())->select($machineTypeId);
if ($machineTypeObject->exists()) {
$machineType = $machineTypeObject->asArray();
}
}
return [
'lane' => $lane->asArray(),
'machine_type' => $machineType,
'vehicle' => $vehicleData,
'reg' => $normalizedReg,
'customer_number' => $resolvedCustomerNumber,
'vehicle_type_id' => $vehicleTypeId,
'answers' => $answers,
'questions' => $visibleQuestions,
'conditions' => $serviceConditionResults,
'tasks' => $activeTasks,
'allowed_services' => $allowedServices,
'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,
],
];
}
protected function enableMachineRelayIfAllowed(array $snapshot, selfserve_wash_sessions_o $session): void
{
if ((bool)$session->machine_relay_enabled->value() === true) {
return;
}
$laneId = (int)$snapshot['lane']['id'];
$lane = (new selfserve())->lane($laneId);
$this->enableCleanerRelayForStartedWash($lane);
$session->markRelayEnabled();
$this->logSessionEvent((int)$session->id, selfserve_wash_event_type::MACHINE_RELAY_ENABLED, [
'lane_id' => $laneId,
'reg' => $snapshot['reg'],
'allowed_services' => $snapshot['allowed_services'],
'task_ids' => array_map(static fn(array $task): int => (int)$task['id'], $snapshot['tasks']),
]);
}
protected function syncMachineRelayFromVisibleServices(array $snapshot, selfserve_wash_sessions_o $session, bool $allowEnable): void
{
$laneId = (int)$snapshot['lane']['id'];
$lane = (new selfserve())->lane($laneId);
$sync = $lane->syncMachineRelayFromVisibleServices(
is_array($snapshot['allowed_services'] ?? null) ? $snapshot['allowed_services'] : [],
$allowEnable
);
if (($sync['relay_action'] ?? '') === 'enabled') {
$this->enableMachineRelayIfAllowed($snapshot, $session);
}
$relayTargetOn = (bool)($sync['relay_target_on'] ?? false);
if (!$relayTargetOn && (bool)$session->machine_relay_enabled->value() === true) {
$session->markRelayDisabled();
$session->updateStatus($this->deriveCurrentStatus($snapshot, $session));
}
}
protected function deriveBaseStatus(array $snapshot): selfserve_wash_session_status
{
if (!$snapshot['all_visible_questions_answered']) {
return selfserve_wash_session_status::PENDING_QUESTIONS;
}
if ($snapshot['allowed']) {
return selfserve_wash_session_status::READY_FOR_MACHINE_START;
}
return selfserve_wash_session_status::MACHINE_NOT_ALLOWED;
}
protected function deriveCurrentStatus(array $snapshot, selfserve_wash_sessions_o $session): selfserve_wash_session_status
{
if ($session->completed_at->value() !== null) {
return selfserve_wash_session_status::COMPLETED;
}
if ((bool)$session->machine_start_triggered->value() === true) {
return selfserve_wash_session_status::MACHINE_STARTED;
}
if ((bool)$session->machine_relay_enabled->value() === true) {
return selfserve_wash_session_status::MACHINE_RELAY_ENABLED;
}
return $this->deriveBaseStatus($snapshot);
}
protected function formatSnapshotResponse(array $snapshot, ?array $session = null): array
{
return [
'lane' => $snapshot['lane'],
'machine_type' => $snapshot['machine_type'],
'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'],
'machine_available' => $snapshot['machine_available'],
'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,
];
}
/**
* @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 !== []) {
return $sharedQuestions;
}
if ($vehicleTypeId === null) {
return [];
}
return $questionsObject->getLegacyQuestionsForLaneProduct($departmentId, $laneId, $vehicleTypeId);
}
/**
* @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);
if ($machineTypeConditions !== []) {
return $machineTypeConditions;
}
}
if ($vehicleTypeId === null) {
return [];
}
return $conditionsObject->getLegacyConditionsForLaneProduct($departmentId, $laneId, $vehicleTypeId);
}
/**
* @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);
if ($machineTypeTasks !== []) {
return $machineTypeTasks;
}
}
if ($vehicleTypeId === null) {
return [];
}
return $tasksObject->getLegacyTasksForLaneProduct($departmentId, $laneId, $vehicleTypeId);
}
/**
* @param array<string,mixed>|null $publishedConfig
*/
protected function loadConditionRules(array $conditions, ?array $publishedConfig = null): array
{
if ($conditions === []) {
return [];
}
$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,
], ['id', 'condition_id', 'type', 'object_type', 'object_id', 'name', 'description']);
}
protected function syncSessionAnswers(int $sessionId, array $questions): void
{
$answersObject = new selfserve_wash_session_answers_o();
$answeredQuestionIds = [];
foreach ($questions as $question) {
if ($question['answer'] === null) {
continue;
}
$answeredQuestionIds[] = (int)$question['id'];
$answersObject->upsert(
$sessionId,
(int)$question['id'],
(string)$question['question'],
(bool)$question['answer'],
);
}
$answersObject->deleteMissingForSession($sessionId, $answeredQuestionIds);
}
protected function syncSessionTasks(int $sessionId, array $tasks): void
{
$tasksObject = new selfserve_wash_session_tasks_o();
$tasksObject->deleteBySession($sessionId);
foreach ($tasks as $task) {
$tasksObject->addSnapshot(
$sessionId,
(int)$task['id'],
(string)$task['task'],
(string)$task['description'],
$task['services'],
$task['buttons'],
$task['dynamic_images_vehicle_type'] ?? null,
);
}
}
protected function logSessionEvent(int $sessionId, selfserve_wash_event_type $eventType, ?array $payload = null): void
{
(new selfserve_wash_session_events_o())->add($sessionId, $eventType, $payload);
}
protected function buildSessionMetadata(array $snapshot): array
{
return [
'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'],
'question' => (string)$question['question'],
'order_priority' => (int)($question['order_priority'] ?? 0),
], $snapshot['questions']),
'task_ids' => array_map(static fn(array $task): int => (int)$task['id'], $snapshot['tasks']),
];
}
/**
* @param array<int,array<string,mixed>> $answerRows
* @return array<int,array<string,mixed>>
*/
protected function buildSessionQuestions(selfserve_wash_sessions_o $session, array $answerRows): array
{
$answersByQuestionId = [];
foreach ($answerRows as $row) {
$answersByQuestionId[(int)$row['question_id']] = $row;
}
$metadata = $session->metadata_json->value();
$metadata = is_array($metadata) ? $metadata : [];
$visibleQuestions = $this->resolveVisibleQuestionsFromMetadata(
$metadata,
(int)$session->department_id->value(),
(int)$session->lane_id->value(),
$session->vehicle_type_id->value() === null ? null : (int)$session->vehicle_type_id->value(),
);
if ($visibleQuestions === []) {
return array_map(static function (array $row): array {
return [
'question_id' => (int)$row['question_id'],
'question' => (string)$row['question_text'],
'answer' => (bool)$row['answer_value'],
'answered_at' => (string)$row['answered_at'],
];
}, $answerRows);
}
$questions = [];
foreach ($visibleQuestions as $visibleQuestion) {
$questionId = (int)$visibleQuestion['id'];
if ($questionId <= 0) {
continue;
}
$answerRow = $answersByQuestionId[$questionId] ?? null;
$questions[] = [
'question_id' => $questionId,
'question' => $answerRow === null ? (string)$visibleQuestion['question'] : (string)$answerRow['question_text'],
'answer' => $answerRow === null ? null : (bool)$answerRow['answer_value'],
'answered_at' => $answerRow === null ? null : (string)$answerRow['answered_at'],
];
unset($answersByQuestionId[$questionId]);
}
foreach ($answerRows as $row) {
$questionId = (int)$row['question_id'];
if (!array_key_exists($questionId, $answersByQuestionId)) {
continue;
}
$questions[] = [
'question_id' => $questionId,
'question' => (string)$row['question_text'],
'answer' => (bool)$row['answer_value'],
'answered_at' => (string)$row['answered_at'],
];
unset($answersByQuestionId[$questionId]);
}
return $questions;
}
/**
* @param array<string,mixed> $metadata
* @return array<int,array{id:int,question:string,order_priority:int}>
*/
protected function resolveVisibleQuestionsFromMetadata(array $metadata, int $departmentId, int $laneId, ?int $vehicleTypeId): array
{
$visibleQuestions = [];
if (isset($metadata['visible_questions']) && is_array($metadata['visible_questions'])) {
foreach ($metadata['visible_questions'] as $visibleQuestion) {
if (!is_array($visibleQuestion)) {
continue;
}
$questionId = (int)($visibleQuestion['id'] ?? 0);
if ($questionId <= 0) {
continue;
}
$visibleQuestions[] = [
'id' => $questionId,
'question' => (string)($visibleQuestion['question'] ?? ''),
'order_priority' => (int)($visibleQuestion['order_priority'] ?? 0),
];
}
return $visibleQuestions;
}
if (!isset($metadata['visible_question_ids']) || !is_array($metadata['visible_question_ids'])) {
return [];
}
$questionById = [];
foreach ($this->loadQuestions($departmentId, $laneId, $vehicleTypeId) as $question) {
$questionById[(int)$question['id']] = [
'question' => (string)($question['question'] ?? ''),
'order_priority' => (int)($question['order_priority'] ?? 0),
];
}
foreach ($metadata['visible_question_ids'] as $visibleQuestionId) {
$questionId = (int)$visibleQuestionId;
if ($questionId <= 0) {
continue;
}
$visibleQuestions[] = [
'id' => $questionId,
'question' => (string)($questionById[$questionId]['question'] ?? ''),
'order_priority' => (int)($questionById[$questionId]['order_priority'] ?? 0),
];
}
return $visibleQuestions;
}
protected function findVehicleByRegistration(string $reg): ?customer_vehicles_o
{
$vehicle = (new customer_vehicles_o())->selectByPlate($reg);
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();
$session->selectLatestOpenByLaneAndReg($laneId, $reg, $customerNumber);
return $session;
}
protected function findLatestOpenSessionByLane(int $laneId, ?int $customerNumber = null): selfserve_wash_sessions_o
{
$rows = (new selfserve_wash_sessions_o())->getFieldsWhere(
[
'lane_id' => $laneId,
'completed_at' => null,
'deleted_at' => null,
...($customerNumber !== null ? ['customer_number' => $customerNumber] : []),
],
['id']
);
if ($rows === []) {
return new selfserve_wash_sessions_o();
}
usort($rows, static fn(array $a, array $b): int => (int)$b['id'] <=> (int)$a['id']);
return (new selfserve_wash_sessions_o())->select((int)$rows[0]['id']);
}
protected function nullableInt(mixed $value): ?int
{
if ($value === null || $value === '' || $value === 0 || $value === '0') {
return null;
}
return (int)$value;
}
/**
* @param array<int,bool|null> $answers
* @param array<int,int> $visibleQuestionIds
* @return array<int,bool|null>
*/
protected function filterAnswersToVisibleQuestions(array $answers, array $visibleQuestionIds): array
{
$filtered = [];
foreach ($visibleQuestionIds as $questionId) {
$questionId = (int)$questionId;
if ($questionId <= 0 || !array_key_exists($questionId, $answers)) {
continue;
}
$filtered[$questionId] = $answers[$questionId];
}
return $filtered;
}
protected function normalizeJsonValue(mixed $value): mixed
{
if ($value === null || $value === '') {
return null;
}
if (is_array($value)) {
return $value;
}
if (is_string($value)) {
$decoded = json_decode($value, true);
if (json_last_error() === JSON_ERROR_NONE) {
return $decoded;
}
}
return $value;
}
protected function normalizeJsonArray(mixed $value): array
{
$decoded = $this->normalizeJsonValue($value);
return is_array($decoded) ? $decoded : [];
}
protected function normalizeServiceNames(array $services): array
{
$normalized = [];
foreach ($services as $service) {
$name = strtoupper(trim((string)$service));
if ($name === '') {
continue;
}
if (!in_array($name, $normalized, true)) {
$normalized[] = $name;
}
}
return $normalized;
}
protected function normalizeIntArray(array $values): array
{
$normalized = [];
foreach ($values as $value) {
$intValue = (int)$value;
if (!in_array($intValue, $normalized, true)) {
$normalized[] = $intValue;
}
}
return $normalized;
}
public function isMachineAllowedToStartWash(int $id): bool
{
$sessionSummary = $this->getSessionSummary($id);
return ($sessionSummary['session']['allowed'] ?? false) === true;
}
}