From e0f582289fb1d3d14e577098eab7ab50e995a0c3 Mon Sep 17 00:00:00 2001 From: Jepp9350 <2jepp9350@gmail.com> Date: Thu, 26 Jun 2025 16:55:37 +0200 Subject: [PATCH 1/2] Refactor `orders_o`, `InvoicingPeriodRoute`, and `object_property`: enhance ID validation, streamline customer processing, and improve method structures for better data handling. --- services/nginx/app/classes/object_property.php | 14 ++++++++++++++ services/nginx/app/objects/orders_o.php | 9 +++------ services/nginx/app/routes/InvoicingPeriodRoute.php | 7 +++++++ 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/services/nginx/app/classes/object_property.php b/services/nginx/app/classes/object_property.php index 9b74b235..82150235 100644 --- a/services/nginx/app/classes/object_property.php +++ b/services/nginx/app/classes/object_property.php @@ -35,6 +35,20 @@ class object_property { // Get the value of the field in the database table global $db; + // Sanitize the id to prevent SQL injection + $this->id = (int)$this->id; + // Check if the id is valid + if ($this->id <= 0) { + throw new \InvalidArgumentException("Invalid ID: $this->id"); + } + // Check if the table and column are valid + if (empty($this->table) || empty($this->column)) { + throw new \InvalidArgumentException("Table or column cannot be empty"); + } + // Check if the type is valid + if (!in_array($this->type, ['int', 'varchar', 'text', 'date', 'datetime', 'string', 'timestamp'])) { + throw new \InvalidArgumentException("Invalid type: $this->type"); + } $sql = "SELECT $this->column FROM $this->table WHERE id = $this->id"; $result = $db->query($sql); $row = $db->fetch_assoc($result); diff --git a/services/nginx/app/objects/orders_o.php b/services/nginx/app/objects/orders_o.php index bea53599..29e62e25 100644 --- a/services/nginx/app/objects/orders_o.php +++ b/services/nginx/app/objects/orders_o.php @@ -674,9 +674,11 @@ class orders_o extends db */ public function isBooked(): bool { + $this->requireSelected(); // 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(); } else { // If there is no invoice collection, the order is not booked @@ -825,16 +827,11 @@ class orders_o extends db $result = $db->query($sql); $customer_numbers = []; while ($row = $result->fetch_assoc()) { - // Trim and cast the customer_id to an integer - $customer_number = (int)trim($row['customer_id']); - if ($customer_number > 0) { - $customer_numbers[] = $customer_number; // Only add valid customer numbers - } + $customer_numbers[] = (int)trim($row['customer_id']); } if (empty($customer_numbers)) { return []; // No customers found in the date range } - // Get the customers by their customer numbers return (new users_o())->getUsersByCustomerNumbers($customer_numbers); } diff --git a/services/nginx/app/routes/InvoicingPeriodRoute.php b/services/nginx/app/routes/InvoicingPeriodRoute.php index f4cb8170..840f40c9 100644 --- a/services/nginx/app/routes/InvoicingPeriodRoute.php +++ b/services/nginx/app/routes/InvoicingPeriodRoute.php @@ -221,8 +221,15 @@ class InvoicingPeriodRoute { // Define the customers with orders in the specified date range $customers = (new orders_o)->getCustomersWithOrdersInDateRange($dateFrom, $dateTo); + $customer_numbers_processed = []; $tmp = []; foreach ( $customers as $customer ) { + if (in_array((int)$customer->customer_number->value(), $customer_numbers_processed)) { + // Skip if the customer has already been processed + continue; + } + $customer_numbers_processed[] = (int)$customer->customer_number->value(); + // Construct the customer object with transactions $tmp[] = self::constructCustomerObject( (int)$customer->customer_number->value(), (new \objects\users_o())->getCustomerName((int)$customer->customer_number->value()), From 6b80637208e5e8dfc512497dff6e1a8cdb4188d4 Mon Sep 17 00:00:00 2001 From: Jepp9350 <2jepp9350@gmail.com> Date: Mon, 30 Jun 2025 14:37:37 +0200 Subject: [PATCH 2/2] Refactor `InvoicingPeriodRoute` and `vehiclesRoute`: streamline invoicing period data retrieval, enhance transaction handling across customer types, introduce new methods for efficient object construction, and add improved type validation in `object_property`. --- .../nginx/app/classes/object_property.php | 5 +- services/nginx/app/objects/users_o.php | 20 ++ .../nginx/app/routes/InvoicingPeriodRoute.php | 324 +++++++++++------- services/nginx/app/routes/vehiclesRoute.php | 3 + 4 files changed, 224 insertions(+), 128 deletions(-) diff --git a/services/nginx/app/classes/object_property.php b/services/nginx/app/classes/object_property.php index 82150235..2f02d822 100644 --- a/services/nginx/app/classes/object_property.php +++ b/services/nginx/app/classes/object_property.php @@ -46,12 +46,15 @@ class object_property throw new \InvalidArgumentException("Table or column cannot be empty"); } // Check if the type is valid - if (!in_array($this->type, ['int', 'varchar', 'text', 'date', 'datetime', 'string', 'timestamp'])) { + if (!in_array($this->type, ['int', 'varchar', 'text', 'date', 'datetime', 'string', 'timestamp', 'boolean', 'bool'])) { throw new \InvalidArgumentException("Invalid type: $this->type"); } $sql = "SELECT $this->column FROM $this->table WHERE id = $this->id"; $result = $db->query($sql); $row = $db->fetch_assoc($result); + if (empty($row)) { + throw new \RuntimeException("No record found for column '$this->column' in table '$this->table' with ID $this->id"); + } return $row[$this->column]; } diff --git a/services/nginx/app/objects/users_o.php b/services/nginx/app/objects/users_o.php index 7d859e4b..c5232f82 100644 --- a/services/nginx/app/objects/users_o.php +++ b/services/nginx/app/objects/users_o.php @@ -1239,4 +1239,24 @@ class users_o extends db return !empty($this->xlvask_customer_id->value()); } + public function getCustomersWithSpecialArrangements(): array + { + global $db; + $sql = "SELECT DISTINCT c.customer_number + FROM users c + INNER JOIN user_key_value_pairs kv ON c.id = kv.user_id + WHERE kv.var = 'OtherSpecialArrangement' + AND kv.val IS NOT NULL + AND kv.val != ''"; + + $result = $db->query($sql); + $customer_numbers = []; + + while ($row = $result->fetch_assoc()) { + $customer_numbers[] = (int)$row['customer_number']; + } + + return $customer_numbers; + } + } \ No newline at end of file diff --git a/services/nginx/app/routes/InvoicingPeriodRoute.php b/services/nginx/app/routes/InvoicingPeriodRoute.php index 840f40c9..946872f4 100644 --- a/services/nginx/app/routes/InvoicingPeriodRoute.php +++ b/services/nginx/app/routes/InvoicingPeriodRoute.php @@ -7,6 +7,7 @@ use Exception; use objects\customer_vehicles_o; use objects\logs_o; use objects\orders_o; +use objects\users_o; use traits\route_t; class InvoicingPeriodRoute @@ -33,6 +34,8 @@ class InvoicingPeriodRoute // Require the dateFrom and dateTo parameters to be valid dates self::requireDateFormat($dateFrom, 'Y-m-d'); self::requireDateFormat($dateTo, 'Y-m-d'); + // Add a day to the dateTo parameter to include the end date in the range + $dateTo = date('Y-m-d', strtotime($dateTo . ' +1 day')); // Get the invoicing period for the user $response->success([...self::getInvoicingPeriod($dateFrom, $dateTo)]); // Log the incident @@ -68,14 +71,17 @@ class InvoicingPeriodRoute */ private static function getInvoicingPeriod(string $dateFrom, string $dateTo): array { + $customersWithTransactions = self::getCustomersWithTransactions($dateFrom, $dateTo); return [ 'dateFrom' => $dateFrom, 'dateTo' => $dateTo, 'types' => [ - 'vehicle_subscriptions' => self::getVehicleSubscriptions($dateFrom, $dateTo), - 'fixed_pricing' => self::getFixedPricing($dateFrom, $dateTo), - 'tank_cleaning' => self::getTankCleaning($dateFrom, $dateTo), - 'all' => self::getCustomersWithTransactions($dateFrom, $dateTo), + 'vehicle_subscriptions' => self::getVehicleSubscriptions($dateFrom, $dateTo, $customersWithTransactions), + 'fixed_pricing' => self::getFixedPricing($dateFrom, $dateTo, $customersWithTransactions), + 'tank_cleaning' => self::getTankCleaning($dateFrom, $dateTo, $customersWithTransactions), + 'special_arrangements' => self::getSpecialArrangements($dateFrom, $dateTo, $customersWithTransactions), + 'invoice_per_order' => self::getInvoicingPerOrder($dateFrom, $dateTo, $customersWithTransactions), + 'all' => $customersWithTransactions, ], ]; } @@ -83,48 +89,134 @@ class InvoicingPeriodRoute /** * @throws Exception */ - private static function getVehicleSubscriptions(string $dateFrom, string $dateTo): array + private static function getCustomersWithTransactions(string $dateFrom, string $dateTo): array { - // Get all customers with vehicle subscriptions + // Define the customers with orders in the specified date range + $customers = (new orders_o)->getCustomersWithOrdersInDateRange($dateFrom, $dateTo); + $customer_numbers_processed = []; + $tmp = []; + foreach ( $customers as $customer ) { + if (in_array((int)$customer->customer_number->value(), $customer_numbers_processed)) { + // Skip if the customer has already been processed + continue; + } + $customer_numbers_processed[] = (int)$customer->customer_number->value(); + // Construct the customer object with transactions + $tmp[] = self::constructCustomerObject( + (int)$customer->customer_number->value(), + (new \objects\users_o())->getCustomerName((int)$customer->customer_number->value()), + (new \objects\orders_o())->getTransactionsForCustomer( + (int)$customer->customer_number->value(), + $dateFrom, + $dateTo + ), + false, + (int)$customer->id + ); + } + return $tmp; + } + + /** + * @param int $customer_number + * @param string $customer_name + * @param orders_o[] $transactions + * @param bool $requires_action + * @return array + * @throws Exception + */ + protected static function constructCustomerObject( + int $customer_number, + string $customer_name, + array $transactions = [], + bool $requires_action = false, + ?int $user_id = null + ): array + { + return [ + 'id' => $user_id ?? (new users_o())->getUserByCustomerNumber((int)$customer_number)->id, + 'customer_number' => $customer_number, + 'customer_name' => $customer_name, + 'transactions' => $parsed_transactions = array_map(function ($transaction) { + return self::constructTransactionObject($transaction); + }, $transactions), + 'requires_action' => self::checkRequiresAction($parsed_transactions, $requires_action), + ]; + } + + /** + * @throws Exception + */ + private static function constructTransactionObject(orders_o $transaction): array + { + return [ + 'id' => $transaction->id, + 'date' => $transaction->created_at->value(), + 'amount' => $transaction->getNetAmount(), + 'booked' => $transaction->isBooked(), + ]; + } + + private static function checkRequiresAction(array $parsed_transactions, bool $requires_action): bool + { + // If requires_action is already set to true, return true + if ($requires_action) { + return true; + } + // Check if any transaction is not booked + foreach ( $parsed_transactions as $transaction ) { + if (!$transaction['booked']) { + return true; + } + } + // If all transactions are booked, return false + return false; + } + + /** + * @throws Exception + */ + private static function getVehicleSubscriptions(string $dateFrom, string $dateTo, array $customersWithTransactions = null): array + { + // If customersWithTransactions is not provided, get all customers with transactions in the specified date range + if ($customersWithTransactions === null) { + $customersWithTransactions = self::getCustomersWithTransactions($dateFrom, $dateTo); + } + // Since these are monthly subscriptions, we don't need to filter by transactions $customer_numbers = (new \objects\users_o())->getCustomersWithVehicleSubscriptions(); + // Get all customers with vehicle subscriptions $subscriptions = []; /** @var int $customer_number */ foreach ( $customer_numbers as $customer_number ) { - $tmp_subscription = [ - 'customer_number' => $customer_number, - 'customer_name' => (new \objects\users_o())->getCustomerName($customer_number), - 'transactions' => [], - 'requires_action' => true, - ]; - // Check if the customer has a vehicle subscription order within the date range - $orders = (new \objects\orders_o())->getWashSubscriptionTransactions($customer_number, false, $dateFrom, $dateTo); - /** @var orders_o $order */ - foreach ( $orders as $order ) { - //echo "Checking order {$order->id} for customer {$customer_number}...\n"; - // Add the order to the subscription transactions - $tmp_subscription['transactions'][] = [ - 'id' => $order->id, - ]; - } - // If there are transactions, set requires_action to false - if (count($tmp_subscription['transactions']) > 0) { - // Check if there's any transaction that has not been booked yet - $requires_action = false; - foreach ( $tmp_subscription['transactions'] as $transaction ) { - $order = (new orders_o())->select((int)$transaction['id']); - if (!$order->isBooked()) { - $requires_action = true; - break; - } - } - $tmp_subscription['requires_action'] = $requires_action; - } - // Add the subscription to the list if it has transactions - $subscriptions[] = $tmp_subscription; + $subscriptions[] = (self::getCustomerFromList((int)$customer_number, $customersWithTransactions)) ?? self::constructCustomerObject( + (int)$customer_number, + (new \objects\users_o())->getCustomerName((int)$customer_number) ?? 'Unknown Customer', + [], + true + ); } return $subscriptions; } + /** + * Get the customer from the list of customers with transactions. + * + * @param int $customer_number The customer number to search for. + * @param array $customersWithTransactions The list of customers with transactions. + * @return array|null The customer object if found, null otherwise. + */ + private static function getCustomerFromList(int $customer_number, array $customersWithTransactions): ?array + { + // Search for the customer in the list of customers with transactions + foreach ( $customersWithTransactions as $customer ) { + if ($customer['customer_number'] === $customer_number) { + return $customer; + } + } + // If the customer is not found, return null + return null; + } + /** * @throws Exception */ @@ -137,7 +229,7 @@ class InvoicingPeriodRoute foreach ( $customer_numbers as $customer_number ) { $tmp_fixed_pricing = [ 'customer_number' => $customer_number, - 'customer_name' => (new \objects\users_o())->getCustomerName($customer_number), + 'customer_name' => (new \objects\users_o())->getCustomerName($customer_number) ?? 'Unknown Customer', 'transactions' => [], 'requires_action' => true, ]; @@ -173,119 +265,97 @@ class InvoicingPeriodRoute /** * @throws Exception */ - private static function getTankCleaning(string $dateFrom, string $dateTo): array + private static function getTankCleaning(string $dateFrom, string $dateTo, array $customersWithTransactions = null): array { - // Get all customers with tank cleaning + // If customersWithTransactions is not provided, get all customers with transactions in the specified date range + if ($customersWithTransactions === null) { + $customersWithTransactions = self::getCustomersWithTransactions($dateFrom, $dateTo); + } + // Filter out customers that do not have any transactions in the specified date range $customer_numbers = (new \objects\users_o())->getCustomersWithTankCleaning(); + self::filterCustomersWithTransactions($customer_numbers, $customersWithTransactions); + // Get all customers with tank cleaning $tank_cleaning = []; /** @var int $customer_number */ foreach ( $customer_numbers as $customer_number ) { - $tmp_tank_cleaning = [ - 'customer_number' => $customer_number, - 'customer_name' => (new \objects\users_o())->getCustomerName($customer_number), - 'transactions' => [], - 'requires_action' => false, - ]; - // Check if the customer has a tank cleaning order within the date range - $orders = (new \objects\orders_o())->getTankCleaningTransactions($customer_number, false, $dateFrom, $dateTo); - /** @var orders_o $order */ - foreach ( $orders as $order ) { - // Add the order to the tank cleaning transactions - $tmp_tank_cleaning['transactions'][] = [ - 'id' => $order->id, - ]; - } - // If there are transactions, set requires_action to false - if (count($tmp_tank_cleaning['transactions']) > 0) { - // Check if there's any transaction that has not been booked yet - $requires_action = false; - foreach ( $tmp_tank_cleaning['transactions'] as $transaction ) { - $order = (new orders_o())->select($transaction['id']); - if (!$order->isBooked()) { - $requires_action = true; - break; - } - } - $tmp_tank_cleaning['requires_action'] = $requires_action; - } + $tank_cleaning[] = self::getCustomerFromList( + (int)$customer_number, + $customersWithTransactions + ); // Add the tank cleaning to the list if it has transactions - $tank_cleaning[] = $tmp_tank_cleaning; } return $tank_cleaning; } /** - * @throws Exception + * Filters the customer numbers based on whether they have transactions in the specified date range. + * + * @param array $customer_numbers The customer numbers to filter. + * @param array $customersWithTransactions The customers with transactions in the specified date range. */ - private static function getCustomersWithTransactions(string $dateFrom, string $dateTo): array + private static function filterCustomersWithTransactions(array &$customer_numbers, array $customersWithTransactions): void { - // Define the customers with orders in the specified date range - $customers = (new orders_o)->getCustomersWithOrdersInDateRange($dateFrom, $dateTo); - $customer_numbers_processed = []; - $tmp = []; - foreach ( $customers as $customer ) { - if (in_array((int)$customer->customer_number->value(), $customer_numbers_processed)) { - // Skip if the customer has already been processed - continue; + // Filter out customers that do not have any transactions in the specified date range + $customer_numbers = array_filter($customer_numbers, function ($customer_number) use ($customersWithTransactions) { + // Check if the customer has any transactions in the specified date range + foreach ( $customersWithTransactions as $customer ) { + if ($customer['customer_number'] === $customer_number) { + return true; + } } - $customer_numbers_processed[] = (int)$customer->customer_number->value(); - // Construct the customer object with transactions - $tmp[] = self::constructCustomerObject( - (int)$customer->customer_number->value(), - (new \objects\users_o())->getCustomerName((int)$customer->customer_number->value()), - (new \objects\orders_o())->getTransactionsForCustomer( - (int)$customer->customer_number->value(), - $dateFrom, - $dateTo - ), - ); - } - return $tmp; + return false; + }); } /** - * @param int $customer_number - * @param string $customer_name - * @param orders_o[] $transactions - * @param bool $requires_action - * @return array * @throws Exception */ - protected static function constructCustomerObject( - int $customer_number, - string $customer_name, - array $transactions = [], - bool $requires_action = false - ): array + private static function getSpecialArrangements(string $dateFrom, string $dateTo, array $customersWithTransactions = null): array { - return [ - 'customer_number' => $customer_number, - 'customer_name' => $customer_name, - 'transactions' => $parsed_transactions = array_map(function ($transaction) { - return [ - 'id' => $transaction->id, - 'date' => $transaction->created_at->value(), - 'amount' => $transaction->getNetAmount(), - 'booked' => $transaction->isBooked(), - ]; - }, $transactions), - 'requires_action' => self::checkRequiresAction($parsed_transactions, $requires_action), - ]; + // If customersWithTransactions is not provided, get all customers with transactions in the specified date range + if ($customersWithTransactions === null) { + $customersWithTransactions = self::getCustomersWithTransactions($dateFrom, $dateTo); + } + // Get all customers with tank cleaning + $customer_numbers = (new \objects\users_o())->getCustomersWithSpecialArrangements(); + // Filter out customers that do not have any transactions in the specified date range + self::filterCustomersWithTransactions($customer_numbers, $customersWithTransactions); + $special_arrangements = []; + /** @var int $customer_number */ + foreach ( $customer_numbers as $customer_number ) { + $special_arrangements[] = self::constructCustomerObject( + (int)$customer_number, + (new \objects\users_o())->getCustomerName((int)$customer_number) ?? 'Unknown Customer', + (new orders_o())->getTransactionsForCustomer( + (int)$customer_number, + $dateFrom, + $dateTo, + ), + false, + ); + } + return $special_arrangements; } - private static function checkRequiresAction(array $parsed_transactions, bool $requires_action): bool + /** + * @throws Exception + */ + private static function getInvoicingPerOrder(string $dateFrom, string $dateTo, array $customersWithTransactions = null): array { - // If requires_action is already set to true, return true - if ($requires_action) { - return true; + // Get all customers with the invoicing per order attribute + $customer_numbers = (new users_o())->getCustomerNumbersWithAttributes(['invoiceAllOrdersIndividually']); + // Filter out customers that do not have any transactions in the specified date range + if ($customersWithTransactions === null) { + $customersWithTransactions = self::getCustomersWithTransactions($dateFrom, $dateTo); } - // Check if any transaction is not booked - foreach ( $parsed_transactions as $transaction ) { - if (!$transaction['booked']) { - return true; - } + self::filterCustomersWithTransactions($customer_numbers, $customersWithTransactions); + + $invoicing_per_order = []; + /** @var int $customer_number */ + foreach ( $customer_numbers as $customer_number ) { + // Add the invoicing per order to the list + $invoicing_per_order[] = self::getCustomerFromList((int)$customer_number, $customersWithTransactions); } - // If all transactions are booked, return false - return false; + return $invoicing_per_order; } } \ No newline at end of file diff --git a/services/nginx/app/routes/vehiclesRoute.php b/services/nginx/app/routes/vehiclesRoute.php index 432f300d..a990f3c2 100644 --- a/services/nginx/app/routes/vehiclesRoute.php +++ b/services/nginx/app/routes/vehiclesRoute.php @@ -278,6 +278,8 @@ class vehiclesRoute $vehicle->type->set( (int)$type ); + // TODO: Make the potential vehicleTypeId reflect the correct type. + // It's currently possible to set the vehicleTypeId to a type that is not allowed for the vehicle. } } if (self::isParametersSet(['reg'])) { @@ -314,6 +316,7 @@ class vehiclesRoute $vehicle->reference->set($reference); } } + $vehicle->objectChanged(); // Return the vehicle $response->success( $vehicle->asArray()