From 8702b41777d2dcacbeb5686c31b5ac46494698e7 Mon Sep 17 00:00:00 2001 From: Jepp9350 <2jepp9350@gmail.com> Date: Tue, 18 Feb 2025 12:34:26 +0100 Subject: [PATCH] Add MotorAPI integration and Economic products support Implemented MotorAPI lookup functionality with proper validation and logging. Added Economic products endpoint for fetching product details. Enhanced route parameter validation with length constraints and improved response handling for objects. --- services/nginx/app/classes/economic.php | 8 + services/nginx/app/classes/motorapi.php | 195 ++++++++++++++++++ services/nginx/app/classes/response.php | 2 +- services/nginx/app/interfaces/motorapi_i.php | 95 +++++++++ .../endpoints/economic_products_endpoint.php | 22 ++ .../products/economic_products_endpoint.php | 26 +++ .../nginx/app/objects/motorapi_lookups_o.php | 82 ++++++++ .../nginx/app/routes/departmentsRoute.php | 31 ++- .../nginx/app/routes/moduleEconomicRoute.php | 68 ++++++ .../nginx/app/routes/moduleMotorAPIRoute.php | 43 ++++ services/nginx/app/traits/route_t.php | 42 ++-- 11 files changed, 585 insertions(+), 29 deletions(-) create mode 100644 services/nginx/app/modules/economic/endpoints/economic_products_endpoint.php create mode 100644 services/nginx/app/modules/economic/endpoints/products/economic_products_endpoint.php create mode 100644 services/nginx/app/objects/motorapi_lookups_o.php create mode 100644 services/nginx/app/routes/moduleMotorAPIRoute.php diff --git a/services/nginx/app/classes/economic.php b/services/nginx/app/classes/economic.php index 98553350..801300c5 100644 --- a/services/nginx/app/classes/economic.php +++ b/services/nginx/app/classes/economic.php @@ -7,6 +7,7 @@ require_once WD . '/modules/economic/endpoints/economic_invoices_endpoint.php'; require_once WD . '/modules/economic/endpoints/economic_departments_endpoint.php'; require_once WD . '/modules/economic/endpoints/economic_layouts_endpoint.php'; require_once WD . '/modules/economic/endpoints/economic_customers_endpoint.php'; +require_once WD . '/modules/economic/endpoints/economic_products_endpoint.php'; use economic_c; @@ -15,6 +16,7 @@ use endpoints\economic_departments_endpoint; use endpoints\economic_invoices_endpoint; use endpoints\economic_layouts_endpoint; use endpoints\economic_orders_endpoint; +use endpoints\economic_products_endpoint; use interfaces\economic_i; class economic implements economic_i @@ -49,6 +51,11 @@ class economic implements economic_i * @var economic_customers_endpoint */ public economic_customers_endpoint $customers; + /** + * Any endpoints reached by the /products endpoint + * @var economic_products_endpoint + */ + public economic_products_endpoint $products; public function __construct() @@ -59,5 +66,6 @@ class economic implements economic_i $this->departments = new economic_departments_endpoint(); $this->layouts = new economic_layouts_endpoint(); $this->customers = new economic_customers_endpoint(); + $this->products = new economic_products_endpoint(); } } \ No newline at end of file diff --git a/services/nginx/app/classes/motorapi.php b/services/nginx/app/classes/motorapi.php index d0750c0a..bd214e9c 100644 --- a/services/nginx/app/classes/motorapi.php +++ b/services/nginx/app/classes/motorapi.php @@ -4,8 +4,10 @@ namespace classes; require_once WD . '/modules/motorapi/motorapi_c.php'; +use Exception; use interfaces\motorapi_i; use motorapi\motorapi_c; +use objects\motorapi_lookups_o; class motorapi implements motorapi_i { @@ -15,8 +17,201 @@ class motorapi implements motorapi_i */ public motorapi_c $config; + /** + * API URL + * @var string + */ + private string $api_url = 'https://v1.motorapi.dk/'; + public function __construct() { $this->config = new motorapi_c(); } + + /** + * @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 + */ + function getLicensePlateInformation(string $licensePlate): object + { + // Get the license plate information from the motorapi + return $this->sendRequest($licensePlate, 'vehicles', [], 'GET'); + } + + /** + * @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 + */ + function sendRequest(string $licensePlate, string $endpoint, array $data = [], string $method = 'GET'): object + { + // Validate the module is enabled + self::requireModuleEnabled(); + // Validate the license plate + self::requireValidLicensePlate($licensePlate); + // Validate the daily limit + self::requireDailyLimitNotExceeded(); + // Validate the secret key + self::requireValidSecretKey(); + // Send the request + $response = match ($method) { + 'GET' => self::sendGetRequest($licensePlate, $endpoint, $data), + 'POST' => self::sendPostRequest($licensePlate, $endpoint, $data), + 'PUT' => self::sendPutRequest($licensePlate, $endpoint, $data), + 'DELETE' => self::sendDeleteRequest($licensePlate, $endpoint, $data), + default => throw new Exception('Invalid method'), + }; + // Add the request to the log + self::addRequestToLog($licensePlate, $endpoint, $response); + // Return the response + return $response; + } + + /** + * @inheritDoc + */ + function requireModuleEnabled(): void + { + // Check if the module is enabled + if (!$this->config->enabled->isTrue()) { + throw new Exception('The motorapi module is not enabled'); + } + } + + /** + * @inheritDoc + */ + function requireValidLicensePlate(string $licensePlate): void + { + // Check if the license plate is valid + if (!preg_match('/^[A-Z0-9]{1,10}$/', $licensePlate)) { + throw new Exception('Invalid license plate'); + } + } + + /** + * @inheritDoc + */ + function requireDailyLimitNotExceeded(): void + { + // Check if the daily limit is exceeded + if ($this->getDailyRequestCounter() >= $this->config->daily_limit->getVariableValue()) { + throw new Exception('Daily limit exceeded'); + } + } + + /** + * @inheritDoc + */ + function getDailyRequestCounter(): int + { + // Count the rows from the motorapi request log that was made today + $motorapi_lookups = new motorapi_lookups_o(); + $motorapi_lookups->getTodayCount(); + return $motorapi_lookups->getTodayCount(); + } + + /** + * @inheritDoc + */ + function requireValidSecretKey(): void + { + // Check if the secret key is valid + if ($this->config->secret_key->getVariableValue() === null) { + throw new Exception('Invalid secret key'); + } + } + + /** + * @inheritDoc + * @throws Exception + */ + function sendGetRequest(string $licensePlate, string $endpoint, array $data): object + { + // Build the query string if any data is provided + $query_string = ''; + if (!empty($data)) { + $query_string = '?' . http_build_query($data); + } + + // Initialize cURL session + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $this->api_url . $endpoint . '/' . $licensePlate . $query_string); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + // Set HTTP headers including the X-AUTH-TOKEN + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'X-AUTH-TOKEN: ' . $this->config->secret_key->getVariableValue(), + ]); + + // Execute the request and handle response + $output = curl_exec($ch); + + // Check for errors + if (curl_errno($ch)) { + throw new Exception('cURL error: ' . curl_error($ch)); + } + + curl_close($ch); + // Decode and return JSON response + return json_decode($output); + } + + /** + * @inheritDoc + */ + function sendPostRequest(string $licensePlate, string $endpoint, array $data): object + { + // Send a POST request to the motorapi + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $this->api_url . $endpoint); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data)); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + $output = curl_exec($ch); + curl_close($ch); + return json_decode($output); + } + + /** + * @inheritDoc + */ + function sendPutRequest(string $licensePlate, string $endpoint, array $data): object + { + // Send a PUT request to the motorapi + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $this->api_url . $endpoint); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data)); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + $output = curl_exec($ch); + curl_close($ch); + return json_decode($output); + } + + /** + * @inheritDoc + */ + function sendDeleteRequest(string $licensePlate, string $endpoint, array $data): object + { + // Send a DELETE request to the motorapi + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $this->api_url . $endpoint); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data)); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + $output = curl_exec($ch); + curl_close($ch); + return json_decode($output); + } + + /** + * @inheritDoc + * @throws Exception + */ + function addRequestToLog(string $licensePlate, string $endpoint, object $response): void + { + // Add the request to the motorapi request log + $motorapi_lookups = new motorapi_lookups_o(); + $motorapi_lookups->add($licensePlate, json_encode($response), $endpoint); + } } \ No newline at end of file diff --git a/services/nginx/app/classes/response.php b/services/nginx/app/classes/response.php index 81ef9d97..68947de6 100644 --- a/services/nginx/app/classes/response.php +++ b/services/nginx/app/classes/response.php @@ -27,7 +27,7 @@ class response implements response_i http_response_code($success ? 200 : 400); } // If the data isn't an array, convert it to an array - if (!is_array($data)) { + if (!is_array($data) && !is_object($data)) { $data = ['message' => $data]; } // If the debug mode is enabled, add the debug data to the response diff --git a/services/nginx/app/interfaces/motorapi_i.php b/services/nginx/app/interfaces/motorapi_i.php index 6fed16ad..7d021dcd 100644 --- a/services/nginx/app/interfaces/motorapi_i.php +++ b/services/nginx/app/interfaces/motorapi_i.php @@ -4,5 +4,100 @@ namespace interfaces; interface motorapi_i { + /** + * Get license plate information from the motorapi + * @param string $licensePlate The license plate to get information about (e.g. "AB12345") + * @return object The license plate information + */ + function getLicensePlateInformation(string $licensePlate): object; + /** + * Get the daily request counter + * @return int The daily request counter + */ + function getDailyRequestCounter(): int; + + /** + * Send a request to the motorapi + * @param string $licensePlate The license plate to get information about (e.g. "AB12345") + * @param string $endpoint The endpoint to send the request to (e.g. "vehicle") + * @param array $data The data to send with the request (e.g. ["key" => "value"]) + * @param string $method The method to use for the request (e.g. "GET") + * @return object The response from the motorapi + */ + function sendRequest(string $licensePlate, string $endpoint, array $data, string $method): object; + + /** + * Require the module to be enabled + * @return void + * @throws \Exception If the module is not enabled + */ + function requireModuleEnabled(): void; + + /** + * Require the license plate to be valid + * @param string $licensePlate The license plate to validate + * @return void + * @throws \Exception If the license plate is not valid + */ + function requireValidLicensePlate(string $licensePlate): void; + + /** + * Require the daily limit to not be exceeded + * @return void + * @throws \Exception If the daily limit is exceeded + */ + function requireDailyLimitNotExceeded(): void; + + /** + * Require the secret key to be valid + * @return void + * @throws \Exception If the secret key is not valid + */ + function requireValidSecretKey(): void; + + /** + * Send a GET request to the motorapi + * @param string $licensePlate The license plate to get information about + * @param string $endpoint The endpoint to send the request to + * @param array $data The data to send with the request + * @return object The response from the motorapi + */ + function sendGetRequest(string $licensePlate, string $endpoint, array $data): object; + + /** + * Send a POST request to the motorapi + * @param string $licensePlate The license plate to get information about + * @param string $endpoint The endpoint to send the request to + * @param array $data The data to send with the request + * @return object The response from the motorapi + */ + function sendPostRequest(string $licensePlate, string $endpoint, array $data): object; + + /** + * Send a PUT request to the motorapi + * @param string $licensePlate The license plate to get information about + * @param string $endpoint The endpoint to send the request to + * @param array $data The data to send with the request + * @return object The response from the motorapi + */ + function sendPutRequest(string $licensePlate, string $endpoint, array $data): object; + + /** + * Send a DELETE request to the motorapi + * @param string $licensePlate The license plate to get information about + * @param string $endpoint The endpoint to send the request to + * @param array $data The data to send with the request + * @return object The response from the motorapi + */ + function sendDeleteRequest(string $licensePlate, string $endpoint, array $data): object; + + /** + * Add a request to the log + * @param string $licensePlate The license plate to get information about + * @param string $endpoint The endpoint to send the request to + * @param object $response The response from the motorapi + * @return void + */ + function addRequestToLog(string $licensePlate, string $endpoint, object $response): void; } \ No newline at end of file diff --git a/services/nginx/app/modules/economic/endpoints/economic_products_endpoint.php b/services/nginx/app/modules/economic/endpoints/economic_products_endpoint.php new file mode 100644 index 00000000..427e4b85 --- /dev/null +++ b/services/nginx/app/modules/economic/endpoints/economic_products_endpoint.php @@ -0,0 +1,22 @@ +products = new products\economic_products_endpoint(); + } +} \ No newline at end of file diff --git a/services/nginx/app/modules/economic/endpoints/products/economic_products_endpoint.php b/services/nginx/app/modules/economic/endpoints/products/economic_products_endpoint.php new file mode 100644 index 00000000..0cd9648f --- /dev/null +++ b/services/nginx/app/modules/economic/endpoints/products/economic_products_endpoint.php @@ -0,0 +1,26 @@ + 'value'] + * @param array $pagination Example: ['maxPageSize' => 100, 'skipPages' => 0] + * @return object {collection: [product], pagination: {maxPageSize: number, skipPages: number, results: number}} + */ + public function get(array $filters = [], array $pagination = []): object + { + $response = $this->send_request( + '/products/?filter=' . self::filters($filters) . self::pagination($pagination), + 'GET'); + // Return the response as an object + return json_decode($response); + } + +} \ No newline at end of file diff --git a/services/nginx/app/objects/motorapi_lookups_o.php b/services/nginx/app/objects/motorapi_lookups_o.php new file mode 100644 index 00000000..825fa9fa --- /dev/null +++ b/services/nginx/app/objects/motorapi_lookups_o.php @@ -0,0 +1,82 @@ +setTable('motorapi_lookups'); + } + + /** + * Add a motorapi lookup + * @param string $license_plate + * @param string $result + * @param string $endpoint + * @return void + * @throws Exception If the object was not created successfully + */ + public function add(string $license_plate, string $result, string $endpoint): void + { + $tmp_id = self::add_object([ + 'license_plate' => $license_plate, + 'result' => $result, + 'endpoint' => $endpoint + ]); + $this->id = $tmp_id; + self::getObjectProperties(); + self::objectChanged(); + } + + public function getObjectProperties(): void + { + $this->license_plate = new object_property($this->table, $this->id, 'license_plate', 'string', false); + $this->result = new object_property($this->table, $this->id, 'result', 'string', false); + $this->endpoint = new object_property($this->table, $this->id, 'endpoint', 'string', false); + $this->created_at = new object_property($this->table, $this->id, 'created_at', 'datetime', false); + } + + public function objectChanged(): void + { + //TODO: Add cache invalidation + } + + /** + * Get the amount of motorapi lookups created today + * @return int + */ + public function getTodayCount(): int + { + global /** @var db $db */ + $db; + $sql = 'SELECT COUNT(*) FROM ' . $this->table . ' WHERE DATE(created_at) = CURDATE()'; + $result = $db->query($sql); + return (int)$result->fetch_row()[0]; + } + + public function asArray(): array + { + return [ + 'id' => (int)$this->id, + 'license_plate' => (string)$this->license_plate->value(), + 'result' => (string)$this->result->value(), + 'endpoint' => (string)$this->endpoint->value(), + 'created_at' => (string)$this->created_at->value(), + ]; + } + +} \ No newline at end of file diff --git a/services/nginx/app/routes/departmentsRoute.php b/services/nginx/app/routes/departmentsRoute.php index 772075d6..0385fa0d 100644 --- a/services/nginx/app/routes/departmentsRoute.php +++ b/services/nginx/app/routes/departmentsRoute.php @@ -97,30 +97,29 @@ class departmentsRoute $this->put('/departments', function () { // Require the user to be logged in global $response; - $this->requirePermission('edit_department'); + self::requirePermission('edit_department'); // Get the user object $user = (new authentication())->get_user(); // Check if the request was successful if ($user) { - // Get the post data - $data = json_decode(file_get_contents('php://input'), true); - // Check if the required fields are set - if (!isset($data['id'])) { - $response->error('ID is required', 400); + // Require the required fields + self::requireParameters(['id']); + // Validate the required fields + self::requireType((int)self::getParameter('id'), self::TYPE_INT()); + // Get the department object + $department = (new departments_o())->select(self::getParameter('id')); + // Update the fields provided + if (self::isParametersSet(['name'])) { + $department->name->set(self::getParameter('name')); } - if (!isset($data['name'])) { - $response->error('Name is required', 400); + if (self::isParametersSet(['description'])) { + $department->description->set(self::getParameter('description')); } - if (!isset($data['description'])) { - $response->error('Description is required', 400); + if (self::isParametersSet(['economic_department_id'])) { + $department->economic_department_id->set(self::getParameter('economic_department_id')); } - if (!isset($data['economic_department_id'])) { - $response->error('Economic department ID is required', 400); - } - // Update the department - (new departments_o())->edit($data['id'], $data['name'], $data['description'], (int)$data['economic_department_id']); // Log the incident - (new logs_o())->add('departments', $data['id'], 1, $user->id, 'EDIT_DEPARTMENT', 'Successfully updated a department ' . $data['name']); + (new logs_o())->add('departments', (int)self::getParameter('id'), 1, $user->id, 'EDIT_DEPARTMENT', 'Successfully edited a department'); // Return a success message $response->success(['message' => 'Department updated successfully']); } else { diff --git a/services/nginx/app/routes/moduleEconomicRoute.php b/services/nginx/app/routes/moduleEconomicRoute.php index 574d7772..8643bac8 100644 --- a/services/nginx/app/routes/moduleEconomicRoute.php +++ b/services/nginx/app/routes/moduleEconomicRoute.php @@ -6,6 +6,7 @@ use classes\authentication; use classes\economic; use classes\response; use classes\router; +use objects\departments_o; use objects\logs_o; use objects\users_o; use traits\route_t; @@ -57,5 +58,72 @@ class moduleEconomicRoute $response->error('Invalid session', 400); } }); + + /** Economic > Departments > GET */ + self::get('/economic/departments', function () { + // Require permission + global /** @var response $response */ + $response; + self::requirePermission('economic_departments_get'); + // Get the user + $user = (new authentication())->get_user(); + // Check if the user is valid + if ($user->exists()) { + // Get the departments + $economic = new economic(); + $departments = $economic->departments->departments->get()->collection; + $departments_o = new departments_o(); + // Parse the departments + $departments = array_map(function ($department) use ($departments_o) { + return [ + 'id' => $department->departmentNumber, // The department number is the ID in this case, this is added for consistency with the rest of the system. - Making it easier to use the department number as the ID, when selecting a department to link (FE). + 'name' => $department->name, + 'departmentNumber' => $department->departmentNumber + ]; + }, $departments); + // Return the departments + $response->success($departments); + } else { + // Log the error + (new logs_o())->add('economic', 'global', 1, 0, 'ECONOMIC_DEPARTMENTS', 'No user found, or invalid session'); + // Return the error + $response->error('Invalid session', 400); + } + }); + + /** Economic > Products > GET */ + self::get('/economic/products', function () { + // Require permission + global /** @var response $response */ + $response; + self::requirePermission('economic_products_get'); + // Get the user + $user = (new authentication())->get_user(); + // Check if the user is valid + if ($user->exists()) { + // Get the products + $economic = new economic(); + $products = $economic->products->products->get([], [ + 'skipPages' => 0, + 'pageSize' => 1000, // Since the limit is 1000, we need to set the page size to 1000. + ])->collection; + // Parse the products + $products = array_map(function ($product) { + return [ + 'id' => (int)$product->productNumber, + 'name' => $product->name . ' (' . $product->productNumber . ')', + 'price' => (int)$product->salesPrice, + 'productNumber' => (int)$product->productNumber + ]; + }, $products); + // Return the products + $response->success($products); + } else { + // Log the error + (new logs_o())->add('economic', 'global', 1, 0, 'ECONOMIC_PRODUCTS', 'No user found, or invalid session'); + // Return the error + $response->error('Invalid session', 400); + } + }); } } \ No newline at end of file diff --git a/services/nginx/app/routes/moduleMotorAPIRoute.php b/services/nginx/app/routes/moduleMotorAPIRoute.php new file mode 100644 index 00000000..7db518bc --- /dev/null +++ b/services/nginx/app/routes/moduleMotorAPIRoute.php @@ -0,0 +1,43 @@ + MotorAPI > Lookup > GET */ + $this->get('/modules/motorapi/lookup', function () { + global $response; + self::requirePermission('modules_motorapi_lookup'); + $user = (new authentication())->get_user(); + if ($user) { + self::requireParameters(['license_plate']); + self::requireType('license_plate', 'string'); + self::requireMinLength('license_plate', 1); + self::requireMaxLength('license_plate', 10); + (new logs_o())->add('modules_motorapi', 'global', 1, $user->id, 'MODULES_MOTORAPI', 'Successfully looked up license plate information'); + $result = (new motorapi())->getLicensePlateInformation($this->fromRequest('license_plate')); + $response->success((object)$result); + } else { + (new logs_o())->add('modules_motorapi', 'global', 1, 0, 'MODULES_MOTORAPI', 'No user found, or invalid session'); + $response->error('Invalid session', 400); + } + }); + + } +} \ No newline at end of file diff --git a/services/nginx/app/traits/route_t.php b/services/nginx/app/traits/route_t.php index 76d61199..ce8e2202 100644 --- a/services/nginx/app/traits/route_t.php +++ b/services/nginx/app/traits/route_t.php @@ -43,18 +43,6 @@ trait route_t return 'integer'; } - /** - * Get the parameter from the request by name - * This is a shorthand for the response class method - * @param string $parameter - * @return mixed|null - */ - public function getParameter(string $parameter): mixed - { - global $response; - return $response->getRequestParameter($parameter); - } - public function requireParameters(array $parameters): void { global $response; @@ -70,6 +58,36 @@ trait route_t } } + public function requireMinLength(string $parameter, int $length): void + { + global $response; + $value = self::getParameter($parameter); + if (strlen($value) < $length) { + $response->error('Parameter ' . $parameter . ' must be at least ' . $length . ' characters long', 400); + } + } + + /** + * Get the parameter from the request by name + * This is a shorthand for the response class method + * @param string $parameter + * @return mixed|null + */ + public function getParameter(string $parameter): mixed + { + global $response; + return $response->getRequestParameter($parameter); + } + + public function requireMaxLength(string $parameter, int $length): void + { + global $response; + $value = self::getParameter($parameter); + if (strlen($value) > $length) { + $response->error('Parameter ' . $parameter . ' must be at most ' . $length . ' characters long', 400); + } + } + public function isParametersSet(array $parameters): bool { global $response;