From 9ee486a49e5b64b516feba7615a39684398b3b35 Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Mon, 15 Sep 2025 15:09:36 +0200 Subject: [PATCH] Add `/vehicles/search` route for vehicle registration search - Introduced a new route `/vehicles/search` to enable searching for vehicles by registration number across multiple categories (e.g., verified, known, booked, unknown). - Added validation for search parameters, including length and format checks. - Ensured proper classification of vehicles with clear prioritization and deduplication logic. - Implemented helper methods to retrieve matching registrations from various sources like bookings, orders, and scanned plates. - Enhanced response format to include vehicle status, customer details, and references for improved usability. --- services/nginx/app/routes/ordersRoute.php | 23 ++ services/nginx/app/routes/vehiclesRoute.php | 222 ++++++++++++++++++++ services/nginx/app/traits/db_object_t.php | 20 ++ 3 files changed, 265 insertions(+) diff --git a/services/nginx/app/routes/ordersRoute.php b/services/nginx/app/routes/ordersRoute.php index e5298bd6..378bedaf 100644 --- a/services/nginx/app/routes/ordersRoute.php +++ b/services/nginx/app/routes/ordersRoute.php @@ -223,6 +223,29 @@ class ordersRoute if (isset($data['reg_1'])) { $order->reg_1->set($data['reg_1']); } + // If the registration number 2 is set, validate it + if (isset($data['reg_2'])) { + $order->reg_2->set($data['reg_2']); + } + // If the registration number 3 is set, validate it + if (isset($data['reg_3'])) { + $order->reg_3->set($data['reg_3']); + } + // If the lane is set, validate it + if (isset($data['lane'])) { + $order->lane->set((int)$data['lane']); + } + // If the department ID is set, validate it + if (isset($data['department_id'])) { + if (!(new departments_o())->getDepartmentById((int)$data['department_id'])) { + $response->error('Department not found', 400); + } + $order->department_id->set((int)$data['department_id']); + } + // If the booking ID is set, validate it + if (isset($data['booking_id'])) { + $order->booking_id->set((int)$data['booking_id']); + } // Check if the invoice collection is set if (isset($data['invoice_collection_id'])) { $order->invoice_collection_id->set((int)$data['invoice_collection_id']); diff --git a/services/nginx/app/routes/vehiclesRoute.php b/services/nginx/app/routes/vehiclesRoute.php index ec2c5e4f..543a853a 100644 --- a/services/nginx/app/routes/vehiclesRoute.php +++ b/services/nginx/app/routes/vehiclesRoute.php @@ -9,6 +9,7 @@ use objects\customer_vehicles_o; use objects\departments_o; use objects\logs_o; use objects\orders_o; +use objects\plate_scans_o; use objects\products_o; use objects\users_o; use traits\route_t; @@ -715,6 +716,227 @@ class vehiclesRoute 'view_vehicle_status' => 'View vehicle status (Verified, Known, Unknown, Booked, Card payment required, etc.)', ] ); + + $this->get('/vehicles/search', function () { + global $response; + $this->requirePermission('search_vehicles'); + /** + * This endpoint allows searching for vehicles by registration number. + * This includes: + * - verified vehicles (in customer_vehicles) + * - known vehicles (in orders). + * - booked vehicles (in bookings). + * - unknown vehicles (not in any of the above). + */ + self::requireParameters(['search']); + self::requireType((string)self::getParameter('search'), self::type_string()); + $search = (string)self::getParameter('search'); + self::requireMinLength('search', 1); + self::requireMaxLength('search', 12); + // Strip the search term of whitespace + $search = preg_replace('/\s+/', '', $search); + /** Get the different lists of registration numbers */ + $options = ['limit' => 10]; + $verified_regs = self::getVerifiedRegs($search, $options); + $known_regs = self::getKnownRegs($search, $options); + $booked_regs = self::getBookedRegs($search, $options); + $unknown_regs = array_filter(array_unique(array_merge([...self::getUnknownRegs($search, $options), $search]))); // Add the search term as the first item, to ensure it is included in the results + /** Prevent overlaps between the lists + * - Booked vehicles should not be in known, unknown or verified + * - Verified vehicles should not be in known or booked + * - Known vehicles should not be in unknown + * - Unknown vehicles should not be in any other list + */ + $known_regs = array_filter($known_regs, function ($reg) use ($verified_regs, $booked_regs) { + return !in_array($reg, $verified_regs) && !in_array($reg, $booked_regs); + }); + $verified_regs = array_filter($verified_regs, function ($reg) use ($booked_regs) { + return !in_array($reg, $booked_regs); + }); + $unknown_regs = array_filter($unknown_regs, function ($reg) use ($verified_regs, $known_regs, $booked_regs) { + return !in_array($reg, $verified_regs) && !in_array($reg, $known_regs) && !in_array($reg, $booked_regs); + }); + // Sort the lists alphabetically + sort($verified_regs); + sort($known_regs); + sort($booked_regs); + sort($unknown_regs); + // Define the custom relevance sorting function + $relevant = [ + ...array_values(array_unique($booked_regs)), + ...array_values(array_unique($verified_regs)), + ...array_values(array_unique($known_regs)), + ...array_values(array_unique($unknown_regs)), + ]; + // Limit the results to 10 items total, while keeping the relevance order + $vehicles = array_slice($relevant, 0, 10); + /** Format the relevance results */ + $vehicles = array_map(function ($reg) use ($verified_regs, $known_regs, $booked_regs, $unknown_regs) { + // Determine the status of the vehicle + $status = null; + $isVerified = in_array($reg, $verified_regs); + $isKnown = in_array($reg, $known_regs); + $isBooked = in_array($reg, $booked_regs); + $isUnknown = in_array($reg, $unknown_regs); + // Initialize other variables + $customer_number = null; // Will be populated for verified / booked vehicles + $customer_name = null; // Will be populated for verified / booked vehicles + $last_order_id = (new customer_vehicles_o())->getLastOrderByPlate($reg)?->id ?? null; + // These variables are only for verified vehicles + $type = null; + $id = null; + $reference = null; + // Booking related variables + $booking_id = null; + $notes = null; // Booking notes + + // Set the status based on priority: booked > verified > known > unknown + if ($isBooked) { + $status = 'booked'; + // Get the booking + $booking = (new bookings_o())->getFieldsWhere([ + 'regNrTraekker' => $reg, + 'status' => 'pending', + ], ['id', 'customer_number', 'reference_number', 'notes']); + // If there is no booking with the tractor reg, check the trailer reg + if (!$booking) { + $booking = (new bookings_o())->getFieldsWhere([ + 'regNrTrailer' => $reg, + 'status' => 'pending', + ], ['id', 'customer_number', 'reference_number', 'notes']); + } + // Populate other variables + $booking_id = $booking ? (int)$booking[0]['id'] : null; + $customer_number = $booking ? (int)$booking[0]['customer_number'] : null; + $reference = $booking ? (string)$booking[0]['reference_number'] : null; + $notes = $booking ? (string)$booking[0]['notes'] : null; + // If the booking has a customer number, check if the customer is barred + if ($customer_number && (new users_o())->isCustomerBarred((int)$customer_number)) { + $status = 'card'; + } + } elseif ($isVerified) { + $status = 'verified'; + // Get the vehicle + $customer_vehicles_o = new customer_vehicles_o(); + $vehicle = $customer_vehicles_o->getFieldsWhere(['reg' => $reg], ['customer_id', 'type', 'id', 'reference']); + // Populate other variables + $customer_number = $vehicle ? (int)$vehicle[0]['customer_id'] : null; + $type = $vehicle ? (int)$vehicle[0]['type'] : null; + $id = $vehicle ? (int)$vehicle[0]['id'] : null; + $reference = $vehicle ? (string)$vehicle[0]['reference'] : null; + // Check if the customer is barred (only if verified) + if ($customer_number && (new users_o())->isCustomerBarred((int)$customer_number)) { + $status = 'card'; + } + } elseif ($isKnown) { + $status = 'known'; + } elseif ($isUnknown) { + $status = 'unknown'; + } + // Get the customer name if we have a customer number + if ($customer_number) { + $customer = (new users_o())->getUserByCustomerNumber((int)$customer_number); + if ($customer->exists()) { + $customer_name = (string)$customer->getCustomerName((int)$customer_number); + } + } + // Return the formatted vehicle + return [ + 'reg' => $reg, + 'status' => $status, + 'type' => $type, + 'customer_id' => $customer_number, + 'id' => $id, + 'last_order_id' => $last_order_id, + 'reference' => $reference, + 'booking_id' => $booking_id, + 'notes' => $notes, + 'customer_name' => $customer_name, + 'barred' => $status === 'card', // If the status is 'card', the customer is barred + ]; + }, $vehicles); + // Return the results + $response->success($vehicles); + }, + [ + 'search_vehicles' => 'Search vehicles by registration number', + ] + ); + } + + private static function getVerifiedRegs(string $search, array $options = ['limit' => null]): array + { + $customer_vehicles_o = new customer_vehicles_o(); + $results = $customer_vehicles_o->getFieldsWhereContaining( + [ + 'reg' => $search + ], + ['reg'], + $options + ); + return array_map(function ($row) { + return (string)$row['reg']; + }, $results); + } + + private static function getKnownRegs(string $search, array $options = ['limit' => null]): array + { + $orders_o = new orders_o(); + $results = $orders_o->getFieldsWhereContaining( + [ + 'reg_1' => $search + ], + ['reg_1'], + $options + ); + return array_map(function ($row) { + return (string)$row['reg_1']; + }, $results); + } + + private static function getBookedRegs(string $search, array $options = ['limit' => null]): array + { + $bookings_o = new bookings_o(); + $results1 = $bookings_o->getFieldsWhereContaining( + [ + 'regNrTraekker' => $search, + 'status' => 'pending', + ], + ['regNrTraekker'], + $options + ); + $results2 = $bookings_o->getFieldsWhereContaining( + [ + 'regNrTrailer' => $search, + 'status' => 'pending', + ], + ['regNrTrailer'], + $options + ); + return array_unique(array_merge( + array_map(function ($row) { + return (string)$row['regNrTraekker']; + }, $results1), + array_map(function ($row) { + return (string)$row['regNrTrailer']; + }, $results2) + )); + } + + private static function getUnknownRegs(string $search, array $options = ['limit' => null]): array + { + // Unknown vehicles are vehicles that has been scanned by the LPR system, but are not in any of the other lists + $plate_scans_o = new plate_scans_o(); + $results = $plate_scans_o->getFieldsWhereContaining( + [ + 'plate' => $search, + ], + ['plate'], + $options + ); + return array_map(function ($row) { + return (string)$row['plate']; + }, $results); } private function getData(mixed $data, $response) diff --git a/services/nginx/app/traits/db_object_t.php b/services/nginx/app/traits/db_object_t.php index 1bb631d9..89691300 100644 --- a/services/nginx/app/traits/db_object_t.php +++ b/services/nginx/app/traits/db_object_t.php @@ -123,6 +123,26 @@ trait db_object_t return $db->fetch_all($result); } + public function getFieldsWhereContaining(array $fieldsAndValues, array $fields, array $options = ['limit' => null]): array + { + global $db; + $table = $this->table; + $where = []; + foreach ( $fieldsAndValues as $field => $value ) { + // Escape the value to prevent SQL injection + $value = $db->escape_string($value); + $where[] = "$field LIKE '%$value%'"; + } + $where = implode(' AND ', $where); + $sql = "SELECT " . implode(', ', $fields) . " FROM $table WHERE $where"; + // If the limit is set, add it to the query + if (isset($options['limit']) && is_int($options['limit']) && $options['limit'] > 0) { + $sql .= " LIMIT " . $options['limit']; + } + $result = $db->query($sql); + return $db->fetch_all($result); + } + /** * Update fields in the database where the conditions are met * @param array $fieldsAndValues The fields and values to update (e.g. ['name' => 'John Doe', 'email' => 'test@email.com'])