From af3b0666fb479eefbf9d859d3edcc1f41c12f0ea Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Mon, 21 Jul 2025 10:32:56 +0200 Subject: [PATCH] Add import tasks and new parsers to XL Vask module - Introduced `runImportTasks` method for vehicle, user, and usage log imports. - Added new product parsers: `ht_b_rstel_s`, `ht_turbo`, and `ikke_b_rster_p_kabinen`. - Implemented simulation and duplicate detection for usage logs via order objects. - Updated usage log handling to include default customer checks and user association. - Enhanced request handling with fast-link functionality for usage orders. --- services/nginx/app/classes/db.php | 17 ++ .../nginx/app/classes/object_property.php | 15 ++ services/nginx/app/classes/redis.php | 6 + .../helpers/xlvask_parser_ht_b_rstel_s.php | 26 +++ .../xlvask/helpers/xlvask_parser_ht_turbo.php | 26 +++ .../xlvask_parser_ikke_b_rster_p_kabinen.php | 26 +++ .../helpers/xlvask_parser_skylning_med_ro.php | 2 +- .../modules/xlvask/helpers/xlvask_tasks.php | 29 +++ .../xlvask/helpers/xlvask_usage_log.php | 34 ++- .../xlvask/helpers/xlvask_wash_item.php | 10 +- .../app/modules/xlvask/xlvask_helpers.php | 4 +- services/nginx/app/objects/groups_o.php | 31 +++ services/nginx/app/objects/orders_o.php | 142 +++++++++++- .../nginx/app/routes/xlvaskUsageLogsRoute.php | 207 ++++++++++++++++++ services/nginx/app/traits/db_object_t.php | 43 +++- 15 files changed, 603 insertions(+), 15 deletions(-) create mode 100644 services/nginx/app/modules/xlvask/helpers/xlvask_parser_ht_b_rstel_s.php create mode 100644 services/nginx/app/modules/xlvask/helpers/xlvask_parser_ht_turbo.php create mode 100644 services/nginx/app/modules/xlvask/helpers/xlvask_parser_ikke_b_rster_p_kabinen.php create mode 100644 services/nginx/app/routes/xlvaskUsageLogsRoute.php diff --git a/services/nginx/app/classes/db.php b/services/nginx/app/classes/db.php index 2c845f14..bbe45c1f 100644 --- a/services/nginx/app/classes/db.php +++ b/services/nginx/app/classes/db.php @@ -4,6 +4,7 @@ namespace classes; use Exception; use mysqli; +use objects\orders_o; class db { @@ -141,5 +142,21 @@ class db return $this->fetch_all($result); } + /** + * Generate a fake ID for a table. + * Fake ids are not stored in the database, they are generated on every request starting from -1. + * This is useful for testing purposes. + * @see orders_o::simulateOrderFromXLVask() + * @param string $table + * @return int + */ + public function getNextFakeId(string $table): int + { + // This is a static variable that will be shared across all instances of the class + static $fakeId = 0; // Start from 0 and decrement on each call + $fakeId--; + return $fakeId; + } + } \ No newline at end of file diff --git a/services/nginx/app/classes/object_property.php b/services/nginx/app/classes/object_property.php index 2f02d822..33325dfe 100644 --- a/services/nginx/app/classes/object_property.php +++ b/services/nginx/app/classes/object_property.php @@ -10,6 +10,7 @@ class object_property private string $type; // The data type of the field in the database table (e.g. int, varchar, text) private bool $required; // Whether the field is required or not private mixed $default; // The default value of the field + private mixed $fake_value; // The fake value of the field, used for testing purposes (When the object id is -1) public function __construct(string $table, int $id, string $column, string $type, bool $required = false, mixed $default = null) { @@ -33,6 +34,10 @@ class object_property */ public function value(): mixed { + if ($this->id < 0) { + // If the object id is less than 0, return the fake value + return $this->fake_value ?? $this->default; + } // Get the value of the field in the database table global $db; // Sanitize the id to prevent SQL injection @@ -64,6 +69,11 @@ class object_property */ public function set(mixed $value): void { + if ($this->id < 0) { + // If the object id is less than 0, set the fake value + $this->fake_value = $value; + return; + } // Set the value of the field in the database table global /** @var db $db */ $db; @@ -87,6 +97,11 @@ class object_property */ public function nullify(): void { + if ($this->id < 0) { + // If the object id is less than 0, set the fake value to null + $this->fake_value = null; + return; + } // Set the value of the field in the database table to null global /** @var db $db */ $db; diff --git a/services/nginx/app/classes/redis.php b/services/nginx/app/classes/redis.php index e1136081..c8032a70 100644 --- a/services/nginx/app/classes/redis.php +++ b/services/nginx/app/classes/redis.php @@ -386,4 +386,10 @@ class redis implements redis_i return $this; } + public function generateTemporaryCacheKey(): string + { + // Generate a temporary cache key + return 'temporary_cache_' . uniqid(); + } + } \ No newline at end of file diff --git a/services/nginx/app/modules/xlvask/helpers/xlvask_parser_ht_b_rstel_s.php b/services/nginx/app/modules/xlvask/helpers/xlvask_parser_ht_b_rstel_s.php new file mode 100644 index 00000000..4402c14e --- /dev/null +++ b/services/nginx/app/modules/xlvask/helpers/xlvask_parser_ht_b_rstel_s.php @@ -0,0 +1,26 @@ +setup('ht_b_rstel_s'); + } + + + /** + * @inheritDoc + * @throws Exception + */ + protected function parse(xlvask_wash_item $wash_item, xlvask_usage_log $xlvask_usage_log): products_o + { + return (new products_o())->select(64); // Irrelevant product ID + } +} \ No newline at end of file diff --git a/services/nginx/app/modules/xlvask/helpers/xlvask_parser_ht_turbo.php b/services/nginx/app/modules/xlvask/helpers/xlvask_parser_ht_turbo.php new file mode 100644 index 00000000..37d0c567 --- /dev/null +++ b/services/nginx/app/modules/xlvask/helpers/xlvask_parser_ht_turbo.php @@ -0,0 +1,26 @@ +setup('ht_turbo'); + } + + + /** + * @inheritDoc + * @throws Exception + */ + protected function parse(xlvask_wash_item $wash_item, xlvask_usage_log $xlvask_usage_log): products_o + { + return (new products_o())->select(64); // Irrelevant product ID + } +} \ No newline at end of file diff --git a/services/nginx/app/modules/xlvask/helpers/xlvask_parser_ikke_b_rster_p_kabinen.php b/services/nginx/app/modules/xlvask/helpers/xlvask_parser_ikke_b_rster_p_kabinen.php new file mode 100644 index 00000000..dd6560ca --- /dev/null +++ b/services/nginx/app/modules/xlvask/helpers/xlvask_parser_ikke_b_rster_p_kabinen.php @@ -0,0 +1,26 @@ +setup('ikke_b_rster_p_kabinen'); + } + + + /** + * @inheritDoc + * @throws Exception + */ + protected function parse(xlvask_wash_item $wash_item, xlvask_usage_log $xlvask_usage_log): products_o + { + return (new products_o())->select(64); // Irrelevant product ID + } +} \ No newline at end of file diff --git a/services/nginx/app/modules/xlvask/helpers/xlvask_parser_skylning_med_ro.php b/services/nginx/app/modules/xlvask/helpers/xlvask_parser_skylning_med_ro.php index 3a3ffac3..c81966b6 100644 --- a/services/nginx/app/modules/xlvask/helpers/xlvask_parser_skylning_med_ro.php +++ b/services/nginx/app/modules/xlvask/helpers/xlvask_parser_skylning_med_ro.php @@ -39,6 +39,6 @@ class xlvask_parser_skylning_med_ro extends xlvask_product_parser return (new products_o())->select(24); // Spot Free- Lastbil product ID } // Throw an exception if the vehicle type is not supported - throw new Exception('Unsupported vehicle type for Skylning med RO product.'); + throw new Exception('Unsupported vehicle type for Skylning med RO product. Vehicle type ID: ' . $primary_product_id . '.'); } } \ No newline at end of file diff --git a/services/nginx/app/modules/xlvask/helpers/xlvask_tasks.php b/services/nginx/app/modules/xlvask/helpers/xlvask_tasks.php index aa2b1b46..2f1d2828 100644 --- a/services/nginx/app/modules/xlvask/helpers/xlvask_tasks.php +++ b/services/nginx/app/modules/xlvask/helpers/xlvask_tasks.php @@ -5,7 +5,10 @@ namespace helpers; use Exception; use objects\orders_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 { @@ -34,6 +37,7 @@ class xlvask_tasks // Run the synchronization tasks if ($xlvask->config->synchronization_enabled->isTrue()) { // Synchronize users, this will keep the users (XL Vask customer objects) in cache up to date + $this->runImportTasks(); $this->runSyncUsers(true); $this->runSyncUsage(); $this->runSyncVehicles(); @@ -41,6 +45,31 @@ class xlvask_tasks }; } + /** + * Run the import tasks + * This task is used to import the XL Vask vehicles, users, usage logs, and other necessary data into the system. + * It will import the necessary classes and objects to be used in the system. + * @return void + * @throws Exception If the task fails + */ + public function runImportTasks(): void + { + // Define the XL Vask object + $xlvask = new \classes\xlvask(); + // Require the module to be enabled + $xlvask->requireModuleEnabled(); + // Run the synchronization tasks + if ($xlvask->config->synchronization_enabled->isTrue()) { + // Synchronize users, this will keep the database tables updated: + // - xlvask_customers + // - xlvask_vehicles + // - xlvask_usage_logs + (new xlvask_customers_o())->importCustomers(); + (new xlvask_vehicles_o())->importVehicles(); + (new xlvask_usage_logs_o())->importUsageLogs(); + }; + } + /** * Run the synchronize users task * This task fetches the users from XL Vask, and synchronizes them with this system. diff --git a/services/nginx/app/modules/xlvask/helpers/xlvask_usage_log.php b/services/nginx/app/modules/xlvask/helpers/xlvask_usage_log.php index 8d314623..156ff1b3 100644 --- a/services/nginx/app/modules/xlvask/helpers/xlvask_usage_log.php +++ b/services/nginx/app/modules/xlvask/helpers/xlvask_usage_log.php @@ -6,6 +6,7 @@ use classes\xlvask; use Exception; use objects\departments_o; use objects\orders_o; +use objects\users_o; use objects\xlvask_potential_order_matches_o; class xlvask_usage_log extends xlvask_helper @@ -33,6 +34,11 @@ class xlvask_usage_log extends xlvask_helper * [VehicleId] => e6679209-e52f-4376-9660-d56cb46f523b * [WashItems] => Array[washItem] => Array */ + public static array $default_customers = [ + 'CTW', + 'Personalebiler Truckwash ', // The space at the end is intentional + 'XL Gønge Transport ApS', // This is a test customer + ]; /** * The wash ID of the usage log entry @@ -283,6 +289,27 @@ class xlvask_usage_log extends xlvask_helper return $this; } + /** + * Get the user associated with this usage log. + * This method retrieves the user object based on the CustomerId (externId). + * If the CustomerId is not set or the user cannot be found, it returns null. + * @return users_o|null + */ + public function getUser(): ?users_o + { + if (empty($this->CustomerId)) { + return null; + } + try { + $user = new users_o(); + $user->getUserByCustomerNumber((int)$this->CustomerId); + return $user; + } catch (Exception $e) { + // Handle the exception if needed + return null; + } + } + /** * Generate wash items from the provided array. * This method takes an array of wash items and converts them into instances of xlvask_wash_item. @@ -562,12 +589,7 @@ class xlvask_usage_log extends xlvask_helper public function hasDefaultCustomer(): bool { // Check if the customer is a default customer (e.g., "Unknown" or similar) - $default_customers = [ - 'CTW', - 'Personalebiler Truckwash ', // The space at the end is intentional - 'XL Gønge Transport ApS', // This is a test customer - ]; - return in_array($this->Customer, $default_customers, true); + return in_array($this->Customer, $this::$default_customers, true); } public function hasExternalId(): bool diff --git a/services/nginx/app/modules/xlvask/helpers/xlvask_wash_item.php b/services/nginx/app/modules/xlvask/helpers/xlvask_wash_item.php index 02503b36..d0073deb 100644 --- a/services/nginx/app/modules/xlvask/helpers/xlvask_wash_item.php +++ b/services/nginx/app/modules/xlvask/helpers/xlvask_wash_item.php @@ -297,7 +297,7 @@ class xlvask_wash_item extends xlvask_helper private function getParserName(): string { // Convert the OriginalProductName to a valid parser name - // Example: "Stor bil" becomes "stor_bil", "2-b\u00f8rstevask" becomes "2_br_stevask" + // Example: "Stor bil" becomes "stor_bil", "2-b\u00f8rstevask" becomes "2_br_stevask", "ingen_bu00f8rster_foran_no_brush_wash_front" => "ingen_b_rster_foran_no_brush_wash_front" // Remove all non-alphanumeric characters and replace spaces with underscores if (empty($this->OriginalProductName)) { $this->slackMessage('Experienced an error parsing a product name: MISSING_ORIGINAL_PRODUCT_NAME ( WashItemId: ' . $this->WashItemId . ' )'); @@ -312,8 +312,12 @@ class xlvask_wash_item extends xlvask_helper $this->slackMessage('Experienced an error parsing a product name: EMPTY_ORIGINAL_PRODUCT_NAME_AFTER_PROCESSING ( WashItemId: ' . $this->WashItemId . ' )'); throw new Exception('The OriginalProductName is empty after processing. Please provide a valid product name.'); } + // Check if there's any "uxxxx" pattern in the string and replace it with the "_" character + $result = preg_replace('/u([0-9a-fA-F]{4})/', '_', $result); // Remove any special characters that might still be present $result = preg_replace('/[^a-z0-9_]/i', '', $result); + // Remove any double underscores + $result = preg_replace('/_+/', '_', $result); return strtolower($result); } @@ -356,6 +360,10 @@ class xlvask_wash_item extends xlvask_helper public function getUnitPriceExVat(): float { // Calculate the unit price excluding VAT + if (!is_numeric($this->Count) || $this->Count <= 0) { + // If Count is not numeric or less than or equal to zero, return 0 + return 0.0; + } return $this->getPriceExVat() / (float)$this->Count; } } \ No newline at end of file diff --git a/services/nginx/app/modules/xlvask/xlvask_helpers.php b/services/nginx/app/modules/xlvask/xlvask_helpers.php index 1cfabb1f..836d2ca6 100644 --- a/services/nginx/app/modules/xlvask/xlvask_helpers.php +++ b/services/nginx/app/modules/xlvask/xlvask_helpers.php @@ -55,7 +55,9 @@ require_once WD . '/modules/xlvask/helpers/xlvask_parser_ikke_b_rster_mellom_bil require_once WD . '/modules/xlvask/helpers/xlvask_parser_s_be_dysebom_tak.php'; require_once WD . '/modules/xlvask/helpers/xlvask_parser_ingen_b_rster_foran_no_brush_wash_front.php'; require_once WD . '/modules/xlvask/helpers/xlvask_parser_ht_dysebom.php'; - +require_once WD . '/modules/xlvask/helpers/xlvask_parser_ht_b_rstel_s.php'; +require_once WD . '/modules/xlvask/helpers/xlvask_parser_ht_turbo.php'; +require_once WD . '/modules/xlvask/helpers/xlvask_parser_ikke_b_rster_p_kabinen.php'; use helpers\xlvask_cache; use helpers\xlvask_create_customer; use helpers\xlvask_customer; diff --git a/services/nginx/app/objects/groups_o.php b/services/nginx/app/objects/groups_o.php index 01424219..57257eda 100644 --- a/services/nginx/app/objects/groups_o.php +++ b/services/nginx/app/objects/groups_o.php @@ -144,4 +144,35 @@ class groups_o extends db (new groups_permissions_o())->add($this->id, $permission_id); return $this; } + + public function getDepartmentsScannerNames(): array + { + self::requireSelected(); + $departments = $this->getDepartments(); + $plate_scanners_o = new plate_scanners_o(); + $tmp = $plate_scanners_o->getFieldsWhere(['department_id' => $departments], ['id', 'name']); + return array_map( + function ($scanner) { + return $scanner['name']; + }, + $tmp + ); + } + + public function getDepartmentsScannersHallIds(): array + { + self::requireSelected(); + $departments = $this->getDepartments(); + $plate_scanners_o = new plate_scanners_o(); + $tmp = $plate_scanners_o->getFieldsWhere([ + 'department_id' => $departments, + 'HallId' => '!null' + ], ['id', 'HallId']); + return array_map( + function ($scanner) { + return (string)$scanner['HallId']; + }, + $tmp + ); + } } \ No newline at end of file diff --git a/services/nginx/app/objects/orders_o.php b/services/nginx/app/objects/orders_o.php index 5acf3da9..c62d8e01 100644 --- a/services/nginx/app/objects/orders_o.php +++ b/services/nginx/app/objects/orders_o.php @@ -493,11 +493,13 @@ class orders_o extends db } /** + * @param bool $cacheResult Whether to cache the result or not, this is used to prevent caching simulated orders + * @param bool $skipCache Whether to skip the cache or not, this is used to force a fresh result + * Convert the order object to an array * @return array The order as an array * @throws Exception - * Convert the order object to an array */ - public function asArray(bool $skipCache = false): array + public function asArray(bool $skipCache = false, bool $cacheResult = true): array { self::requireSelected(); if (!$skipCache) { @@ -520,7 +522,7 @@ class orders_o extends db 'completed_at' => $this->completed_at->value(), 'created_at' => $this->created_at->value(), 'deleted_at' => $this->deleted_at->value(), - 'total_net_amount' => $this->getNetAmount(), + 'total_net_amount' => $this->temporary_net_amount ?: $this->getNetAmount(), 'invoice_collection_id' => (int)$this->invoice_collection_id->value(), 'booking_id' => (int)$this->booking_id->value(), 'wash_id' => $this->wash_id->value(), @@ -528,8 +530,10 @@ class orders_o extends db 'closed_at' => (int)$this->invoice_collection_id->value() ? (new collected_order_invoices_o())->select((int)$this->invoice_collection_id->value())->closed_at->value() : null, ]; // Cache the object - self::cache('asArray', $tmp, $this->id); - self::setCachedExpiration('asArray', self::$asArrayCacheExpiration, $this->id); + if ($cacheResult) { + self::cache('asArray', $tmp, $this->id); + self::setCachedExpiration('asArray', self::$asArrayCacheExpiration, $this->id); + } return $tmp; } @@ -862,6 +866,51 @@ class orders_o extends db return $this; } + /** + * Generate a fake order from a usage log - Used to preview the order before it is created + * @param xlvask_usage_log $xlvask_usage_log The usage log to generate the order from + * @param bool $includeItems Whether to include the order items in the simulated order, together with the total net amount + * @returns array['order' => array, 'order_items' => array] + * @throws Exception + */ + public function simulateOrderFromXLVask(xlvask_usage_log $xlvask_usage_log, bool $includeItems = true): array + { + $this->id = self::generateFakeId(); // Set a negative ID to indicate this is a simulated order + $this->getObjectProperties(); + $this->customer_id->set(($xlvask_usage_log->hasBillableCustomer() ? (int)$xlvask_usage_log->CustomerId : 12345679)); // Set the customer ID from the usage log, or 0 if not billable + $this->cashier_id->set(2285); // Set the cashier ID to a default value for simulation + $this->reference->set('Simulated Order from XL Vask'); + $this->notes->set('This is a simulated order generated from an XL Vask usage log'); + $this->department_id->set($xlvask_usage_log->getDepartment()->id); // Set a default department ID for simulation + $this->reg_1->set((string)$xlvask_usage_log->RegistrationNumber); // Set the registration number from the usage log + $this->reg_2->set(''); + $this->reg_3->set(''); + $this->completed_at->set(null); // Set completed_at to null for simulation + $this->created_at->set($xlvask_usage_log->getTimestampStart()); // Set the created_at to the start time of the usage log + $this->deleted_at->set(null); // Set deleted_at to null for simulation + $this->invoice_collection_id->set(null); // Set invoice_collection_id to null for simulation + $this->booking_id->set(null); // Set booking_id to null for simulation + $this->wash_id->set($xlvask_usage_log->WashId); // Set the wash ID from the usage log + $this->lane->set($xlvask_usage_log->getLane()); // Set lane to null for simulation + if ($includeItems) { + // If we are including items, simulate the order items from the usage log + $order_items = $this->simulateOrderItemsFromXLVask($xlvask_usage_log); + // Save the total net amount for the simulated order (this is the sum of all item prices) + $this->setTemporaryNetAmount( + array_sum(array_map(function ($item) { + return (int)$item['price'] * (int)$item['quantity']; + }, $order_items)) + ); + } else { + $order_items = []; // No items included in the simulated order + $this->setTemporaryNetAmount(0); // Set the temporary net amount to 0 + } + return [ + 'order' => $this->asArray(true, false), // Return the simulated order as an array + 'order_items' => $order_items, // Return the simulated order items + ]; + } + /** * Get a list of customers who have placed orders within a specific date range * @param string $dateFrom (E.g. "2023-01-01 00:00:00") @@ -1048,4 +1097,87 @@ class orders_o extends db // Set a temporary net amount for the order $this->temporary_net_amount = $amount; } + + /** + * @param xlvask_usage_log $xlvask_usage_log + * @return array + * @throws Exception + */ + public function simulateOrderItemsFromXLVask(xlvask_usage_log $xlvask_usage_log): array + { +// Add the products to the simulated order + $firstItemId = null; // Initialize the first item ID to null + $current_item_id = null; + $order_items = []; // Initialize the order items array + /** @var xlvask_wash_item $washItem */ + foreach ( $xlvask_usage_log->WashItems as $washItem ) { + // Check if the item should be included in the order + // Check if the product is with id 64 (irrelevant product, a bi product of the wash) + if ($washItem->getProduct($xlvask_usage_log)->id === 64) { + continue; // Skip items that should not be included in the order + } + if (!$washItem->shouldIncludeInOrder() || $washItem->getProduct($xlvask_usage_log)->id === 64) { + continue; // Skip items that should not be included in the order + } + $order_item = new order_items_o(); + $order_item->id = self::generateFakeId(); // Set a negative ID to indicate this is a simulated order item + // If the id is positive, forcefully quit + if ($order_item->id > 0) { + throw new Exception('The order item ID must be negative for simulation'); + } + $order_item->getObjectProperties(); + $order_item->order_id->set($this->id); // Set the order ID to the simulated order ID + $order_item->product_id->set($washItem->getProduct($xlvask_usage_log)->id); // Set the product ID from the wash item + $order_item->reference->set(''); + $order_item->notes->set(null); // Set notes for the simulated order item + $order_item->price->set((int)$washItem->getUnitPriceExVat() * (int)$washItem->Count); // Set the price based on the wash item + $order_item->quantity->set((int)$washItem->Count); // Set the quantity based on the wash item + $order_item->related_item_id->set($firstItemId === null ? null : $firstItemId); // Set the related item ID to the first item ID (if applicable) + $order_item->include_in_invoice->set(true); // Set include_in_invoice to true for the simulated order item + $order_item->cashier_id->set($this->cashier_id->value()); // Set the cashier ID to the simulated order's cashier ID + // Set the first item ID for the next item to link to (provided this is the first item) + if ($firstItemId === null) { + $firstItemId = $order_item->id; // Set the first item ID to the current item ID + } + $order_items[] = $order_item->getItemAsArray(); // Add the simulated order item to the order items array + // clear up memory + unset($order_item); + } + return $order_items; + } + + public function getOrderPotentialDuplicatesIfFound(string $reg_1, string $reg_2, string $reg_3, int $department_id, string $created_at): array + { + global /** @var db $db */ + $db; + // Validate the registration numbers + $reg_1 = $db->escape_string($reg_1); + $reg_2 = $db->escape_string($reg_2); + $reg_3 = $db->escape_string($reg_3); + // Strip any whitespace from the registration numbers + $reg_1 = trim($reg_1); + $reg_2 = trim($reg_2); + $reg_3 = trim($reg_3); + // Created at must be between: + $created_at_from = date('Y-m-d H:i:s', strtotime($created_at . ' - 24 hours')); + $created_at_to = date('Y-m-d H:i:s', strtotime($created_at . ' + 24 hours')); + // Validate the department ID + if ($department_id <= 0) { + throw new Exception('Invalid department ID provided'); + } + // Select the unique order ids that match the registration numbers and department ID within the 24 hour range + $sql = "SELECT id FROM $this->table WHERE (reg_1 = '$reg_1' AND reg_2 = '$reg_2' AND reg_3 = '$reg_3') AND department_id = $department_id AND created_at BETWEEN '$created_at_from' AND '$created_at_to' AND deleted_at IS NULL"; + // Execute the query + $result = $db->query($sql); + if ($result->num_rows === 0) { + return []; // No potential duplicates found + } + // Fetch all potential duplicates + $result = $db->fetch_all($result); + return array_map(function ($row) { + $order = new orders_o(); + $order->select((int)$row['id']); + return [...$order->asArray(), 'order_items' => $order->getOrderItems($order->id)]; // Include order items in the result + }, $result); // Return the potential duplicates as an array of order arrays + } } \ No newline at end of file diff --git a/services/nginx/app/routes/xlvaskUsageLogsRoute.php b/services/nginx/app/routes/xlvaskUsageLogsRoute.php new file mode 100644 index 00000000..5cb0ae19 --- /dev/null +++ b/services/nginx/app/routes/xlvaskUsageLogsRoute.php @@ -0,0 +1,207 @@ +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) + $response_includes_items_link = true; // Whether to include a cached direct link to fetch the order with items (This is used to avoid memory issues when the items are not needed) + // 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) { + // Log the incident + (new logs_o())->add('xlvask_usage_orders', 'global', 1, $user->id, 'LIST_XLVASK_USAGE_ORDERS', 'User accessed the list of xlvask usage orders'); + $xlvask_usage_logs = new xlvask_usage_logs_o(); + $xlvask = new xlvask(); + $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'); + } + // Set debug php + error_reporting(E_ALL); + ini_set('display_errors', '1'); + // 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_link, $response_includes_items, $xlvask_usage_logs, $user, $xlvask) { + // Remove the 'id' field from the log + $id = (int)$log['id']; + unset($log['id']); + // Convert the 'WashItems' field from JSON to an array + $log['WashItems'] = json_decode($log['WashItems'], true); + // Create a new xlvask usage log object + $tmp = $xlvask->new($xlvask->helpers->xlvask_usage_log); + // Set the properties of the temporary object + $tmp->setProperties($log); + // Define the result structure + $isEligibleForAutomaticContinuance = $tmp->isEligibleForAutomaticContinuance(true); + // Generate a fast link key for the order, used to retrieve the order with items later. + if ($response_includes_items_link && $isEligibleForAutomaticContinuance) { + // Add a link to the order with items + $fast_link_key = redis->generateTemporaryCacheKey(); + redis->set( + $fast_link_key, + json_encode($tmp->toArray(), JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + ); + redis->expire($fast_link_key, 3600); // Set the link to expire in 1 hour + } + // 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 + // Clear memory + unset($tmp); + unset($log); + // Return the result + return [ + 'id' => $id, // Return the ID of the log + 'fast_link_key' => $fast_link_key ?? null, // Return the fast link key if it was generated + ...$tmp_res['order'], // Return the simulated order from XLVask (with or without items) + ]; + }, + $xlvask_usage_logs->forceRestrictFilters( + [ + // This makes sure that the user can only see department logs that belong to their departments + 'HallId' => $user->getGroup()->getDepartmentsScannersHallIds(), + 'FinishStatus' => ['1'], // Only show finished logs + ] + ) + ); + // Return the response + $response->success($result); + } else { + // Log the incident + (new logs_o())->add('xlvask_usage_orders', 'global', 1, 0, 'LIST_XLVASK_USAGE_ORDERS', 'No user found, or invalid session'); + // 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/fast-link', function () { + global $response; + 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)) { + // 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' + ] + ); + } +} \ No newline at end of file diff --git a/services/nginx/app/traits/db_object_t.php b/services/nginx/app/traits/db_object_t.php index 49eecc04..b91ccbe7 100644 --- a/services/nginx/app/traits/db_object_t.php +++ b/services/nginx/app/traits/db_object_t.php @@ -50,6 +50,13 @@ trait db_object_t private string $table; // The fields to search in the database (e.g. ['name', 'email']). If empty, all fields will be searched private array $searchableFields = []; // The where clauses to add to the pagination query private array $whereClauses = []; // The cache expiration time for the asArray function, in seconds. This is used to cache the result of the asArray function to improve performance. Default is 10 minutes (600 seconds). + /** + * Example: + * // Set the CustomerId to be not "NULL". + * setAdditionalWhereClause('AND `CustomerId` IS NOT NULL'); + * @var string + */ + private string $additionalWhereClause = ''; // Additional where clause to add to the pagination query, this is used to add custom where clauses to the pagination query public function __construct() { @@ -88,6 +95,9 @@ trait db_object_t // If the value is null, add a where clause to check if the field is null if ($value === null) { $where[] = "$field IS NULL"; + } elseif (is_string($value) && strtolower($value) === '!null') { + // If the value is "!null", add a where clause to check if the field is not null + $where[] = "$field IS NOT NULL"; } elseif (is_array($value)) { // If the value is an array, add a where clause to check if the field is in the array $in = implode(',', array_map(function ($v) { @@ -191,6 +201,20 @@ trait db_object_t return self::db_object_t__listObjectsWithPaginationIfSet($parseFunction, $forcedFilters, $join); } + /** + * Set the additional where clause to add to the pagination query + * @param string $clause The additional where clause to add to the pagination query + * Example: + * setAdditionalWhereClause('AND `CustomerId` IS NOT NULL'); + * @note This is cleared after each pagination query, so it can be used to add custom where clauses to the pagination query + */ + public function setAdditionalWhereClause(string $clause): self + { + // Add the additional where clause to the pagination query + $this->additionalWhereClause = $clause; + return $this; + } + /** * List objects with pagination (if set) * @note This is separate from listObjectsWithPaginationIfSet, as it is used in the API routes with specific permission checks @@ -432,9 +456,14 @@ trait db_object_t $whereClauses[] = "`deleted_at` IS NULL"; } + + // Add the additional where clause, if set + if (!empty($this->additionalWhereClause)) { + $whereClauses[] = $this->additionalWhereClause; + } + // Combine WHERE clauses $whereQuery = !empty($whereClauses) ? 'WHERE ' . implode(' AND ', $whereClauses) : ''; - // Order clause $orderQuery = ''; if (!empty($order)) { @@ -877,6 +906,18 @@ trait db_object_t self::objectChanged(); } + /** + * generateFakeId - This function generates a fake id for the object + * @see orders_o::simulateOrderFromXLVask() + * @returns int A fake (negative) id for the object + */ + public function generateFakeId(): int + { + global /** @var db $db */ + $db; + return $db->getNextFakeId($this->table); + } + /** * Object changed * This function should be called after an object property is changed