get('/modules/xlvask/services/usage/orders', function () { // Define the permissions: $permission_list_own = 'list_xlvask_usage_orders_own'; // Permission to list own orders (Without department filter) $permission_list_all = 'list_xlvask_usage_orders_all'; // Permission to list all orders (With department filter) $response_includes_items = false; // Whether to include items in the response (This would increase the memory usage significantly) // Require the user to be logged in global $response; self::requirePermission($permission_list_own); // Check if the user has permission to list all entries if (self::hasPermission($permission_list_all)) { $this->requirePermission($permission_list_all); } else { $this->requirePermission($permission_list_own); } // Get the user object $user = (new authentication())->get_user(); // Check if the request was successful if ($user) { $allowedHallIds = self::allowedHallIdsForUser($user); if ($allowedHallIds === []) { $response->error('No XL Vask hall scope is available', 403); return; } $xlvask_usage_logs = new xlvask_usage_logs_o(); $xlvask = new xlvask(); $automation_service = new xlvask_automation_service(); $linked_order_ids_by_wash_id = []; $xlvask->new($xlvask->helpers->xlvask_usage_log)->getDepartment(); $orders_o = new orders_o(); $xlvask_usage_log = $xlvask->new($xlvask->helpers->xlvask_usage_log); // Increase the memory limit to 512MB (Provided it's currently less than that) if (ini_get('memory_limit') < '5120M') { ini_set('memory_limit', '5120M'); } // Return the list of usage logs $result = $xlvask_usage_logs // Make sure the Customer is not in the default customers list ->setAdditionalWhereClause("`Customer` NOT IN ('" . implode("', '", $xlvask_usage_log::$default_customers) . "')") ->listObjectsWithPaginationIfSet( function ($log) use ($response_includes_items, $xlvask_usage_logs, $xlvask, $automation_service, &$linked_order_ids_by_wash_id) { // Remove the 'id' field from the log $id = (int)$log['id']; $automation = $automation_service->readAutomationStateByUsageLogId($id, $log); $amount_summary = $xlvask_usage_logs->getAmountSummaryReadOnly($log); unset($log['id']); // Convert the 'WashItems' field from JSON to an array $log['WashItems'] = json_decode($log['WashItems'], true); $usage_log_payload = array_intersect_key($log, array_flip([ 'WashId', 'CustomerId', 'Customer', 'VatNumber', 'Location', 'Hall', 'HallId', 'StartTime', 'FinishTime', 'RegistrationNumber', 'VehicleType', 'IdentificationType', 'IdentificationId', 'Info', 'Updated', 'Prepaid', 'FinishStatus', 'CustomerGuid', 'VehicleId', 'WashItems', 'ignored_at', 'ignored_by', 'ignored_reason', ])); // Create a new xlvask usage log object $tmp = $xlvask->new($xlvask->helpers->xlvask_usage_log); // Set the properties of the temporary object $tmp->setProperties($usage_log_payload); $wash_id = (string)$tmp->WashId; if ($wash_id !== '' && !array_key_exists($wash_id, $linked_order_ids_by_wash_id)) { $linked_order = (new orders_o())->selectByWashId($wash_id); $linked_order_ids_by_wash_id[$wash_id] = $linked_order !== null ? (int)$linked_order->id : null; } $linked_order_id = $wash_id !== '' ? $linked_order_ids_by_wash_id[$wash_id] : null; // Define the result structure $isEligibleForAutomaticContinuance = $tmp->isEligibleForAutomaticContinuance(true); // Return the result $tmp_res = ($isEligibleForAutomaticContinuance ? (new orders_o())->simulateOrderFromXLVask($tmp, $response_includes_items) : []); $tmp_res['order']['customer_name'] = $tmp->Customer; // Add the customer name to the order $tmp_res['order']['total_net_amount'] = $amount_summary['total_net_amount']; $tmp_res['order']['xlvask_primary_product_name'] = $amount_summary['primary_product_name']; $tmp_res['order']['xlvask_amount_cached'] = $amount_summary['cached']; // Clear memory unset($tmp); // Return the result return [ 'id' => $id, // Return the ID of the log 'fast_link_key' => null, 'automation' => $automation, 'source_hash' => $log['source_hash'] ?? null, 'source_revision' => $log['source_revision'] ?? null, 'source_observed_at' => $log['source_observed_at'] ?? null, 'source_stable_since' => $log['source_stable_since'] ?? null, 'source_observation_count' => (int)($log['source_observation_count'] ?? 0), 'import_state' => $log['import_state'] ?? 'unchanged', 'resolution_state' => $log['resolution_state'] ?? 'needs_review', 'certainty' => $log['certainty'] ?? 'none', 'planned_action' => $log['planned_action'] ?? 'none', 'state_reason' => $log['state_reason'] ?? null, 'expected_version' => isset($log['expected_version']) ? (int)$log['expected_version'] : 1, 'last_run_id' => isset($log['last_run_id']) ? (int)$log['last_run_id'] : null, 'last_evaluated_at' => $log['last_evaluated_at'] ?? null, ...$tmp_res['order'], // Return the simulated order from XLVask (with or without items) 'usage_log_id' => $id, 'linked_order_id' => $linked_order_id, ]; }, $xlvask_usage_logs->forceRestrictFilters( [ // This makes sure that the user can only see department logs that belong to their departments 'HallId' => $allowedHallIds, 'FinishStatus' => ['1'], // Only show finished logs ] ) ); // Return the response $response->success($result); } else { // Return an error $response->error('Invalid session', 400); } }, [ 'list_xlvask_usage_orders_own' => 'List own xlvask usage orders (Without department filter)', 'list_xlvask_usage_orders_all' => 'List all xlvask usage orders (With department filter)', ] ); $this->get('/modules/xlvask/services/usage/orders/summary', function () { global $response; $this->requirePermission('list_xlvask_usage_orders_own'); $user = (new authentication())->get_user(); if (!$user) { $response->error('Invalid session', 400); } $dateFrom = $this->isParametersSet(['dateFrom']) ? (string)$this->getParameter('dateFrom') : null; $dateTo = $this->isParametersSet(['dateTo']) ? (string)$this->getParameter('dateTo') : null; $response->success([ 'summary' => (new xlvask_autopilot_service())->getSummary( $dateFrom, $dateTo, self::allowedHallIdsForUser($user) ), ]); }, [ 'list_xlvask_usage_orders_own' => 'Read XL Vask usage-log automation summary', ] ); $this->post('/modules/xlvask/services/usage/autopilot-runs', function () { global $response; $this->requirePermission('manage_xlvask_usage_automation'); $user = (new authentication())->get_user(); if (!$user) { $response->error('Invalid session', 400); } $input = []; foreach (['ids', 'dateFrom', 'dateTo', 'limit', 'forceRefetch', 'mode', 'idempotency_key'] as $key) { if ($this->isParametersSet([$key])) { $input[$key] = $this->getParameter($key); } } $response->success([ 'run' => (new xlvask_autopilot_service())->createRun( $input, (int)$user->id, self::allowedHallIdsForUser($user) ), ], 202); }, [ 'manage_xlvask_usage_automation' => 'Create an XL Vask usage-log autopilot run', ] ); $this->get('/modules/xlvask/services/usage/automation/admin/readiness', function () { global $response; $this->requirePermission('superuser_xlvask_automation_activate'); $response->success((new xlvask_autopilot_service())->activationReadiness()); }, [ 'superuser_xlvask_automation_activate' => 'Inspect XL Vask automation activation readiness', ]); $this->post('/modules/xlvask/services/usage/automation/admin/calibrations/backtest', function () { global $response; $this->requirePermission('superuser_xlvask_automation_activate'); self::requireParameters(['segment_key']); $user = (new authentication())->get_user(); if (!$user) { $response->error('Invalid session', 400); } $response->success([ 'artifact' => (new xlvask_autopilot_service())->generateCalibrationArtifact( trim((string)$this->getParameter('segment_key')), (int)$user->id ), ]); }, [ 'superuser_xlvask_automation_activate' => 'Generate an inactive XL Vask historical calibration artifact', ]); $this->post('/modules/xlvask/services/usage/automation/admin/calibrations/labels', function () { global $response; $this->requirePermission('superuser_xlvask_automation_activate'); self::requireParameters(['suggestion_id', 'outcome']); $user = (new authentication())->get_user(); if (!$user) { $response->error('Invalid session', 400); } $response->success((new xlvask_autopilot_service())->adjudicateCalibrationLabel( (int)$this->getParameter('suggestion_id'), trim((string)$this->getParameter('outcome')), (int)$user->id )); }, [ 'superuser_xlvask_automation_activate' => 'Adjudicate one exact XL Vask suggestion for calibration evidence', ]); $this->post('/modules/xlvask/services/usage/automation/admin/calibrations/{id}/activate', function () { global $response; $this->requirePermission('superuser_xlvask_automation_activate'); self::requireParameters(['artifact_hash', 'confirmation_text']); $user = (new authentication())->get_user(); if (!$user) { $response->error('Invalid session', 400); } $response->success((new xlvask_autopilot_service())->activateCalibration( (int)($this->fromRoute('id') ?? 0), trim((string)$this->getParameter('artifact_hash')), (string)$this->getParameter('confirmation_text'), (int)$user->id )); }, [ 'superuser_xlvask_automation_activate' => 'Explicitly activate a qualifying XL Vask calibration artifact', ]); $this->post('/modules/xlvask/services/usage/automation/admin/wash-id-uniqueness/activate', function () { global $response; $this->requirePermission('superuser_xlvask_automation_activate'); self::requireParameters(['confirmation_text']); $response->success((new xlvask_autopilot_service())->activateWashIdUniqueness( (string)$this->getParameter('confirmation_text') )); }, [ 'superuser_xlvask_automation_activate' => 'Explicitly activate the guarded XL Vask wash-id uniqueness migration', ]); $this->get('/modules/xlvask/services/usage/autopilot-runs/{id}', function () { global $response; $this->requirePermission('manage_xlvask_usage_automation'); $id = (int)($this->fromRoute('id') ?? 0); if ($id < 1) { $response->error('Invalid XL Vask autopilot run id', 400); } $user = (new authentication())->get_user(); if (!$user) { $response->error('Invalid session', 400); } $run = (new xlvask_autopilot_service())->getRun( $id, (int)$user->id, self::allowedHallIdsForUser($user) ); $response->success(['run' => $run]); }, [ 'manage_xlvask_usage_automation' => 'Read XL Vask usage-log autopilot run status', ] ); $this->post('/modules/xlvask/services/usage/automation/decisions/preview', function () { global $response; $this->requirePermission('manage_xlvask_usage_automation'); $user = (new authentication())->get_user(); if (!$user) { $response->error('Invalid session', 400); } $input = []; foreach (['usage_log_ids', 'action', 'suggestion_id', 'order_id', 'reason'] as $key) { if ($this->isParametersSet([$key])) { $input[$key] = $this->getParameter($key); } } $response->success([ 'preview' => (new xlvask_autopilot_service())->createDecisionPreview( $input, (int)$user->id, self::allowedHallIdsForUser($user) ), ]); }, ['manage_xlvask_usage_automation' => 'Preview an XL Vask automation decision']); $this->post('/modules/xlvask/services/usage/automation/decisions/apply', function () { global $response; $this->requirePermission('manage_xlvask_usage_automation'); $user = (new authentication())->get_user(); if (!$user) { $response->error('Invalid session', 400); } $input = []; foreach (['preview_id', 'selection_hash', 'confirmation_text'] as $key) { if ($this->isParametersSet([$key])) { $input[$key] = $this->getParameter($key); } } $response->success( (new xlvask_autopilot_service())->applyDecision( $input, (int)$user->id, self::allowedHallIdsForUser($user) ) ); }, ['manage_xlvask_usage_automation' => 'Apply a previewed XL Vask automation decision']); $this->patch('/modules/xlvask/services/usage/orders/{id}/ignore', function () { global $db, $response; $this->requirePermission('ignore_xlvask_usage_order'); $user = (new authentication())->get_user(); if (!$user) { $response->error('Invalid session', 400); } $id = (int)($this->fromRoute('id') ?? 0); if ($id < 1) { $response->error('Invalid XL Vask usage log id', 400); } self::requireUsageLogInHallScope($id, self::allowedHallIdsForUser($user)); $reason = $this->isParametersSet(['reason']) ? mb_substr(trim((string)$this->getParameter('reason')), 0, 1000) : null; $reasonSql = $reason === null || $reason === '' ? 'NULL' : "'" . $db->escape_string($reason) . "'"; (new xlvask_usage_logs_o())->structure(); $db->query( "UPDATE xlvask_usage_logs SET ignored_at = NOW(), ignored_by = " . (int)$user->id . ", ignored_reason = {$reasonSql}, resolution_state = 'ignored', certainty = 'none', planned_action = 'none', expected_version = expected_version + 1 WHERE id = {$id}" ); $response->success([ 'id' => $id, 'ignored' => true, ]); }, [ 'ignore_xlvask_usage_order' => 'Ignore an XL Vask usage log for invoice period flagging', ] ); $this->post('/modules/xlvask/services/usage/orders/automation/run', function () { global $response; $this->requirePermission('manage_xlvask_usage_automation'); $user = (new authentication())->get_user(); if (!$user) { $response->error('Invalid session', 400); } $ids = $this->isParametersSet(['ids']) ? $this->getParameter('ids') : []; if (!is_array($ids)) { $ids = []; } $dateFrom = $this->isParametersSet(['dateFrom']) ? (string)$this->getParameter('dateFrom') : null; $dateTo = $this->isParametersSet(['dateTo']) ? (string)$this->getParameter('dateTo') : null; $limit = $this->isParametersSet(['limit']) ? (int)$this->getParameter('limit') : 100; $allowedHallIds = self::allowedHallIdsForUser($user); if ($allowedHallIds === []) { $response->error('No XL Vask hall scope is available', 403); } $response->success([ 'run' => (new xlvask_autopilot_service())->createRun([ 'mode' => 'execute', 'dateFrom' => $dateFrom, 'dateTo' => $dateTo, 'ids' => $ids, 'limit' => $limit, ], (int)$user->id, $allowedHallIds), ], 202); }, [ 'manage_xlvask_usage_automation' => 'Evaluate and execute XL Vask usage-log automation', ] ); $this->post('/modules/xlvask/services/usage/orders/{id}/automation/evaluate', function () { global $response; $this->requirePermission('manage_xlvask_usage_automation'); $user = (new authentication())->get_user(); if (!$user) { $response->error('Invalid session', 400); } $id = (int)($this->fromRoute('id') ?? 0); if ($id < 1) { $response->error('Invalid XL Vask usage log id', 400); } self::requireUsageLogInHallScope($id, self::allowedHallIdsForUser($user)); $response->success([ 'run' => (new xlvask_autopilot_service())->createRun([ 'mode' => 'dry_run', 'ids' => [$id], 'limit' => 1, ], (int)$user->id, self::allowedHallIdsForUser($user)), ], 202); }, [ 'manage_xlvask_usage_automation' => 'Evaluate XL Vask usage-log automation', ] ); $this->post('/modules/xlvask/services/usage/orders/{id}/automation/accept', function () { global $response; $this->requirePermission('manage_xlvask_usage_automation'); $user = (new authentication())->get_user(); if (!$user) { $response->error('Invalid session', 400); } $id = (int)($this->fromRoute('id') ?? 0); if ($id < 1) { $response->error('Invalid XL Vask usage log id', 400); } self::requireUsageLogInHallScope($id, self::allowedHallIdsForUser($user)); $response->error('Use the server-generated automation decision preview and apply endpoints.', 409); }, [ 'manage_xlvask_usage_automation' => 'Accept an XL Vask usage-log automation suggestion', ] ); $this->post('/modules/xlvask/services/usage/orders/{id}/automation/deny', function () { global $response; $this->requirePermission('manage_xlvask_usage_automation'); $user = (new authentication())->get_user(); if (!$user) { $response->error('Invalid session', 400); } $id = (int)($this->fromRoute('id') ?? 0); if ($id < 1) { $response->error('Invalid XL Vask usage log id', 400); } self::requireUsageLogInHallScope($id, self::allowedHallIdsForUser($user)); $response->error('Use the server-generated automation decision preview and apply endpoints.', 409); }, [ 'manage_xlvask_usage_automation' => 'Deny an XL Vask usage-log automation suggestion', ] ); $this->get('/modules/xlvask/services/usage/orders/fast-link', function () { global $response; $this->requirePermission('list_xlvask_usage_orders_own'); $user = (new authentication())->get_user(); if (!$user) { $response->error('Invalid session', 400); } self::requireParameters([ 'fast_link_key', // Example: 'temporary_cache_6878cf0603d77' ]); // Get the fast link key from the request $fast_link_key = (string)self::getParameter('fast_link_key'); self::requireType($fast_link_key, self::type_string()); self::requireMinLength('fast_link_key', 20); // Minimum length of the fast link key self::requireMaxLength('fast_link_key', 50); // Maximum length of the fast link key // Check if the fast link key is valid // First, check if the key has the correct format if (preg_match('/^temporary_cache_[a-z0-9]{12,32}$/', $fast_link_key)) { // Get the cached data from Redis $cached_data = redis->get($fast_link_key); // Check if the cached data is valid if ($cached_data) { // Decode the cached data $data = json_decode($cached_data, true); // Check if the data is valid if (is_array($data)) { $allowedHallIds = self::allowedHallIdsForUser($user); if (!in_array(trim((string)($data['HallId'] ?? '')), $allowedHallIds, true)) { $response->error('Fast link is outside the current XL Vask hall scope', 403); } // Delete the cached data from Redis redis->delete($fast_link_key); // Return the data $xlvask = new xlvask(); $order_arr = (new orders_o())->simulateOrderFromXLVask($xlvask->new($xlvask->helpers->xlvask_usage_log)->setProperties($data), true); // ['order' => $order_arr, 'order_items' => $items_arr] $tmp_order_obj = (object)[]; $tmp_order_arr = $order_arr['order'] ?? []; /** * "id": -1, * "customer_id": 39159000, * "cashier_id": 2285, * "reference": "Simulated Order from XL Vask", * "notes": "This is a simulated order generated from an XL Vask usage log", * "department_id": 1, * "reg_1": "DE55248", * "reg_2": "", * "reg_3": "", * "completed_at": null, * "created_at": "2025-07-16 13:04:54", * "deleted_at": null, * "total_net_amount": 683, * "invoice_collection_id": 0, * "booking_id": 0, * "wash_id": "b24728ea-e22b-4dce-8cd3-a0998f7fdc5e", * "lane": 1, * "closed_at": null */ $tmp_order_obj->customer_id = $tmp_order_arr['customer_id'] ?? 0; $tmp_order_obj->department_id = $tmp_order_arr['department_id'] ?? 0; $tmp_order_obj->reg_1 = $tmp_order_arr['reg_1'] ?? ''; $tmp_order_obj->reg_2 = $tmp_order_arr['reg_2'] ?? ''; $tmp_order_obj->reg_3 = $tmp_order_arr['reg_3'] ?? ''; $tmp_order_obj->created_at = $tmp_order_arr['created_at'] ?? ''; $tmp_order_obj->wash_id = $tmp_order_arr['wash_id'] ?? ''; $tmp_order_obj->lane = $tmp_order_arr['lane'] ?? 0; $response->success([ ...$order_arr, 'potential_duplicates' => (new orders_o())->getOrderPotentialDuplicatesIfFound( $tmp_order_obj->reg_1, $tmp_order_obj->reg_2, $tmp_order_obj->reg_3, $tmp_order_obj->department_id, $tmp_order_obj->created_at, ), ]); } else { // Return an error if the data is not valid $response->error('Invalid cached data', 400); } } else { // Return an error if the fast link key does not exist in Redis $response->error('Fast link key not found', 404); } } else { // Return an error if the fast link key is invalid $response->error('Invalid fast link key format', 400); } }, [ 'fast_link_key' => 'string', // Example: 'temporary_cache_6878cf0603d77' ] ); } private static function allowedHallIdsForUser(object $user): array { return array_values(array_unique(array_filter( array_map(static fn(mixed $id): string => trim((string)$id), (array)$user->getGroup()->getDepartmentsScannersHallIds()), static fn(string $id): bool => $id !== '' && strlen($id) <= 191 ))); } private static function requireUsageLogInHallScope(int $usageLogId, array $allowedHallIds): void { global $db, $response; if ($allowedHallIds === []) { $response->error('XL Vask usage log not found', 404); } $hallSql = implode(',', array_map( static fn(string $hallId): string => "'" . $db->escape_string($hallId) . "'", $allowedHallIds )); $result = $db->query( "SELECT id FROM xlvask_usage_logs WHERE id = {$usageLogId} AND HallId IN ({$hallSql}) LIMIT 1" ); if ($result === false || $result->num_rows < 1) { $response->error('XL Vask usage log not found', 404); } } }