get('/modules/xlvask/services/usage/orders', function () { $permission_list_own = 'list_xlvask_usage_orders_own'; $permission_list_all = 'list_xlvask_usage_orders_all'; $response_includes_items = false; global $response; if (!$this->hasPermission($permission_list_all)) { $this->requirePermission($permission_list_own); } $user = (new authentication())->get_user(); if (!$user) { $response->error('Invalid session', 400); } $allowedHallIds = $this->allowedHallIdsForUser($user); if ($allowedHallIds === []) { $response->error('No XL Vask hall scope is available', 403); } $xlvask_usage_logs = new xlvask_usage_logs_o(); $xlvask = new xlvask(); $linked_order_ids_by_wash_id = []; $xlvask_usage_log_class = $xlvask->helpers->xlvask_usage_log; if (ini_get('memory_limit') < '5120M') { ini_set('memory_limit', '5120M'); } $result = $xlvask_usage_logs ->setAdditionalWhereClause( "`Customer` NOT IN ('" . implode("', '", $xlvask_usage_log_class::$default_customers) . "')" ) ->listObjectsWithPaginationIfSet( function ($log) use ( $response_includes_items, $xlvask_usage_logs, $xlvask, &$linked_order_ids_by_wash_id ) { $id = (int)$log['id']; $amount_summary = $xlvask_usage_logs->getAmountSummaryReadOnly($log); unset($log['id']); $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', ])); $tmp = $xlvask->new($xlvask_usage_log_class); $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; $isEligibleForAutomaticContinuance = $tmp->isEligibleForAutomaticContinuance(true); $tmp_res = $isEligibleForAutomaticContinuance ? (new orders_o())->simulateOrderFromXLVask($tmp, $response_includes_items) : []; $tmp_res['order']['customer_name'] = $tmp->Customer; $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']; unset($tmp); return [ 'id' => $id, 'fast_link_key' => null, 'usage_log_id' => $id, 'linked_order_id' => $linked_order_id, 'ignored_at' => $log['ignored_at'] ?? null, 'ignored_by' => isset($log['ignored_by']) ? (int)$log['ignored_by'] : null, 'ignored_reason' => $log['ignored_reason'] ?? null, ...$tmp_res['order'], ]; }, $xlvask_usage_logs->forceRestrictFilters([ 'HallId' => $allowedHallIds, 'FinishStatus' => ['1'], ]) ); $response->success($result); }, [ '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; if (!$this->hasPermission('list_xlvask_usage_orders_all')) { $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; $allowedHallIds = $this->allowedHallIdsForUser($user); $summary = (new xlvask_usage_logs_o())->summarizeUsageOrdersReadOnly( $dateFrom, $dateTo, $allowedHallIds ); $response->success(['summary' => $summary]); }, [ 'list_xlvask_usage_orders_own' => 'Read XL Vask usage-log summary', 'list_xlvask_usage_orders_all' => 'Read all XL Vask usage-log summaries', ]); $this->patch('/modules/xlvask/services/usage/orders/{id}/ignore', function () { global $response; $this->requirePermission('review_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::requireParameters(['reason']); $reason = trim((string)$this->getParameter('reason')); if (mb_strlen($reason) > 500) { $response->error('Reason is too long (max 500 characters)', 400); } $allowedHallIds = $this->allowedHallIdsForUser($user); self::requireUsageLogInHallScope($id, $allowedHallIds); $log = new xlvask_usage_logs_o(); $log->getById($id); if (!$log->id) { $response->error('XL Vask usage log not found', 404); } $log->ignored_at->set(date('Y-m-d H:i:s')); $log->ignored_by->set((int)$user->id); $log->ignored_reason->set($reason); $log->objectChanged(); $response->success([ 'id' => $id, 'ignored_at' => $log->ignored_at->get(), 'ignored_by' => (int)$log->ignored_by->get(), 'ignored_reason' => $log->ignored_reason->get(), ]); }, [ 'review_xlvask_usage_order' => 'Ignore an XL Vask usage log for invoice period flagging', ]); $this->post('/modules/xlvask/services/usage/orders/{id}/unignore', function () { global $response; $this->requirePermission('review_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); } $allowedHallIds = $this->allowedHallIdsForUser($user); self::requireUsageLogInHallScope($id, $allowedHallIds); $log = new xlvask_usage_logs_o(); $log->getById($id); if (!$log->id) { $response->error('XL Vask usage log not found', 404); } $log->ignored_at->set(null); $log->ignored_by->set(null); $log->ignored_reason->set(null); $log->objectChanged(); $response->success(['id' => $id]); }, [ 'review_xlvask_usage_order' => 'Clear ignore metadata on an XL Vask usage log', ]); $this->post('/modules/xlvask/services/usage/orders/{id}/accept', function () { global $response; $this->requirePermission('review_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); } $allowedHallIds = $this->allowedHallIdsForUser($user); self::requireUsageLogInHallScope($id, $allowedHallIds); $log = new xlvask_usage_logs_o(); $log->getById($id); if (!$log->id) { $response->error('XL Vask usage log not found', 404); } $xlvask = new xlvask(); $log_helper = new ($xlvask->helpers->xlvask_usage_log)(); $log_helper->setProperties(array_intersect_key($log->toArray(), array_flip([ 'WashId', 'CustomerId', 'Customer', 'VatNumber', 'Location', 'Hall', 'HallId', 'StartTime', 'FinishTime', 'RegistrationNumber', 'VehicleType', 'IdentificationType', 'IdentificationId', 'Info', 'Updated', 'Prepaid', 'FinishStatus', 'CustomerGuid', 'VehicleId', 'WashItems', ]))); $customer = $log_helper->getCustomer(); if ($customer === null) { $response->error('XL Vask customer is not linkable', 422); } if (empty($customer->externId)) { $response->error('XL Vask customer has no external id', 422); } $tmp_user = $customer->getUser(); if (!$tmp_user) { $response->error('XL Vask customer is not provisioned in this system', 422); } try { $order = $xlvask->getTasks()->createOrderFromWash($log_helper, $customer); } catch (\Throwable $e) { error_log('[xlvask-accept] createOrderFromWash failed: ' . $e->getMessage()); $response->error('Could not create order from XL Vask usage log: ' . $e->getMessage(), 422); } $log->ignored_at->set(date('Y-m-d H:i:s')); $log->ignored_by->set((int)$user->id); $log->ignored_reason->set('Accepted and converted to order ' . (int)$order->id); $log->objectChanged(); $response->success([ 'order_id' => (int)$order->id, 'usage_log_id' => $id, ]); }, [ 'review_xlvask_usage_order' => 'Convert an XL Vask usage log into an order', ]); $this->post('/modules/xlvask/services/usage/orders/{id}/reject', function () { global $response; $this->requirePermission('review_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::requireParameters(['reason']); $reason = trim((string)$this->getParameter('reason')); if (mb_strlen($reason) > 500) { $response->error('Reason is too long (max 500 characters)', 400); } $allowedHallIds = $this->allowedHallIdsForUser($user); self::requireUsageLogInHallScope($id, $allowedHallIds); $log = new xlvask_usage_logs_o(); $log->getById($id); if (!$log->id) { $response->error('XL Vask usage log not found', 404); } $log->ignored_at->set(date('Y-m-d H:i:s')); $log->ignored_by->set((int)$user->id); $log->ignored_reason->set('Rejected: ' . $reason); $log->objectChanged(); $response->success([ 'id' => $id, 'ignored_at' => $log->ignored_at->get(), 'ignored_by' => (int)$log->ignored_by->get(), 'ignored_reason' => $log->ignored_reason->get(), ]); }, [ 'review_xlvask_usage_order' => 'Reject an XL Vask usage log with a reviewer note', ]); $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']); $fast_link_key = (string)self::getParameter('fast_link_key'); self::requireType($fast_link_key, self::type_string()); self::requireMinLength('fast_link_key', 20); self::requireMaxLength('fast_link_key', 50); if (!preg_match('/^temporary_cache_[a-z0-9]{12,32}$/', $fast_link_key)) { $response->error('Invalid fast link key format', 400); } $cached_data = redis->get($fast_link_key); if (!$cached_data) { $response->error('Fast link key not found', 404); } $data = json_decode($cached_data, true); if (!is_array($data)) { $response->error('Invalid cached data', 400); } $allowedHallIds = $this->allowedHallIdsForUser($user); if (!in_array(trim((string)($data['HallId'] ?? '')), $allowedHallIds, true)) { $response->error('Fast link is outside the current XL Vask hall scope', 403); } redis->delete($fast_link_key); $xlvask = new xlvask(); $order_arr = (new orders_o())->simulateOrderFromXLVask( $xlvask->new($xlvask->helpers->xlvask_usage_log)->setProperties($data), true ); $tmp_order_obj = (object)[]; $tmp_order_arr = $order_arr['order'] ?? []; $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, ), ]); }, [ 'fast_link_key' => 'string', ]); } private function allowedHallIdsForUser(object $user): array { global $db; if ($this->hasPermission('list_xlvask_usage_orders_all')) { $sqlWithSoftDelete = "SELECT DISTINCT HallId FROM plate_scanners WHERE HallId IS NOT NULL AND TRIM(HallId) <> '' AND deleted_at IS NULL"; $sqlWithoutSoftDelete = "SELECT DISTINCT HallId FROM plate_scanners WHERE HallId IS NOT NULL AND TRIM(HallId) <> ''"; try { $result = $db->query($sqlWithSoftDelete); } catch (\Throwable $throwable) { $message = (string)$throwable->getMessage(); $missingDeletedAt = str_contains($message, "Unknown column 'deleted_at'") || str_contains($message, "Unknown column `deleted_at`"); if (!$missingDeletedAt) { throw $throwable; } $result = $db->query($sqlWithoutSoftDelete); } $hallIds = $result === false ? [] : array_map( static fn(array $row): string => trim((string)($row['HallId'] ?? '')), $db->fetch_all($result) ); } else { $hallIds = (array)$user->getGroup()->getDepartmentsScannersHallIds(); } return array_values(array_unique(array_filter( array_map(static fn(mixed $id): string => trim((string)$id), $hallIds), 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); } } }