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

787 lines
32 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/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_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\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): array
{
$snapshot = $this->buildEligibilitySnapshot($laneId, $reg, $customerNumber);
$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
{
$snapshot = $this->buildEligibilitySnapshot($laneId, $reg, $customerNumber);
$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->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']),
]);
if ($activateMachine && (bool)$snapshot['allowed']) {
$this->enableMachineRelayIfAllowed($snapshot, $session);
}
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)
|| empty($lane->department_lane->relay_machine_id)
|| trim((string)$lane->department_lane->relay_machine_id->value()) === ''
) {
return;
}
$lane->setMachineRelayStatusHard(false);
} catch (\Throwable) {
// Best effort only; session completion flow must continue.
}
}
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,
];
}
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): 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();
$vehicle = $this->findVehicleByRegistration($normalizedReg);
$vehicleData = $vehicle?->asArray();
$vehicleTypeId = $vehicle !== null ? (int)$vehicle->type->value() : null;
$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);
$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);
$activeTasks = [];
foreach ($tasks as $task) {
$gateId = $this->nullableInt($task['condition_id'] ?? null);
if (!$this->conditionEvaluator->taskGateSatisfied($gateId, $serviceConditionResults, $visibleAnswers)) {
continue;
}
$activeTasks[] = [
'id' => (int)$task['id'],
'task' => (string)$task['task'],
'description' => (string)($task['description'] ?? ''),
'condition_id' => $gateId,
'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,
];
}
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);
$lane->setLaneCache($laneId, $lane::CACHE_SELFSERVE_LANE_KEY_ALLOWED_SERVICES, $snapshot['allowed_services']);
$lane->turnOnRelay(selfserve_lane_relay::MACHINE);
$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 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'],
'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,
];
}
protected function loadQuestions(int $departmentId, int $laneId, ?int $vehicleTypeId): array
{
$questionsObject = new department_selfserve_questions_o();
$sharedQuestions = $questionsObject->getSharedQuestions();
if ($sharedQuestions !== []) {
return $sharedQuestions;
}
if ($vehicleTypeId === null) {
return [];
}
return $questionsObject->getLegacyQuestionsForLaneProduct($departmentId, $laneId, $vehicleTypeId);
}
protected function loadConditions(int $departmentId, int $laneId, ?int $vehicleTypeId, ?int $machineTypeId): array
{
$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);
}
protected function loadTasks(int $departmentId, int $laneId, ?int $vehicleTypeId, ?int $machineTypeId): array
{
$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);
}
protected function loadConditionRules(array $conditions): array
{
if ($conditions === []) {
return [];
}
$conditionIds = array_map(static fn(array $condition): int => (int)$condition['id'], $conditions);
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'],
);
}
}
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'],
'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 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;
}
}