787 lines
32 KiB
PHP
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;
|
|
}
|
|
}
|