From d3e93911605d64b095dac92a6d36d86f19ba9acf Mon Sep 17 00:00:00 2001 From: Jepp9350 <2jepp9350@gmail.com> Date: Fri, 24 Jan 2025 13:04:10 +0100 Subject: [PATCH] Add new cron tasks and enhance user synchronization logic Introduced cron tasks for syncing user economic customer details and discounts. Enhanced caching mechanisms for users and departments, added Slack messaging capabilities, and updated CLI/script routing for better flexibility. New routes and utility methods improve cron execution and logging. --- classes/redis.php | 18 +++- classes/slack.php | 7 ++ cli.php | 24 ++++- config.example.php | 3 +- cron/ClearAllUsersEconomicCustomerDetails.php | 22 +++++ ...ClearAllUsersEconomicCustomerDiscounts.php | 22 +++++ cron/Cron.php | 32 ++++++- cron/SyncBookings.php | 7 +- index.php | 2 +- objects/logs_o.php | 10 ++- objects/users_o.php | 87 +++++++++++++------ routes/cronRoute.php | 48 ++++++++++ traits/db_object_t.php | 13 +++ 13 files changed, 253 insertions(+), 42 deletions(-) create mode 100644 cron/ClearAllUsersEconomicCustomerDetails.php create mode 100644 cron/ClearAllUsersEconomicCustomerDiscounts.php create mode 100644 routes/cronRoute.php diff --git a/classes/redis.php b/classes/redis.php index f72cd4fc..2140ed1b 100644 --- a/classes/redis.php +++ b/classes/redis.php @@ -87,10 +87,15 @@ class redis implements redis_i public function cache_department_name(int $department_id, string $department_name): self { // Cache the department name (If it is not empty or null) - if (!empty($department_name)) { + if (empty($department_name)) { $department_name = 'IS_EMPTY_OR_NULL'; + return $this; } - $this->set('department_name_' . $department_id, $department_name); + // Set the department's name in the cache + if ($this->get_department_name($department_id) === null) { + $this->set('department_' . $department_id, json_encode(['name' => $department_name] + )); + }; return $this; } @@ -100,7 +105,12 @@ class redis implements redis_i public function get_department_name(int $department_id): string|null { // Get the department name - return $this->get('department_name_' . $department_id); + $tmp_department = $this->get('department_' . $department_id); + if ($tmp_department === 'IS_EMPTY_OR_NULL' || $tmp_department === null) { + return null; + } + // Return the department name + return json_decode($tmp_department, true)->name; } /** @@ -109,7 +119,7 @@ class redis implements redis_i public function clear_department_name(int $department_id): self { // Clear the department name - $this->delete('department_name_' . $department_id); + $this->delete('department_' . $department_id); return $this; } diff --git a/classes/slack.php b/classes/slack.php index 95d8d78d..e3c83ea0 100644 --- a/classes/slack.php +++ b/classes/slack.php @@ -129,4 +129,11 @@ class slack implements notification_i . "Notes: $notes\n" . "Status: $status"; } + + public function send_message(string $string): void + { + global $SLACK_DEFAULT_WEBHOOK; + // Send the message to the slack webhook + self::add_log(self::send_webhook_message($string, $SLACK_DEFAULT_WEBHOOK)); + } } \ No newline at end of file diff --git a/cli.php b/cli.php index 92b58a94..0213c9d9 100644 --- a/cli.php +++ b/cli.php @@ -1,9 +1,27 @@ 'error', + 'message' => 'Invalid token' + ]); + exit; + } +} + // If the first argument is 'run', switch to the second argument if ($args[1] === 'run') { @@ -45,4 +63,6 @@ if ($args[1] === 'run') { echo "Invalid script name"; break; } +} else { + echo "Invalid action"; } \ No newline at end of file diff --git a/config.example.php b/config.example.php index cb6f0939..8d7684e5 100644 --- a/config.example.php +++ b/config.example.php @@ -1,4 +1,4 @@ - '', // IP address of the database server e.g. 127.0.0.1 'user' => '', // Username of the database server e.g. root @@ -29,6 +29,7 @@ $MINIO = [ 'access_key' => '', // Minio access 'secret_key' => '' // Minio secret key ]; +$SLACK_DEFAULT_WEBHOOK = ''; // Default Slack webhook URL e.g. https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX $REDIS_CONFIG = [ 'host' => '', // Redis host (IP address) 'database' => 0, // Redis database number (0-15) diff --git a/cron/ClearAllUsersEconomicCustomerDetails.php b/cron/ClearAllUsersEconomicCustomerDetails.php new file mode 100644 index 00000000..d4b98ab3 --- /dev/null +++ b/cron/ClearAllUsersEconomicCustomerDetails.php @@ -0,0 +1,22 @@ +clearAllUsersEconomicCustomerDetailsFromCache(); +$end = microtime(true); +//$slack = new \classes\slack(); +//$slack->send_message("The booking sync script has finished. It took " . round($end - $start, 2) . " seconds to run."); \ No newline at end of file diff --git a/cron/ClearAllUsersEconomicCustomerDiscounts.php b/cron/ClearAllUsersEconomicCustomerDiscounts.php new file mode 100644 index 00000000..f5b7cb0d --- /dev/null +++ b/cron/ClearAllUsersEconomicCustomerDiscounts.php @@ -0,0 +1,22 @@ +clearAllUsersEconomicCustomerDiscountsFromCache(); +$end = microtime(true); +//$slack = new \classes\slack(); +//$slack->send_message("The booking sync script has finished. It took " . round($end - $start, 2) . " seconds to run."); \ No newline at end of file diff --git a/cron/Cron.php b/cron/Cron.php index 4c556dc6..a6d77669 100644 --- a/cron/Cron.php +++ b/cron/Cron.php @@ -3,6 +3,7 @@ use objects\bookings_o; use objects\logs_o; +use objects\users_o; if (!defined('WD')) { exit; @@ -14,6 +15,8 @@ function warn($message): void echo "\n\033[33m$message\033[0m\n"; } +$response_cron = []; + // Define the cron tasks $cron_tasks = [ 'CheckUnfulfilledBookings' => [ @@ -35,6 +38,18 @@ $cron_tasks = [ 'next_run' => 0, 'function' => 'syncLogsToDatabase', ], + 'SyncUserEconomicCustomerDiscounts' => [ + 'interval' => 600, // 10 minutes + 'last_run' => 0, + 'next_run' => 0, + 'function' => 'SyncUserEconomicCustomerDiscounts', + ], + 'SyncUserEconomicCustomerDetails' => [ + 'interval' => 43200, // 12 hours + 'last_run' => 0, + 'next_run' => 0, + 'function' => 'SyncUserEconomicCustomerDetails', + ], ]; function checkUnfulfilledBookings(): void @@ -46,7 +61,7 @@ function checkUnfulfilledBookings(): void function syncBookings(): void { $bookings_o = new bookings_o(); - $bookings_o->syncBookings(); + $response_cron[] = $bookings_o->syncBookings(); } function syncLogsToDatabase(): void @@ -55,6 +70,19 @@ function syncLogsToDatabase(): void $logs_o->syncLogsToDatabase(); } +function SyncUserEconomicCustomerDiscounts(): void +{ + $users_o = new users_o(); + $users_o->clearAllUsersEconomicCustomerDiscountsFromCache(); +} + +function SyncUserEconomicCustomerDetails(): void +{ + $users_o = new users_o(); + $users_o->clearAllUsersEconomicCustomerDetailsFromCache(); + $users_o->syncAllUsersEconomicCustomerDetails(); +} + foreach ( $cron_tasks as $task => $data ) { $lastRun = redis->get_last_crond_run($task) === null ? 0 : redis->get_last_crond_run($task); $nextRun = $lastRun + $data['interval']; @@ -64,6 +92,6 @@ foreach ( $cron_tasks as $task => $data ) { $data['function'](); redis->set_last_crond_run($task, time()); } else { - warn('Task ' . $task . ' is not due to run yet, next run is at ' . date('Y-m-d H:i:s', $nextRun) . ' (' . ($nextRun - time()) . ' seconds)'); + $response_cron[] = $task . ' is not due to run yet, next run is at ' . date('Y-m-d H:i:s', $nextRun) . ' (' . ($nextRun - time()) . ' seconds)'; } } \ No newline at end of file diff --git a/cron/SyncBookings.php b/cron/SyncBookings.php index fdd2b93a..2c6d98f3 100644 --- a/cron/SyncBookings.php +++ b/cron/SyncBookings.php @@ -13,7 +13,10 @@ use objects\bookings_o; if (!defined('WD')) { exit; } - +$start = microtime(true); // Sync the bookings $bookings_o = new bookings_o(); -$bookings_o->syncBookings(); \ No newline at end of file +$bookings_o->syncBookings(); +$end = microtime(true); +//$slack = new \classes\slack(); +//$slack->send_message("The booking sync script has finished. It took " . round($end - $start, 2) . " seconds to run."); \ No newline at end of file diff --git a/index.php b/index.php index c2a451d3..63eba215 100644 --- a/index.php +++ b/index.php @@ -96,7 +96,7 @@ foreach ( glob(WD . '/objects/*.php') as $object ) { } // If the program was called from the command line, run the cli script -if (php_sapi_name() === 'cli') { +if (php_sapi_name() === 'cli' || isset($_GET['internalCronCall'])) { require_once 'cli.php'; exit; } diff --git a/objects/logs_o.php b/objects/logs_o.php index 1b2b78fc..1df90b20 100644 --- a/objects/logs_o.php +++ b/objects/logs_o.php @@ -58,10 +58,12 @@ class logs_o extends db echo "\r\033[33m" . str_repeat('=', $percentage) . str_repeat(' ', 100 - $percentage) . "\033[0m " . number_format($percentage, 2) . '%'; } // Show the percentage, completed and total - echo "\r\033[33m" . number_format($percentage, 2) . '% (' . $current . '/' . $total . ")\033[0m"; - // If the current is equal to the total, add a new line - if ($current === $total) { - echo "\n"; + if ($display_progress) { + echo "\r\033[33m" . number_format($percentage, 2) . '% (' . $current . '/' . $total . ")\033[0m"; + // If the current is equal to the total, add a new line + if ($current === $total) { + echo "\n"; + } } } diff --git a/objects/users_o.php b/objects/users_o.php index a0c51307..5e077602 100644 --- a/objects/users_o.php +++ b/objects/users_o.php @@ -259,11 +259,9 @@ class users_o extends db if ($customer_number === 0) { return null; } - $tmp_user = redis->get_economic_customer_name((int)$customer_number); - if ($tmp_user !== null) { - return $tmp_user; - } + // Create a temporary user object $tmp_user = new users_o(); + $tmp_user->getUserByCustomerNumber($customer_number); // Get the customer name (Check cache first) $cached = $tmp_user->getCached('economic_customer'); if (!$cached) { @@ -578,23 +576,6 @@ class users_o extends db $this->keys->setUser($this->id)->deleteValue('open_invoice_draft'); } - /** - * Set a custom price (DISCOUNT) for the user - * @param int $user_id - * @param int $object_id The ID of the object - * @param int $discount_percentage The discount percentage - * @param bool $is_category If the object is a category - * @return void - */ - public function setCustomPrice(int $user_id, int|string $object_id, int $discount_percentage, bool $is_category = false): void - { - $this->id = $user_id; - // Get the user object properties - $this->getObjectProperties(); - // Set the custom price (key = 'custom_price') - $this->price_overrides->setUser($this->id)->setPrice($is_category, $object_id, $discount_percentage); - } - /** * Get the custom price (DISCOUNT) for the user * @param int $object_id The ID of the object @@ -726,7 +707,7 @@ class users_o extends db // Check if the name is cached $cached_name = $this->getCached('economic_customer', $id); if (!$cached_name) { - echo 'No cached name found for user with ID: ' . $id; + // No cached name, get the name from the external source // Get the name from the external source $tmp_user = new users_o(); $tmp_user->getUserById($id); @@ -741,6 +722,38 @@ class users_o extends db return $cached_name->name ?? null; } + public function hasPassword(): bool + { + return $this->password->value() !== null; + } + + public function getPassword(): string|null + { + return $this->password->value(); + } + + 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'); + // Loop through the cached results + foreach ( $cached_results as $key ) { + // Clear the cached discount percentage + redis->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'); + // Loop through the cached results + foreach ( $cached_results as $key ) { + // Clear the cached economic customer details + redis->delete($key); + } + } + public function getEconomicCustomerDiscountPercentage(): int { // Check if the discount percentage is cached @@ -756,13 +769,35 @@ class users_o extends db return $discount_percentage; } - public function hasPassword(): bool + /** + * Set a custom price (DISCOUNT) for the user + * @param int $user_id + * @param int $object_id The ID of the object + * @param int $discount_percentage The discount percentage + * @param bool $is_category If the object is a category + * @return void + */ + public function setCustomPrice(int $user_id, int|string $object_id, int $discount_percentage, bool $is_category = false): void { - return $this->password->value() !== null; + $this->id = $user_id; + // Get the user object properties + $this->getObjectProperties(); + // Set the custom price (key = 'custom_price') + $this->price_overrides->setUser($this->id)->setPrice($is_category, $object_id, $discount_percentage); } - public function getPassword(): string|null + public function syncAllUsersEconomicCustomerDetails(): void { - return $this->password->value(); + // Get all users + $users = $this->getFields(['id', 'customer_number']); + // Loop through the users + foreach ( $users as $user ) { + // Check if the customer number is set (Or if it is 0) + if ($user['customer_number'] === 0) { + continue; + } + // Get the customer data from the external source + $this->getCustomerEcocomicData($user['customer_number']); + } } } \ No newline at end of file diff --git a/routes/cronRoute.php b/routes/cronRoute.php new file mode 100644 index 00000000..86925d7b --- /dev/null +++ b/routes/cronRoute.php @@ -0,0 +1,48 @@ +post('/superuser/cron', function () { + // Get the post data + global $response; + // Make sure the user has the SUPERUSER_RUN_CRON permission + if (!$this->requirePermission('SUPERUSER_RUN_CRON')) { + $response->error('Permission denied', 403); + } + // Get the user object + $user = (new authentication())->get_user(); + // Get the post data + $data = json_decode(file_get_contents('php://input'), true); + // Check if a specific cron job is requested + if (isset($data['job'])) { + // Check if the cron job exists + if (!file_exists(WD . '/cron/' . $data['job'] . '.php')) { + $response->error('Cron job not found', 404); + } + // Include the cron job + require_once WD . '/cron/' . $data['job'] . '.php'; + // Log the incident + (new logs_o())->add('cron', 'global', 1, $user->id, 'CRON_JOB_RUN', 'Ran cron job: ' . $data['job']); + $response->success('Cron job ran successfully'); + } + // If no specific cron job is requested, run all cron jobs (through the cron.php file) + require_once WD . '/cron/Cron.php'; + // Log the incident + (new logs_o())->add('cron', 'global', 1, $user->id, 'CRON_RUN', 'Ran all cron jobs'); + $response->success([ + 'message' => 'All cron jobs ran successfully', + 'data' => $response_cron ?? [] + ]); + }); + } +} \ No newline at end of file diff --git a/traits/db_object_t.php b/traits/db_object_t.php index d84a5348..05243adb 100644 --- a/traits/db_object_t.php +++ b/traits/db_object_t.php @@ -375,4 +375,17 @@ trait db_object_t // Delete the cached data redis->delete($this->table . '_' . $objectId . '_' . $key); } + + /** + * Get all objects fields + * @param array $fields The fields to get from the objects (Eg. ['name', 'description']) + * @return array The list of fields from the objects (Eg. [['name' => 'John', 'description' => 'Doe'], ['name' => 'Jane', 'description' => 'Doe']]) + */ + public function getFields(array $fields): array + { + global $db; + $sql = "SELECT " . implode(', ', $fields) . " FROM $this->table"; + $result = $db->query($sql); + return $db->fetch_all($result); + } } \ No newline at end of file