Add unit tests for Redis namespace safety, MotorAPI cache functionality, and configuration classes, alongside implementation of xlvask_automation_service

- Added tests to ensure Redis namespace safety for `db_object_t` and `users_o`.
- Implemented `MotorApiCachedResultTest` to validate metadata caching behavior.
- Introduced configuration classes for `xlvask_automatic_order_attachment_enabled` and `xlvask_automatic_order_creation_enabled`.
- Developed `xlvask_automation_service` with supporting features for usage log evaluation, suggestion building, and order automation.
This commit is contained in:
Jeppe Bundgaard
2026-05-12 00:22:27 +02:00
parent c1b66a81cc
commit 70080086da
21 changed files with 2326 additions and 57 deletions
@@ -630,7 +630,8 @@ class invoice_period_flag_service
&& !$this->hasAttribute($attributes, $customerNumber, 'restrictTankCleaning')
&& !$this->hasAttribute($attributes, $customerNumber, 'restrictSpotFree')
&& !$this->hasAttribute($attributes, $customerNumber, 'restrictInteriorCleaning')
&& !$this->hasAttribute($attributes, $customerNumber, 'exemptFromAdministrationFee')) {
&& !$this->hasAttribute($attributes, $customerNumber, 'exemptFromAdministrationFee')
&& !$this->hasAttribute($attributes, $customerNumber, 'onlyTankCleaning')) {
continue;
}
@@ -638,6 +639,8 @@ class invoice_period_flag_service
continue;
}
$isTankCleaningProduct = $this->rowIsTankCleaningProduct($row);
if ($this->hasAttribute($attributes, $customerNumber, 'restrictAdditionalServices')
&& (int)($row['related_item_id'] ?? 0) > 0
&& (int)($row['item_price'] ?? 0) > 0) {
@@ -652,8 +655,31 @@ class invoice_period_flag_service
);
}
if ($this->hasAttribute($attributes, $customerNumber, 'restrictTankCleaning') && $isTankCleaningProduct) {
$flags[] = $this->automaticFlag(
'customer_rule_restrict_tank_cleaning',
'order_item',
(int)$row['order_item_id'],
null,
$row,
['product' => $this->productLabel($row)],
$this->orderItemContext($row)
);
}
if ($this->hasAttribute($attributes, $customerNumber, 'onlyTankCleaning') && !$isTankCleaningProduct) {
$flags[] = $this->automaticFlag(
'customer_rule_only_tank_cleaning',
'order_item',
(int)$row['order_item_id'],
null,
$row,
['product' => $this->productLabel($row)],
$this->orderItemContext($row)
);
}
$restrictedProducts = [
'restrictTankCleaning' => ['customer_rule_restrict_tank_cleaning', ['tank cleaning', 'tankcleaning', 'tankrens']],
'restrictSpotFree' => ['customer_rule_restrict_spot_free', ['spot free', 'spotfree']],
'restrictInteriorCleaning' => ['customer_rule_restrict_interior_cleaning', ['interior', 'indvendig']],
'exemptFromAdministrationFee' => ['customer_rule_exempt_from_administration_fees', ['administration fee', 'administrationsgebyr', 'administration']],
@@ -937,7 +963,14 @@ class invoice_period_flag_service
$reg = strtoupper(trim((string)($row['reg_1'] ?? '')));
$key = (int)$row['customer_number'] . '|' . $reg;
$expectedProductId = (int)($vehicleTypeByCustomerReg[$key]['product_id'] ?? 0);
if ($expectedProductId > 0 && $expectedProductId !== (int)$row['product_id']) {
$expectedProductName = (string)($vehicleTypeByCustomerReg[$key]['product_name'] ?? '');
if ($expectedProductId > 0
&& !$this->primaryVehicleProductsMatch(
(int)$row['product_id'],
$this->productLabel($row),
$expectedProductId,
$expectedProductName
)) {
$flags[] = $this->automaticFlag(
'vehicle_subscription_type_mismatch',
'order_item',
@@ -946,7 +979,7 @@ class invoice_period_flag_service
$row,
[
'product' => $this->productLabel($row),
'expected_product' => (string)($vehicleTypeByCustomerReg[$key]['product_name'] ?? $expectedProductId),
'expected_product' => $expectedProductName !== '' ? $expectedProductName : (string)$expectedProductId,
],
$this->orderItemContext($row) + ['expected_product_id' => $expectedProductId]
);
@@ -960,7 +993,12 @@ class invoice_period_flag_service
continue;
}
$expectedProductId = (int)$history[$reg]['product_id'];
if ($expectedProductId === (int)$row['product_id']) {
if ($this->primaryVehicleProductsMatch(
(int)$row['product_id'],
$this->productLabel($row),
$expectedProductId,
(string)$history[$reg]['product_name']
)) {
continue;
}
$flags[] = $this->automaticFlag(
@@ -1126,6 +1164,7 @@ class invoice_period_flag_service
'customer_rule_restrict_spot_free' => "{$product} violates restricted Spot Free.",
'customer_rule_restrict_interior_cleaning' => "{$product} violates restricted interior wash.",
'customer_rule_exempt_from_administration_fees' => "{$product} is an administration fee for an exempt customer.",
'customer_rule_only_tank_cleaning' => "{$product} violates the only tank cleaning rule.",
'customer_rule_requires_reference' => "Order is missing a required reference.",
'customer_rule_requires_po_number' => "Order is missing a required PO number.",
'customer_rule_invoice_all_orders_individually' => "Invoice collection contains multiple orders for a customer requiring individual invoices.",
@@ -1163,6 +1202,10 @@ class invoice_period_flag_service
['type' => 'order', 'text' => 'Wash certificate'],
['type' => 'text', 'text' => ' is attached without a wash certificate item.'],
],
'xlvask_missing_order_link' => [
['type' => 'xlvask_usage_log', 'text' => 'XL Vask wash'],
['type' => 'text', 'text' => ' is neither ignored nor linked to an order in the selected period.'],
],
default => [],
};
}
@@ -1466,6 +1509,50 @@ class invoice_period_flag_service
return $history;
}
private function primaryVehicleProductsMatch(
int $currentProductId,
string $currentProductName,
int $expectedProductId,
string $expectedProductName
): bool {
if ($expectedProductId > 0 && $currentProductId === $expectedProductId) {
return true;
}
$currentVehicleType = $this->normalizePrimaryVehicleProductName($currentProductName);
$expectedVehicleType = $this->normalizePrimaryVehicleProductName($expectedProductName);
return $currentVehicleType !== ''
&& $expectedVehicleType !== ''
&& $currentVehicleType === $expectedVehicleType;
}
private function normalizePrimaryVehicleProductName(string $productName): string
{
$normalized = strtolower(strtr($productName, [
'Æ' => 'ae',
'Ø' => 'oe',
'Å' => 'aa',
'æ' => 'ae',
'ø' => 'oe',
'å' => 'aa',
]));
$normalized = (string)preg_replace('/[^a-z0-9]+/', ' ', $normalized);
$tokens = array_values(array_filter(
explode(' ', trim($normalized)),
static fn(string $token): bool => $token !== ''
&& !in_array($token, [
'indvendig',
'indv',
'interior',
'internal',
'vask',
'wash',
], true)
));
return implode(' ', $tokens);
}
private function getOrderItemsForPreview(int $orderId): array
{
global $db;
@@ -1600,6 +1687,12 @@ class invoice_period_flag_service
return false;
}
private function rowIsTankCleaningProduct(array $row): bool
{
return (int)($row['product_category'] ?? 0) === 5
|| $this->rowMatchesProductTerms($row, ['tank cleaning', 'tankcleaning', 'tankrens']);
}
private function isIncludedOrderItem(array $row): bool
{
$value = $row['item_include_in_invoice'] ?? 1;
+9 -2
View File
@@ -155,7 +155,7 @@ class motorapi implements motorapi_i
// Get the cached result from the log/local database/cache
$motorapi_lookups = new motorapi_lookups_o();
// Add the cached value to the meta
$response->add_meta('cached', true);
self::addCachedMetaIfPossible($response);
$cleaned_result = self::cleanJSON($motorapi_lookups->getCachedResult($licensePlate)->result->value());
$object = json_decode($cleaned_result);
if ($object === null) {
@@ -165,6 +165,13 @@ class motorapi implements motorapi_i
return json_decode($cleaned_result);
}
public static function addCachedMetaIfPossible(mixed $response): void
{
if (is_object($response) && method_exists($response, 'add_meta')) {
$response->add_meta('cached', true);
}
}
/**
* @inheritDoc
* @throws Exception If the module is not enabled, the license plate is invalid, the daily limit is exceeded, or the secret key is invalid
@@ -385,4 +392,4 @@ class motorapi implements motorapi_i
$motorapi_lookups = new motorapi_lookups_o();
$motorapi_lookups->add($licensePlate, json_encode($response), $endpoint);
}
}
}
+49 -2
View File
@@ -34,11 +34,58 @@ class openai implements openai_i
*/
public function requireModuleEnabled(): void
{
if (!(bool)$this->config->enabled->getVariableValue()) {
if (!$this->config->enabled->isTrue()) {
throw new Exception('OpenAI module is not enabled.');
}
}
/**
* Send a structured JSON text task to the OpenAI Responses API.
*
* @throws Exception
*/
public function jsonTask(string $schemaName, string $prompt, array $payload, array $schema, float $temperature = 0.1): array
{
$this->requireModuleEnabled();
$data = [
'model' => $this->model,
'input' => [
[
'role' => 'user',
'content' => [
[
'type' => 'input_text',
'text' => $prompt . "\n\nData:\n" . json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
],
],
],
],
'text' => [
'format' => [
'type' => 'json_schema',
'name' => $schemaName,
'schema' => $schema,
'strict' => true,
],
],
'temperature' => $temperature,
];
$response = $this->sendRequest($data);
$output = $response['output'][0]['content'][0]['text'] ?? null;
if (!is_string($output) || $output === '') {
throw new Exception('Invalid response format from OpenAI API. (Missing text field)');
}
$decoded = json_decode($output, true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($decoded)) {
throw new Exception('Error parsing JSON response: ' . json_last_error_msg());
}
return $decoded;
}
protected function getLPRSchema(): array
{
return [
@@ -248,4 +295,4 @@ class openai implements openai_i
//print_r($responseData);
return $responseData;
}
}
}
File diff suppressed because it is too large Load Diff
@@ -28,10 +28,81 @@ class xlvask_usage_logs_schema_bootstrap
self::addColumnIfMissing($db, 'xlvask_usage_logs', 'ignored_at', 'DATETIME NULL AFTER WashItems');
self::addColumnIfMissing($db, 'xlvask_usage_logs', 'ignored_by', 'INT NULL AFTER ignored_at');
self::addColumnIfMissing($db, 'xlvask_usage_logs', 'ignored_reason', 'TEXT NULL AFTER ignored_by');
self::ensureAutomationTables($db);
self::$initialized = true;
}
private static function ensureAutomationTables(object $db): void
{
$db->query(
"CREATE TABLE IF NOT EXISTS `xlvask_automation_suggestions` (
`id` INT NOT NULL AUTO_INCREMENT,
`usage_log_id` INT NOT NULL,
`wash_id` VARCHAR(128) NOT NULL,
`signature_hash` CHAR(64) NOT NULL,
`signature_json` LONGTEXT NULL,
`action` VARCHAR(32) NOT NULL,
`status` VARCHAR(32) NOT NULL DEFAULT 'suggested',
`confidence` DECIMAL(5,4) NOT NULL DEFAULT 0.0000,
`source` VARCHAR(32) NOT NULL DEFAULT 'deterministic',
`matched_order_id` INT NULL,
`created_order_id` INT NULL,
`proposed_order_json` LONGTEXT NULL,
`candidate_order_json` LONGTEXT NULL,
`reason` TEXT NULL,
`created_by` INT NULL,
`decided_by` INT NULL,
`decided_at` DATETIME NULL,
`executed_at` DATETIME NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_xlvask_automation_usage` (`usage_log_id`),
KEY `idx_xlvask_automation_wash` (`wash_id`),
KEY `idx_xlvask_automation_signature` (`signature_hash`),
KEY `idx_xlvask_automation_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
);
$db->query(
"CREATE TABLE IF NOT EXISTS `xlvask_automation_feedback` (
`id` INT NOT NULL AUTO_INCREMENT,
`usage_log_id` INT NULL,
`wash_id` VARCHAR(128) NULL,
`signature_hash` CHAR(64) NOT NULL,
`signature_json` LONGTEXT NULL,
`action` VARCHAR(32) NOT NULL,
`decision` VARCHAR(32) NOT NULL,
`order_id` INT NULL,
`reason` TEXT NULL,
`created_by` INT NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_xlvask_feedback_signature_action` (`signature_hash`, `action`),
KEY `idx_xlvask_feedback_usage` (`usage_log_id`),
KEY `idx_xlvask_feedback_decision` (`decision`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
);
$db->query(
"CREATE TABLE IF NOT EXISTS `xlvask_automation_openai_cache` (
`id` INT NOT NULL AUTO_INCREMENT,
`cache_key` CHAR(64) NOT NULL,
`schema_name` VARCHAR(96) NOT NULL,
`input_json` LONGTEXT NOT NULL,
`result_json` LONGTEXT NOT NULL,
`hits` INT NOT NULL DEFAULT 0,
`last_hit_at` DATETIME NULL,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_xlvask_openai_cache_key` (`cache_key`),
KEY `idx_xlvask_openai_cache_schema` (`schema_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
);
}
private static function addColumnIfMissing(object $db, string $table, string $column, string $definition): void
{
if (!self::columnExists($db, $table, $column)) {
@@ -0,0 +1,29 @@
<?php
namespace xlvask\config;
use Exception;
use traits\module_config_variable;
class xlvask_automatic_order_attachment_enabled_c
{
use module_config_variable;
/**
* @throws Exception
*/
public function __construct()
{
self::setupConfigVariable(
'xlvask',
'automatic_order_attachment_enabled',
'bool',
true,
null,
'Whether XL Vask usage logs may automatically be attached to existing same-day employee orders.',
'0',
false,
'false'
);
}
}
@@ -0,0 +1,29 @@
<?php
namespace xlvask\config;
use Exception;
use traits\module_config_variable;
class xlvask_automatic_order_creation_enabled_c
{
use module_config_variable;
/**
* @throws Exception
*/
public function __construct()
{
self::setupConfigVariable(
'xlvask',
'automatic_order_creation_enabled',
'bool',
true,
null,
'Whether XL Vask usage logs may automatically create orders when no same-day order can be attached.',
'0',
false,
'false'
);
}
}
@@ -0,0 +1,29 @@
<?php
namespace xlvask\config;
use Exception;
use traits\module_config_variable;
class xlvask_openai_integration_enabled_c
{
use module_config_variable;
/**
* @throws Exception
*/
public function __construct()
{
self::setupConfigVariable(
'xlvask',
'openai_integration_enabled',
'bool',
true,
null,
'Whether XL Vask automation may ask OpenAI for attachment or creation suggestions.',
'0',
false,
'false'
);
}
}
@@ -2,6 +2,9 @@
namespace helpers;
require_once WD . '/classes/xlvask_automation_service.php';
use classes\xlvask_automation_service;
use Exception;
use objects\orders_o;
use objects\users_o;
@@ -67,6 +70,7 @@ class xlvask_tasks
(new xlvask_customers_o())->importCustomers();
(new xlvask_vehicles_o())->importVehicles();
(new xlvask_usage_logs_o())->importUsageLogs();
(new xlvask_automation_service())->runPending(null, null, [], 100, null);
};
}
@@ -608,4 +612,4 @@ class xlvask_tasks
$xlvask->requireModuleEnabled();
// Run
}
}
}
+25 -1
View File
@@ -3,11 +3,17 @@
namespace xlvask;
require_once WD . '/modules/xlvask/config/xlvask_enabled_c.php';
require_once WD . '/modules/xlvask/config/xlvask_synchronization_enabled_c.php';
require_once WD . '/modules/xlvask/config/xlvask_automatic_order_attachment_enabled_c.php';
require_once WD . '/modules/xlvask/config/xlvask_automatic_order_creation_enabled_c.php';
require_once WD . '/modules/xlvask/config/xlvask_openai_integration_enabled_c.php';
require_once WD . '/modules/xlvask/config/xlvask_username_c.php';
require_once WD . '/modules/xlvask/config/xlvask_password_c.php';
use traits\module_config_t;
use xlvask\config\xlvask_automatic_order_attachment_enabled_c;
use xlvask\config\xlvask_automatic_order_creation_enabled_c;
use xlvask\config\xlvask_enabled_c;
use xlvask\config\xlvask_openai_integration_enabled_c;
use xlvask\config\xlvask_password_c;
use xlvask\config\xlvask_synchronization_enabled_c;
use xlvask\config\xlvask_username_c;
@@ -31,6 +37,18 @@ class xlvask_c
* @var xlvask_synchronization_enabled_c $synchronization_enabled
*/
public xlvask_synchronization_enabled_c $synchronization_enabled;
/**
* @var xlvask_automatic_order_attachment_enabled_c $automatic_order_attachment_enabled
*/
public xlvask_automatic_order_attachment_enabled_c $automatic_order_attachment_enabled;
/**
* @var xlvask_automatic_order_creation_enabled_c $automatic_order_creation_enabled
*/
public xlvask_automatic_order_creation_enabled_c $automatic_order_creation_enabled;
/**
* @var xlvask_openai_integration_enabled_c $openai_integration_enabled
*/
public xlvask_openai_integration_enabled_c $openai_integration_enabled;
/**
* The username
* @var xlvask_username_c
@@ -53,12 +71,18 @@ class xlvask_c
$this->allowUpdate([
xlvask_enabled_c::class,
xlvask_synchronization_enabled_c::class,
xlvask_automatic_order_attachment_enabled_c::class,
xlvask_automatic_order_creation_enabled_c::class,
xlvask_openai_integration_enabled_c::class,
xlvask_username_c::class,
xlvask_password_c::class
]);
$this->enabled = new xlvask_enabled_c();
$this->synchronization_enabled = new xlvask_synchronization_enabled_c();
$this->automatic_order_attachment_enabled = new xlvask_automatic_order_attachment_enabled_c();
$this->automatic_order_creation_enabled = new xlvask_automatic_order_creation_enabled_c();
$this->openai_integration_enabled = new xlvask_openai_integration_enabled_c();
$this->username = new xlvask_username_c();
$this->password = new xlvask_password_c();
}
}
}
+22 -18
View File
@@ -48,6 +48,10 @@ class users_o extends db
$this->setTable('users');
}
private static function redisCache(): ?redis
{
return defined('redis') ? constant('redis') : null;
}
public function edit(int $id, string $customer_number, string|null $role, string|null $password, string|null $display_name): void
{
@@ -59,12 +63,12 @@ class users_o extends db
if ($old_res && $old_res->num_rows > 0) {
$old_cn = (int)$old_res->fetch_assoc()['customer_number'];
if ($old_cn !== 0 && $old_cn !== (int)$customer_number) {
redis->clear_user_id_from_customer_number($old_cn);
self::redisCache()?->clear_user_id_from_customer_number($old_cn);
}
}
// Cache the mapping from customer_number to user_id (new value)
redis->cache_user_id_from_customer_number((int)$customer_number, $this->id);
redis->cache_customer_number_from_user_id($this->id, (int)$customer_number);
self::redisCache()?->cache_user_id_from_customer_number((int)$customer_number, $this->id);
self::redisCache()?->cache_customer_number_from_user_id($this->id, (int)$customer_number);
// Avoid SQL injection
$customer_number = $db->escape_string($customer_number);
@@ -235,8 +239,8 @@ class users_o extends db
$this->id = (int)$db->insert_id();
// Cache the mapping from customer_number to user_id
redis->cache_user_id_from_customer_number((int)$customer_number, $this->id);
redis->cache_customer_number_from_user_id($this->id, (int)$customer_number);
self::redisCache()?->cache_user_id_from_customer_number((int)$customer_number, $this->id);
self::redisCache()?->cache_customer_number_from_user_id($this->id, (int)$customer_number);
// Set the values of the object properties
$this->getObjectProperties();
@@ -360,7 +364,7 @@ class users_o extends db
{
global $db;
// Check Redis for existence (by checking if we have the customer number)
$customer_number = redis->get_customer_number_from_user_id($id);
$customer_number = self::redisCache()?->get_customer_number_from_user_id($id);
if ($customer_number !== null) {
$this->id = $id;
$this->getObjectProperties();
@@ -374,7 +378,7 @@ class users_o extends db
$this->id = $id;
$customer_number = (int)$result->fetch_assoc()['customer_number'];
// Cache the result
redis->cache_customer_number_from_user_id($id, $customer_number);
self::redisCache()?->cache_customer_number_from_user_id($id, $customer_number);
$this->getObjectProperties();
}
return $this;
@@ -384,7 +388,7 @@ class users_o extends db
{
global $db;
// Check Redis first
$user_id = redis->get_user_id_from_customer_number($customer_number);
$user_id = self::redisCache()?->get_user_id_from_customer_number($customer_number);
if ($user_id !== null) {
$this->id = (int)$user_id;
$this->getObjectProperties();
@@ -397,7 +401,7 @@ class users_o extends db
if ($result->num_rows > 0) {
$this->id = (int)$result->fetch_assoc()['id'];
// Cache the result
redis->cache_user_id_from_customer_number($customer_number, $this->id);
self::redisCache()?->cache_user_id_from_customer_number($customer_number, $this->id);
$this->getObjectProperties();
} else {
// Import the customer
@@ -1073,22 +1077,22 @@ class users_o extends db
public function clearAllUsersEconomicCustomerDiscountsFromCache(): void
{
// Get all the cached results matching the pattern 'users_*_economic_customer_discount_percentage'
$cached_results = redis->get_keys('users_*_economic_customer_discount_percentage');
$cached_results = self::redisCache()?->get_keys('users_*_economic_customer_discount_percentage') ?? [];
// Loop through the cached results
foreach ( $cached_results as $key ) {
// Clear the cached discount percentage
redis->delete($key);
self::redisCache()?->delete($key);
}
}
public function clearAllUsersEconomicCustomerDetailsFromCache(): void
{
// Get all the cached results matching the pattern 'users_*_economic_customer'
$cached_results = redis->get_keys('users_*_economic_customer');
$cached_results = self::redisCache()?->get_keys('users_*_economic_customer') ?? [];
// Loop through the cached results
foreach ( $cached_results as $key ) {
// Clear the cached economic customer details
redis->delete($key);
self::redisCache()?->delete($key);
}
}
@@ -1096,7 +1100,7 @@ class users_o extends db
{
self::requireSelected();
// Check if the discount percentage is cached
$cached_discount_percentage = redis->get_economic_customer_discount_percentage($this->id);
$cached_discount_percentage = self::redisCache()?->get_economic_customer_discount_percentage($this->id);
if ($cached_discount_percentage !== null) {
return $cached_discount_percentage;
}
@@ -1104,7 +1108,7 @@ class users_o extends db
$economic = new economicCustomers();
$discount_percentage = $economic->getCustomerDiscountPercentage($this->customer_number->value());
// Cache the discount percentage
redis->cache_economic_customer_discount_percentage($this->id, $discount_percentage);
self::redisCache()?->cache_economic_customer_discount_percentage($this->id, $discount_percentage);
return $discount_percentage;
}
@@ -1143,7 +1147,7 @@ class users_o extends db
public function isImportedFromEconomic($customerNumber): bool
{
// Check Redis first
$user_id = redis->get_user_id_from_customer_number((int)$customerNumber);
$user_id = self::redisCache()?->get_user_id_from_customer_number((int)$customerNumber);
if ($user_id !== null) {
return true;
}
@@ -1158,7 +1162,7 @@ class users_o extends db
public function getUserIdFromEconomic($customerNumber): int
{
// Check Redis first
$user_id = redis->get_user_id_from_customer_number((int)$customerNumber);
$user_id = self::redisCache()?->get_user_id_from_customer_number((int)$customerNumber);
if ($user_id !== null) {
return (int)$user_id;
}
@@ -1168,7 +1172,7 @@ class users_o extends db
$id = (int)$user[0]['id'];
// Cache the result
redis->cache_user_id_from_customer_number((int)$customerNumber, $id);
self::redisCache()?->cache_user_id_from_customer_number((int)$customerNumber, $id);
return $id;
}
@@ -2,10 +2,13 @@
namespace routes;
require_once WD . '/classes/xlvask_automation_service.php';
use classes\authentication;
use classes\response;
use classes\router;
use classes\xlvask;
use classes\xlvask_automation_service;
use objects\orders_o;
use objects\users_o;
use objects\xlvask_customers_o;
@@ -256,6 +259,7 @@ class moduleXLVaskRoute
$xlvask_usage_logs_o = new \objects\xlvask_usage_logs_o();
// Import usage logs
$xlvask_usage_logs_o->importUsageLogs();
(new xlvask_automation_service())->runPending(null, null, [], 100, null);
// Response
$response->success(
'Usage logs imported',
@@ -267,4 +271,4 @@ class moduleXLVaskRoute
]
);
}
}
}
@@ -2,11 +2,14 @@
namespace routes;
require_once WD . '/classes/xlvask_automation_service.php';
use classes\authentication;
use classes\redis;
use classes\response;
use classes\stripe;
use classes\xlvask;
use classes\xlvask_automation_service;
use objects\collected_order_invoices_o;
use objects\departments_o;
use objects\economic_module_orders;
@@ -14,7 +17,6 @@ use objects\logs_o;
use objects\orders_o;
use objects\stripe_module_orders_o;
use objects\stripe_payment_intents_o;
use objects\users_o;
use objects\xlvask_usage_logs_o;
use traits\route_t;
@@ -47,6 +49,8 @@ class xlvaskUsageLogsRoute
(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();
$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);
@@ -62,7 +66,8 @@ class xlvaskUsageLogsRoute
// 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) {
function ($log) use ($response_includes_items_link, $response_includes_items, $xlvask_usage_logs, $user, $xlvask, $automation_service, &$linked_order_ids_by_wash_id) {
$automation = $automation_service->evaluateUsageLogRow($log, (int)$user->id, true);
// Remove the 'id' field from the log
$id = (int)$log['id'];
unset($log['id']);
@@ -72,6 +77,12 @@ class xlvaskUsageLogsRoute
$tmp = $xlvask->new($xlvask->helpers->xlvask_usage_log);
// Set the properties of the temporary object
$tmp->setProperties($log);
$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);
// Generate a fast link key for the order, used to retrieve the order with items later.
@@ -94,7 +105,10 @@ class xlvaskUsageLogsRoute
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
'automation' => $automation,
...$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(
@@ -160,6 +174,115 @@ class xlvaskUsageLogsRoute
]
);
$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);
return;
}
$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;
$response->success(
(new xlvask_automation_service())->runPending($dateFrom, $dateTo, $ids, $limit, (int)$user->id)
);
},
[
'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);
return;
}
$id = (int)($this->fromRoute('id') ?? 0);
if ($id < 1) {
$response->error('Invalid XL Vask usage log id', 400);
return;
}
$response->success(
(new xlvask_automation_service())->evaluateUsageLogById($id, (int)$user->id, true)
);
},
[
'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);
return;
}
$id = (int)($this->fromRoute('id') ?? 0);
if ($id < 1) {
$response->error('Invalid XL Vask usage log id', 400);
return;
}
$suggestionId = $this->isParametersSet(['suggestion_id']) ? (int)$this->getParameter('suggestion_id') : null;
$reason = $this->isParametersSet(['reason']) ? trim((string)$this->getParameter('reason')) : null;
$response->success(
(new xlvask_automation_service())->acceptUsageLogById($id, (int)$user->id, $suggestionId, $reason)
);
},
[
'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);
return;
}
$id = (int)($this->fromRoute('id') ?? 0);
if ($id < 1) {
$response->error('Invalid XL Vask usage log id', 400);
return;
}
$suggestionId = $this->isParametersSet(['suggestion_id']) ? (int)$this->getParameter('suggestion_id') : null;
$reason = $this->isParametersSet(['reason']) ? trim((string)$this->getParameter('reason')) : null;
$response->success(
(new xlvask_automation_service())->denyUsageLogById($id, (int)$user->id, $suggestionId, $reason)
);
},
[
'manage_xlvask_usage_automation' => 'Deny an XL Vask usage-log automation suggestion',
]
);
$this->get('/modules/xlvask/services/usage/orders/fast-link', function () {
global $response;
self::requireParameters([
@@ -1,5 +1,15 @@
<?php
it('creates auth tokens and reports missing customer users clearly', function (): void {
$script = tempnam(sys_get_temp_dir(), 'truckwash-create-token-') . '.php';
$appRoot = addslashes(WD);
file_put_contents($script, str_replace('__APP_ROOT__', $appRoot, <<<'PHP'
<?php
namespace {
define('WD', '__APP_ROOT__');
}
namespace objects {
class users_o
{
@@ -33,27 +43,47 @@ namespace objects {
}
namespace {
app_require('interfaces/authentication_i.php');
app_require('classes/authentication.php');
}
require_once WD . '/interfaces/authentication_i.php';
require_once WD . '/classes/authentication.php';
namespace {
it('creates a 64-char auth token for an existing user', function (): void {
\objects\users_o::$existing = [111111];
\objects\tokens_o::$created = [];
$result = [
'success_token' => null,
'created' => [],
'missing_error' => null,
];
$token = (new \classes\authentication())->create_token(111111);
expect($token)->toBeString()->toMatch('/^[a-f0-9]{64}$/');
expect(\objects\tokens_o::$created)->toHaveCount(1);
expect(\objects\tokens_o::$created[0]['user_id'])->toBe(123);
expect(\objects\tokens_o::$created[0]['type'])->toBe('AUTH_TOKEN');
});
it('throws a clear exception when customer user is missing', function (): void {
\objects\users_o::$existing = [];
\objects\users_o::$existing = [111111];
\objects\tokens_o::$created = [];
$result['success_token'] = (new \classes\authentication())->create_token(111111);
$result['created'] = \objects\tokens_o::$created;
\objects\users_o::$existing = [];
try {
(new \classes\authentication())->create_token(222222);
})->throws(\Exception::class, 'User not found for customer number: 222222');
}
} catch (\Throwable $exception) {
$result['missing_error'] = $exception->getMessage();
}
echo json_encode($result, JSON_THROW_ON_ERROR);
}
PHP));
try {
$output = [];
$exitCode = 0;
exec(PHP_BINARY . ' ' . escapeshellarg($script), $output, $exitCode);
expect($exitCode)->toBe(0);
$result = json_decode(implode("\n", $output), true, 512, JSON_THROW_ON_ERROR);
expect($result['success_token'])->toBeString()->toMatch('/^[a-f0-9]{64}$/')
->and($result['created'])->toHaveCount(1)
->and($result['created'][0]['user_id'])->toBe(123)
->and($result['created'][0]['type'])->toBe('AUTH_TOKEN')
->and($result['missing_error'])->toBe('User not found for customer number: 222222');
} finally {
if (is_file($script)) {
unlink($script);
}
}
});
@@ -0,0 +1,9 @@
<?php
it('keeps db object Redis access namespace safe', function (): void {
$content = file_get_contents(WD . '/traits/db_object_t.php');
expect($content)
->not->toContain('redis->')
->and($content)->toContain("constant('redis')");
});
@@ -111,6 +111,29 @@ it('builds interactive message parts for order and wash certificate warnings', f
[],
['department_id' => 1, 'order_id' => 9001, 'order_item_id' => 7001],
]);
$xlVaskFlag = invoice_period_flag_service_invoke('automaticFlag', [
'xlvask_missing_order_link',
'xlvask_usage_log',
55,
null,
[
'customer_number' => 424242,
'customer_name' => 'Flagged Customer',
'xlvask_usage_log_id' => 55,
],
[
'wash_id' => 'wash-55',
'registration_number' => 'AB12345',
],
[
'customer_number' => 424242,
'customer_name' => 'Flagged Customer',
'xlvask_usage_log_id' => 55,
'wash_id' => 'wash-55',
'registration_number' => 'AB12345',
'start_time' => '2026-05-11 10:00:00',
],
]);
expect($orderFlag['message_parts'])->toBe([
['type' => 'order', 'text' => 'Order'],
@@ -120,6 +143,10 @@ it('builds interactive message parts for order and wash certificate warnings', f
['type' => 'order_item', 'text' => 'Wash certificate item'],
['type' => 'text', 'text' => ' is present without a wash certificate.'],
]);
expect($xlVaskFlag['message_parts'])->toBe([
['type' => 'xlvask_usage_log', 'text' => 'XL Vask wash'],
['type' => 'text', 'text' => ' is neither ignored nor linked to an order in the selected period.'],
]);
});
it('includes order item preview context for required order field warnings', function (): void {
@@ -227,6 +254,159 @@ it('includes order item preview context for required order field warnings', func
}
});
it('allows tank cleaning products for only tank cleaning customers', function (): void {
$baseRow = [
'customer_number' => 424242,
'customer_name' => 'Tank Customer',
'order_id' => 61426,
'invoice_collection_id' => 3090,
'department_id' => 5,
'related_item_id' => 0,
'item_price' => 100,
'order_reference' => 'REF',
'order_po' => 'PO',
'reg_1' => 'NI465',
];
$tankCleaningRow = $baseRow + [
'order_item_id' => 801,
'product_id' => 30,
'product_name' => 'Tank cleaning 4 spulehoveder',
'product_category' => 5,
'category_name' => 'Tank cleaning',
];
$tankCleaningAddonRow = $baseRow + [
'order_item_id' => 802,
'product_id' => 33,
'product_name' => 'Saebe/kemi, 1-4 spulehoveder',
'product_category' => 5,
'category_name' => 'Tank cleaning',
];
$washRow = $baseRow + [
'order_item_id' => 803,
'product_id' => 3,
'product_name' => 'Forvogn',
'product_category' => 1,
'category_name' => 'Vask',
];
$onlyTankCleaningFlags = invoice_period_flag_service_invoke('detectCustomerRuleViolations', [
[$tankCleaningRow, $tankCleaningAddonRow, $washRow],
[424242 => ['onlyTankCleaning' => true]],
]);
expect(array_column($onlyTankCleaningFlags, 'definition_key'))->toBe(['customer_rule_only_tank_cleaning']);
expect($onlyTankCleaningFlags[0]['target_id'])->toBe(803);
$restrictedTankCleaningFlags = invoice_period_flag_service_invoke('detectCustomerRuleViolations', [
[$tankCleaningRow, $washRow],
[424242 => ['restrictTankCleaning' => true]],
]);
expect(array_column($restrictedTankCleaningFlags, 'definition_key'))->toBe(['customer_rule_restrict_tank_cleaning']);
expect($restrictedTankCleaningFlags[0]['target_id'])->toBe(801);
});
it('does not flag interior wash variants as historical primary product mismatches', function (): void {
global $db;
$hadDb = array_key_exists('db', $GLOBALS);
$previousDb = $GLOBALS['db'] ?? null;
$db = new class {
public function escape_string(string $value): string
{
return addslashes($value);
}
public function query(string $sql): object|false
{
if (str_contains($sql, 'SHOW COLUMNS FROM `customer_vehicles`')) {
return $this->result([]);
}
if (str_contains($sql, 'FROM customer_vehicles')) {
return $this->result([]);
}
if (str_contains($sql, 'FROM orders o') && str_contains($sql, 'GROUP BY UPPER(TRIM(o.reg_1))')) {
return $this->result([
[
'reg' => 'CN96636',
'product_id' => 3,
'product_name' => 'Forvogn',
'usage_count' => 5,
],
]);
}
return false;
}
private function result(array $rows): object
{
return new class($rows) {
public int $num_rows;
public array $rows;
public function __construct(array $rows)
{
$this->rows = $rows;
$this->num_rows = count($rows);
}
public function fetch_assoc(): ?array
{
return array_shift($this->rows);
}
};
}
};
try {
$baseRow = [
'customer_number' => 424242,
'customer_name' => 'History Customer',
'order_id' => 61311,
'order_item_id' => 901,
'invoice_collection_id' => 16912,
'department_id' => 7,
'reg_1' => 'CN96636',
'is_wash' => 1,
'related_item_id' => 0,
'order_created_at' => '2026-05-11 08:05:21',
];
$interiorVariantFlags = invoice_period_flag_service_invoke('detectVehicleTypeMismatches', [
[
$baseRow + [
'product_id' => 99,
'product_name' => 'Indvendig vask Forvogn',
],
],
'2026-05-11 00:00:00',
]);
expect($interiorVariantFlags)->toBe([]);
$mismatchFlags = invoice_period_flag_service_invoke('detectVehicleTypeMismatches', [
[
$baseRow + [
'product_id' => 17,
'product_name' => 'Bus',
],
],
'2026-05-11 00:00:00',
]);
expect(array_column($mismatchFlags, 'definition_key'))->toBe(['historical_primary_product_mismatch']);
} finally {
if ($hadDb) {
$db = $previousDb;
} else {
unset($GLOBALS['db']);
}
}
});
it('does not report duplicate primary vehicle products from duplicated detector rows for the same order item', function (): void {
$row = [
'customer_number' => 424242,
@@ -0,0 +1,22 @@
<?php
use classes\motorapi;
require_once WD . '/classes/motorapi.php';
it('only writes cached MotorAPI metadata when a response object exists', function (): void {
motorapi::addCachedMetaIfPossible(null);
$response = new class {
public array $meta = [];
public function add_meta(string $key, mixed $value): void
{
$this->meta[$key] = $value;
}
};
motorapi::addCachedMetaIfPossible($response);
expect($response->meta)->toBe(['cached' => true]);
});
@@ -0,0 +1,9 @@
<?php
it('keeps users Redis access namespace safe', function (): void {
$content = file_get_contents(WD . '/objects/users_o.php');
expect($content)
->not->toContain('redis->')
->and($content)->toContain("constant('redis')");
});
@@ -0,0 +1,137 @@
<?php
use classes\xlvask_automation_service;
require_once WD . '/classes/xlvask_automation_service.php';
it('normalizes registrations for XL Vask automation signatures', function (): void {
expect(xlvask_automation_service::normalizeRegistrationForAutomation(' ec 21-233 '))
->toBe('EC21233');
});
it('builds stable XL Vask automation item signatures', function (): void {
$items = [
['product_id' => 20, 'quantity' => 1, 'price' => 275],
['product_id' => 10, 'quantity' => 2, 'price' => 649],
['product_id' => 20, 'quantity' => 1, 'price' => 0],
];
expect(xlvask_automation_service::itemSignaturePartsForAutomation($items))
->toBe([
'10:2:649',
'20:1:0',
'20:1:275',
]);
});
it('normalizes persisted XL Vask usage-log rows before helper hydration', function (): void {
$row = xlvask_automation_service::normalizeUsageLogRowForAutomation([
'id' => 47086,
'WashId' => 'cc1eabc1-b4e1-425b-ad7c-dc68f8c97ceb',
'WashItems' => '[{"OriginalProductName":"Bus","Count":1}]',
]);
expect($row)
->not->toHaveKey('id')
->and($row['WashItems'])->toBe([
[
'OriginalProductName' => 'Bus',
'Count' => 1,
],
]);
});
it('builds stable OpenAI cache keys for identical automation input', function (): void {
$prompt = 'Prompt';
$schemaName = 'xlvask_automation';
$schema = [
'required' => ['action'],
'properties' => [
'confidence' => ['type' => 'number'],
'action' => ['type' => 'string'],
],
];
$schemaWithDifferentKeyOrder = [
'properties' => [
'action' => ['type' => 'string'],
'confidence' => ['type' => 'number'],
],
'required' => ['action'],
];
$payloadA = [
'usage_log' => [
'registration' => 'AB12345',
'creation_allowed' => true,
],
'candidate_orders' => [
['id' => 10, 'items' => [['product_id' => 1, 'quantity' => 1, 'price' => 100]]],
],
];
$payloadB = [
'candidate_orders' => [
['items' => [['price' => 100, 'quantity' => 1, 'product_id' => 1]], 'id' => 10],
],
'usage_log' => [
'creation_allowed' => true,
'registration' => 'AB12345',
],
];
expect(xlvask_automation_service::openAiCacheKeyForAutomation($schemaName, $prompt, $payloadA, $schema, 0.1))
->toBe(xlvask_automation_service::openAiCacheKeyForAutomation($schemaName, $prompt, $payloadB, $schemaWithDifferentKeyOrder, 0.1));
});
it('changes OpenAI cache keys when automation eligibility input changes', function (): void {
$schema = ['type' => 'object'];
$newerWashPayload = ['usage_log' => ['creation_allowed' => false, 'age_bucket' => 'newer_than_6_hours']];
$olderWashPayload = ['usage_log' => ['creation_allowed' => true, 'age_bucket' => 'older_than_6_hours']];
expect(xlvask_automation_service::openAiCacheKeyForAutomation('xlvask_automation', 'Prompt', $newerWashPayload, $schema, 0.1))
->not->toBe(xlvask_automation_service::openAiCacheKeyForAutomation('xlvask_automation', 'Prompt', $olderWashPayload, $schema, 0.1));
});
it('declares a persistent OpenAI cache table for XL Vask automation', function (): void {
$bootstrapContent = file_get_contents(WD . '/classes/xlvask_usage_logs_schema_bootstrap.php');
expect($bootstrapContent)
->toContain('xlvask_automation_openai_cache')
->toContain('UNIQUE KEY `uniq_xlvask_openai_cache_key` (`cache_key`)');
});
it('scores same-day orders with matching XL Vask products and extra add-ons as attach suggestions', function (): void {
$usageItems = [
['product_id' => 10, 'quantity' => 1, 'price' => 519, 'product' => ['name' => 'Forvogn']],
['product_id' => 24, 'quantity' => 1, 'price' => 39, 'product' => ['name' => 'Spot Free- Lastbil']],
['product_id' => 21, 'quantity' => 1, 'price' => 79, 'product' => ['name' => 'Undervognskyl pr. Enhed']],
['product_id' => 99, 'quantity' => 8, 'price' => 0, 'product' => ['name' => 'Halleje']],
];
$orderItems = [
['product_id' => 10, 'quantity' => 1, 'price' => 519, 'product' => ['name' => 'Forvogn']],
['product_id' => 50, 'quantity' => 1, 'price' => 319, 'product' => ['name' => 'Indvendig vask Forvogn']],
['product_id' => 21, 'quantity' => 1, 'price' => 79, 'product' => ['name' => 'Undervognskyl pr. Enhed']],
['product_id' => 24, 'quantity' => 1, 'price' => 39, 'product' => ['name' => 'Spot Free- Lastbil']],
];
$score = xlvask_automation_service::scoreItemMatchForAutomation($usageItems, $orderItems);
expect($score['source'])
->toBe('fuzzy')
->and($score['confidence'])->toBeGreaterThanOrEqual(0.70)
->and($score['confidence'])->toBeLessThan(0.92)
->and($score['reason'])->toContain('ekstra ydelser');
});
it('does not score an order with only the primary product as a matching add-on attachment', function (): void {
$usageItems = [
['product_id' => 10, 'quantity' => 1, 'price' => 519, 'product' => ['name' => 'Forvogn']],
['product_id' => 24, 'quantity' => 1, 'price' => 39, 'product' => ['name' => 'Spot Free- Lastbil']],
['product_id' => 21, 'quantity' => 1, 'price' => 79, 'product' => ['name' => 'Undervognskyl pr. Enhed']],
];
$orderItems = [
['product_id' => 10, 'quantity' => 1, 'price' => 519, 'product' => ['name' => 'Forvogn']],
['product_id' => 50, 'quantity' => 1, 'price' => 319, 'product' => ['name' => 'Indvendig vask Forvogn']],
];
expect(xlvask_automation_service::scoreItemMatchForAutomation($usageItems, $orderItems)['confidence'])
->toBe(0.0);
});
@@ -0,0 +1,16 @@
<?php
it('exposes direct linked order metadata on XL Vask usage order rows', function (): void {
$route = file_get_contents(WD . '/routes/xlvaskUsageLogsRoute.php');
expect($route)->not->toBeFalse();
$route = (string)$route;
expect($route)
->toContain('$linked_order_ids_by_wash_id = []')
->and($route)->toContain('array_key_exists($wash_id, $linked_order_ids_by_wash_id)')
->and($route)->toContain('selectByWashId($wash_id)')
->and($route)->toContain("'linked_order_id' => \$linked_order_id")
->and($route)->toContain("'usage_log_id' => \$id");
});
+14 -6
View File
@@ -1029,6 +1029,11 @@ trait db_object_t
$this->table = $table;
}
private static function dbObjectRedisCache(): ?object
{
return defined('redis') ? constant('redis') : null;
}
/**
* Cache object
* @param string $key The key to cache the object
@@ -1045,7 +1050,7 @@ trait db_object_t
$data = json_encode($data);
}
// Cache the data
redis->set($this->table . '_' . $objectId . '_' . $key, $data);
self::dbObjectRedisCache()?->set($this->table . '_' . $objectId . '_' . $key, $data);
}
/**
@@ -1060,9 +1065,9 @@ trait db_object_t
return [];
}
return redis->mget(array_map(function($objectId) use ($key) {
return self::dbObjectRedisCache()?->mget(array_map(function($objectId) use ($key) {
return $this->table . '_' . $objectId . '_' . $key;
}, $objectIds));
}, $objectIds)) ?? [];
}
/**
@@ -1081,7 +1086,7 @@ trait db_object_t
$objectId = $this->id;
}
// Set the expiration time for the cached data
redis->expire($this->table . '_' . $objectId . '_' . $key, $seconds);
self::dbObjectRedisCache()?->expire($this->table . '_' . $objectId . '_' . $key, $seconds);
}
/**
@@ -1113,7 +1118,7 @@ trait db_object_t
$objectId = $this->id;
}
// Get the cached data
$data = redis->get($this->table . '_' . $objectId . '_' . $key) ?? null;
$data = self::dbObjectRedisCache()?->get($this->table . '_' . $objectId . '_' . $key) ?? null;
// if the data is a JSON string, convert it to an array
if (is_string($data) && json_decode($data)) {
$data = json_decode($data);
@@ -1164,8 +1169,11 @@ trait db_object_t
if (!$objectId) {
$objectId = $this->id;
}
if (!defined('redis')) {
return;
}
// Delete the cached data
redis->delete($this->table . '_' . $objectId . '_' . $key);
\constant('redis')->delete($this->table . '_' . $objectId . '_' . $key);
}
/**