From ecd44a455a48f886c1e1a7aca57d56bcdf3ec1c3 Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Wed, 26 Nov 2025 10:01:24 +0100 Subject: [PATCH] - Add Redis `mget` method and cache management improvements - Update cache expiration times for `economicCustomerName` and `isBooked` objects - Introduce `getCachedForMultipleObjects` for batch cache retrieval - Optimize `isBooked` with optional caching and update to store results - Implement `getCustomerNames` in `users_o` with caching for bulk name retrieval - Refactor customer transaction handling in `InvoicingPeriodRoute` for efficiency - Filter orders excluded from invoicing in `collected_order_invoices_o` --- services/nginx/app/classes/redis.php | 6 + .../objects/collected_order_invoices_o.php | 7 ++ services/nginx/app/objects/orders_o.php | 17 ++- services/nginx/app/objects/users_o.php | 61 ++++++++++ .../nginx/app/routes/InvoicingPeriodRoute.php | 113 ++++++++++-------- services/nginx/app/traits/db_object_t.php | 18 ++- 6 files changed, 168 insertions(+), 54 deletions(-) diff --git a/services/nginx/app/classes/redis.php b/services/nginx/app/classes/redis.php index c8032a70..a8c0f023 100644 --- a/services/nginx/app/classes/redis.php +++ b/services/nginx/app/classes/redis.php @@ -392,4 +392,10 @@ class redis implements redis_i return 'temporary_cache_' . uniqid(); } + public function mget(array $array_map): array + { + // Get multiple keys from Redis + return $this->redis->mget($array_map); + } + } \ No newline at end of file diff --git a/services/nginx/app/objects/collected_order_invoices_o.php b/services/nginx/app/objects/collected_order_invoices_o.php index 95ef8272..d8bbf29c 100644 --- a/services/nginx/app/objects/collected_order_invoices_o.php +++ b/services/nginx/app/objects/collected_order_invoices_o.php @@ -918,6 +918,13 @@ class collected_order_invoices_o extends db self::removeVehicleSubscriptionsTransactions(); // Get the orders in the invoice collection $orders = self::getOrders(); + // Remove any orders exempted from invoicing + $orders = array_filter($orders, function ($order) { + $order_object = new orders_o(); + $order_object->select((int)$order['id']); + $order_object->requireSelected(); + return $order_object->isIncludedInInvoicing(); + }); // Get the customers vehicle subscriptions $vehicles_o = new customer_vehicles_o(); $vehicle_ids = $vehicles_o->getFieldsWhere( diff --git a/services/nginx/app/objects/orders_o.php b/services/nginx/app/objects/orders_o.php index 72f9cf7a..eaa44ee4 100644 --- a/services/nginx/app/objects/orders_o.php +++ b/services/nginx/app/objects/orders_o.php @@ -759,16 +759,29 @@ class orders_o extends db /** * @throws Exception */ - public function isBooked(): bool + public function isBooked(bool $useCache = false): bool { $this->requireSelected(); + // Check if we should use the cached value + if ($useCache) { + $cached = self::getCached('isBooked', $this->id); + if ($cached !== null) { + return (bool)$cached; + } + } // Check the invoice collection has been booked if ((int)$this->invoice_collection_id->value() > 0) { $invoice_collection = (new collected_order_invoices_o())->select((int)$this->invoice_collection_id->value()); self::requireSelected(); - return $invoice_collection->isBooked(); + $isBooked = $invoice_collection->isBooked(); + // Cache the result + self::cache('isBooked', $isBooked, $this->id); + self::setCachedExpiration('isBooked', self::$isBookedCacheExpiration, $this->id); + return $isBooked; } else { // If there is no invoice collection, the order is not booked + self::cache('isBooked', false, $this->id); + self::setCachedExpiration('isBooked', self::$isBookedCacheExpiration, $this->id); return false; } } diff --git a/services/nginx/app/objects/users_o.php b/services/nginx/app/objects/users_o.php index f8087127..65636b16 100644 --- a/services/nginx/app/objects/users_o.php +++ b/services/nginx/app/objects/users_o.php @@ -5,6 +5,7 @@ namespace objects; use classes\db; use classes\language_packs; use classes\object_property; +use classes\redis; use classes\response; use classes\xlvask; use customers\economic_customer_mo; @@ -42,6 +43,7 @@ class users_o extends db public object_property $wash_certificate_email; // Optional protected array $wash_subscription_transactions; + public function structure(): void { $this->setTable('users'); @@ -401,6 +403,9 @@ class users_o extends db return $array; } + /** + * @throws Exception + */ public function getCustomerName(int $customer_number): string|null { // Get the customer from the customer object @@ -1400,4 +1405,60 @@ class users_o extends db $this->objectChanged(); } + /** + * @param int[] $customer_numbers + * @return array Map of customer number to customer name + */ + public function getCustomerNames(array $customer_numbers): array + { + global $db; + $customer_numbers = array_map('intval', $customer_numbers); + // Look in the cache first + $customer_numbers_to_fetch = []; + $customer_names_cached = self::getCachedForMultipleObjects('economic_customer_name', $customer_numbers); + // Loop through the customer numbers and check if they are cached + $customer_names = array_map(function ($cached_name) { + return $cached_name ? json_decode($cached_name)->name : null; + }, array_values($customer_names_cached)); + // Set the names for the cached customer numbers [ "customer_number" => "customer_name" ] + $customer_names = array_combine( + array_map('strval', $customer_numbers), + $customer_names + ); + // Find the customer numbers that are not cached + foreach ( $customer_names as $customer_number => $customer_name ) { + if ($customer_name === null) { + $customer_numbers_to_fetch[] = (int)$customer_number; + } + } + // Fetch the remaining customer names from E-conomic + if (count($customer_numbers_to_fetch) > 0) { + foreach ( $customer_numbers_to_fetch as $customer_number ) { + // Get the customer name from the external source + try { + // Try to get the economic customer data cached in the user + $tmp_user = new users_o(); + $tmp_user->getUserByCustomerNumber($customer_number); + $cached_name = $tmp_user->getCached('economic_customer'); + // If not cached, fetch from E-conomic + if (!$cached_name) { + $tmp_user->getCustomerEcocomicData($customer_number); + $cached_name = $tmp_user->getCached('economic_customer'); + } + if ($cached_name) { + $customer_names[(string)$customer_number] = $cached_name->name; + } + // Cache the name + $this->cache('economic_customer_name', $cached_name, $customer_number); + $this->setCachedExpiration('economic_customer_name', self::$economicCustomerNameCacheExpiration, $customer_number); + } catch ( Exception $e ) { + // Ignore exceptions + $customer_names[(string)$customer_number] = 'Unable to fetch name'; + } + } + } + // Return the customer names + return $customer_names; + } + } \ No newline at end of file diff --git a/services/nginx/app/routes/InvoicingPeriodRoute.php b/services/nginx/app/routes/InvoicingPeriodRoute.php index c3d8ef32..a9e79091 100644 --- a/services/nginx/app/routes/InvoicingPeriodRoute.php +++ b/services/nginx/app/routes/InvoicingPeriodRoute.php @@ -640,21 +640,24 @@ class InvoicingPeriodRoute */ $customer_numbers = []; $tmp = []; - foreach ( $customers as $customer ) { - $customer_number = (int)$customer->customer_number->value(); - if (empty($customer_number)) { - // Skip if the customer number is empty - continue; + self::debugGetTime(function () use ($customers, &$customer_numbers) { + // Process the customer numbers to ensure they are unique + foreach ( $customers as $customer ) { + $customer_number = (int)$customer->customer_number->value(); + if (empty($customer_number)) { + // Skip if the customer number is empty + continue; + } + // Check if the customer number is already in the array + // This ensures that we only process each customer number once + // We use (int)$customer_number to ensure that the customer number is an integer + if (isset($customer_numbers[$customer_number])) { + continue; + } + // Add the customer number to the array + $customer_numbers[$customer_number] = $customer->id; } - // Check if the customer number is already in the array - // This ensures that we only process each customer number once - // We use (int)$customer_number to ensure that the customer number is an integer - if (isset($customer_numbers[$customer_number])) { - continue; - } - // Add the customer number to the array - $customer_numbers[$customer_number] = $customer->id; - } + }, 'process_customer_numbers'); // Get the transactions for the customers in the specified date range /** * @example @@ -666,48 +669,58 @@ class InvoicingPeriodRoute * ] * @var $customer_number_transactions */ - $customer_number_transactions = (new orders_o())->getTransactionsForCustomersInDateRange(array_keys($customer_numbers), $dateFrom, $dateTo); + self::debugGetTime(function () use ($customer_numbers, $dateFrom, $dateTo, &$customer_number_transactions) { + $customer_number_transactions = (new orders_o())->getTransactionsForCustomersInDateRange(array_keys($customer_numbers), $dateFrom, $dateTo); + }, 'get_transactions_for_customers_in_date_range'); // Calculate the total amount for each transaction, to minimize the number of queries - $transaction_ids = []; - foreach ( $customer_number_transactions as $customer_number => $transactions ) { - foreach ( $transactions as $transaction ) { - if ($transaction instanceof orders_o) { - $transaction_ids[] = $transaction->id; + self::debugGetTime(function () use ($customer_number_transactions) { + // Get all transaction ids + $transaction_ids = []; + foreach ( $customer_number_transactions as $customer_number => $transactions ) { + foreach ( $transactions as $transaction ) { + if ($transaction instanceof orders_o) { + $transaction_ids[] = $transaction->id; + } } } - } - // Get the total amount for each transaction - $transaction_totals = (new orders_o())->getNetAmountForOrders($transaction_ids); - // Add the total amount to each transaction - foreach ( $customer_number_transactions as $customer_number => $transactions ) { - foreach ( $transactions as $transaction ) { - if ($transaction instanceof orders_o) { - // Set the total amount for the transaction - $transaction->setTemporaryNetAmount($transaction_totals[$transaction->id] ?? 0); + // Get the total amount for each transaction + $transaction_totals = (new orders_o())->getNetAmountForOrders($transaction_ids); + // Add the total amount to each transaction + foreach ( $customer_number_transactions as $customer_number => $transactions ) { + foreach ( $transactions as $transaction ) { + if ($transaction instanceof orders_o) { + // Set the total amount for the transaction + $transaction->setTemporaryNetAmount($transaction_totals[$transaction->id] ?? 0); + } } } - } + }, 'calculate_transaction_totals'); + // Get the customer names from the cache + $customer_names = (new \objects\users_o())->getCustomerNames(array_keys($customer_numbers)); // Process the customer numbers to ensure they are unique - foreach ( $customer_numbers as $customer_number => $user_id ) { - if (empty($customer_number)) { - // Skip if the customer number is empty - continue; + self::debugGetTime(function () use ($customer_numbers, $customer_number_transactions, &$tmp, $customer_names) { + foreach ( $customer_numbers as $customer_number => $user_id ) { + if (empty($customer_number)) { + // Skip if the customer number is empty + continue; + } + // Check if the customer number is already in the array + if (isset($tmp[$customer_number])) { + // If the customer number is already in the array, skip it + continue; + } + // If the customer number is not in the array, add it + // Construct the customer object with transactions + $tmp[] = self::constructCustomerObject( + (int)$customer_number, + $customer_names[(int)$customer_number] ?? 'Unknown Customer', + //(new \objects\users_o())->getCustomerName((int)$customer_number), + $customer_number_transactions[(int)$customer_number] ?? [], + false, + (int)$user_id, + ); } - // Check if the customer number is already in the array - if (isset($tmp[$customer_number])) { - // If the customer number is already in the array, skip it - continue; - } - // If the customer number is not in the array, add it - // Construct the customer object with transactions - $tmp[] = self::constructCustomerObject( - (int)$customer_number, - (new \objects\users_o())->getCustomerName((int)$customer_number), - $customer_number_transactions[(int)$customer_number] ?? [], - false, - (int)$user_id, - ); - } + }, 'construct_customer_objects'); return $tmp; } @@ -749,7 +762,7 @@ class InvoicingPeriodRoute 'id' => $transaction->id, 'date' => $transaction->created_at->value(), 'amount' => $transaction->temporary_net_amount, // Use the temporary net amount set earlier - 'booked' => $transaction->isBooked(), + 'booked' => $transaction->isBooked(true), 'excluded' => !$transaction->isIncludedInInvoicing() ]; } diff --git a/services/nginx/app/traits/db_object_t.php b/services/nginx/app/traits/db_object_t.php index 0d077371..4e43e193 100644 --- a/services/nginx/app/traits/db_object_t.php +++ b/services/nginx/app/traits/db_object_t.php @@ -49,8 +49,9 @@ use objects\users_o; trait db_object_t { + public static int $economicCustomerNameCacheExpiration = 86400; // The economic customer name cache expiration time, in seconds. Default is 1 day (86400 seconds). public static int $asArrayCacheExpiration = 600; // The id of the object in the database - public static int $isBookedCacheExpiration = 60; // The booked / not booked cache expiration time, in seconds. Default is 1 minute (60 seconds). + public static int $isBookedCacheExpiration = 300; // The booked / not booked cache expiration time, in seconds. Default is 1 minute (60 seconds). public int $id; // The table of the objects in the database (e.g. users) 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 @@ -914,6 +915,19 @@ trait db_object_t redis->set($this->table . '_' . $objectId . '_' . $key, $data); } + /** + * Get cached values for multiple objects + * @param string $key The key to get the cached object + * @param array $objectIds The object ids to get the cached objects for + * @return array The cached object keys + */ + public function getCachedForMultipleObjects(string $key, array $objectIds): array + { + return redis->mget(array_map(function($objectId) use ($key) { + return $this->table . '_' . $objectId . '_' . $key; + }, $objectIds)); + } + /** * Set cached object expiration time * @param string $key The key to set the cached object expiration time @@ -939,9 +953,9 @@ trait db_object_t */ public function getCachedKey(string $key, $objectId = null): string { - self::requireSelected(); // If the object id is not set, use the object id if (!$objectId) { + self::requireSelected(); $objectId = $this->id; } // Return the cached data key