diff --git a/services/nginx/app/cron/RunXLVaskModuleCron.php b/services/nginx/app/cron/RunXLVaskModuleCron.php index 2ca221c7..ac8f2ce9 100644 --- a/services/nginx/app/cron/RunXLVaskModuleCron.php +++ b/services/nginx/app/cron/RunXLVaskModuleCron.php @@ -19,7 +19,6 @@ $xlvask = new xlvask; try { if ($xlvask->config->enabled->isTrue() && $xlvask->config->synchronization_enabled->isTrue()) { $xlvask->getTasks()->runCronTasks(); - // TODO: Add synchronization for usage logs and vehicles. } } catch (Exception $e) { // This is automatically running, but an XL Vask module failure here diff --git a/services/nginx/app/modules/xlvask/helpers/xlvask_tasks.php b/services/nginx/app/modules/xlvask/helpers/xlvask_tasks.php index e0bc145d..eef40aaa 100644 --- a/services/nginx/app/modules/xlvask/helpers/xlvask_tasks.php +++ b/services/nginx/app/modules/xlvask/helpers/xlvask_tasks.php @@ -11,6 +11,7 @@ use objects\plate_scanners_o; use objects\users_o; use objects\xlvask_customers_o; use objects\xlvask_potential_order_matches_o; +use objects\xlvask_usage_logs_o; use objects\xlvask_vehicles_o; class xlvask_tasks @@ -271,11 +272,127 @@ class xlvask_tasks } /** - * Usage synchronization is intentionally disabled until the XL Vask integration is completed. + * Synchronize XL Vask usage logs. + * + * Pulls finished washes from XL Vask for the given window and persists them + * via the revision-aware {@see xlvask_usage_logs_o::importUsageLogsWithSummary()}. + * Potential order matches are also recorded so the linked-order UI has the + * data it needs to offer accept/deny actions. + * + * @param string|null $dateFrom Optional import start date or date-time modifier. Defaults to the last 7 days. + * @param string|null $dateTo Optional inclusive import end date. + * @return array{ + * fetched:int, + * new:int, + * updated:int, + * unchanged:int, + * invalid:int, + * errors:array>, + * linked:int, + * orders_created:int + * } Summary of the import run. + * @throws Exception If the XL Vask module is not enabled. */ - public function runSyncUsage(?string $dateFrom = null, ?string $dateTo = null): void + public function runSyncUsage(?string $dateFrom = null, ?string $dateTo = null): array { - unset($dateFrom, $dateTo); + $xlvask = new \classes\xlvask(); + $xlvask->requireModuleEnabled(); + if (!$xlvask->config->synchronization_enabled->isTrue()) { + return [ + 'fetched' => 0, 'new' => 0, 'updated' => 0, 'unchanged' => 0, + 'invalid' => 0, 'errors' => [], 'linked' => 0, 'orders_created' => 0, + ]; + } + + $summary = (new xlvask_usage_logs_o())->importUsageLogsWithSummary($dateFrom, $dateTo); + $linkSummary = $this->linkImportedUsageLogsToOrders($dateFrom, $dateTo); + + return [ + ...$summary, + 'linked' => $linkSummary['linked'], + 'orders_created' => $linkSummary['orders_created'], + ]; + } + + /** + * Find imported XL Vask usage logs that look like they belong to an existing order + * (same registration, department, and a window around the wash time) and record + * them as potential order matches so they show up in the linking UI. + * + * Optionally creates the missing order when automatic order creation is enabled, + * the customer has an external ID, and the wash qualifies for automatic continuance. + * + * @return array{linked:int, orders_created:int} + */ + private function linkImportedUsageLogsToOrders(?string $dateFrom, ?string $dateTo): array + { + global $db; + $xlvask = new \classes\xlvask(); + $orders_o = new orders_o(); + $matches = new xlvask_potential_order_matches_o(); + $linked = 0; + $ordersCreated = 0; + + $dateFromSql = $dateFrom !== null && $dateFrom !== '' + ? "'" . $db->escape_string(xlvask_usage_logs_o::formatImportDateFrom($dateFrom)) . "'" + : 'DATE_SUB(NOW(), INTERVAL 7 DAY)'; + $dateToSql = $dateTo !== null && $dateTo !== '' + ? "'" . $db->escape_string((string)$dateTo) . " 23:59:59'" + : 'NOW()'; + + $sql = "SELECT * FROM xlvask_usage_logs + WHERE StartTime >= {$dateFromSql} + AND StartTime <= {$dateToSql} + AND FinishStatus = 1 + AND WashId IS NOT NULL AND TRIM(WashId) <> '' + ORDER BY id ASC"; + $result = $db->query($sql); + if ($result === false || $result->num_rows === 0) { + return ['linked' => 0, 'orders_created' => 0]; + } + $rows = $db->fetch_all($result); + + $createEnabled = $xlvask->config->automatic_order_creation_enabled->isTrue(); + + foreach ($rows as $row) { + $log = (new $xlvask->helpers->xlvask_usage_log())->setProperties($row); + $washId = (string)$log->WashId; + if ($washId === '' || $log->hasDefaultCustomer() || !$log->hasExternalId()) { + continue; + } + if ($matches->doesWashPotentialOrderMatchExist($washId)) { + continue; // already linked or explicitly ignored + } + + $existingOrder = $log->getPotentialOrder(); + if ($existingOrder !== null && (int)$existingOrder->id > 0) { + $matches->add( + $washId, + (int)$existingOrder->id, + (string)$log->CustomerId, + (int)$log->CustomerId, + (int)$log->getDepartment()->id, + ); + $linked++; + continue; + } + + // No matching order — try to create one if automatic creation is on. + if (!$createEnabled || !$log->isEligibleForAutomaticContinuance(false)) { + continue; + } + try { + $customer = $log->getCustomer(); + $order = (new self())->createOrderFromWash($log, $customer); + if ($order !== null) { + $ordersCreated++; + } + } catch (Exception $e) { + error_log('[xlvask-tasks] auto-create order failed for wash ' . $washId . ': ' . $e->getMessage()); + } + } + + return ['linked' => $linked, 'orders_created' => $ordersCreated]; } private static function formatUsageLogs(array $getUsageLog): array