products.rs (3538B)
1 //! Product API handlers 2 //! 3 //! This module contains the HTTP request handlers for product-related endpoints. 4 //! Handlers delegate database operations to the repository layer (db module). 5 6 use axum::{ 7 extract::{Path, Query, State}, 8 http::StatusCode, 9 response::{IntoResponse, Json}, 10 }; 11 use sqlx::PgPool; 12 use tracing::instrument; 13 use uuid::Uuid; 14 15 use crate::db; 16 use crate::models::{ProductQueryParams, ProductsResponse}; 17 use crate::utils::{calculate_pagination, calculate_total_pages, internal_error, not_found_error}; 18 19 /// List products with pagination and filtering 20 /// 21 /// # Endpoint 22 /// `GET /products` 23 /// 24 /// # Query Parameters 25 /// - **Pagination:** 26 /// - `page` (default: 1) - Page number (1-indexed) 27 /// - `page_size` (default: 20, max: 100) - Items per page 28 /// 29 /// - **Filters:** 30 /// - `name` - Product name partial match (case-insensitive) 31 /// - `category_id` - Filter by category ID 32 /// - `brand` - Brand partial match (case-insensitive) 33 /// - `start_date` / `end_date` - Filter by update date range 34 /// - `rating_gt` / `rating_lt` / `rating_eq` - Rating filters 35 /// - `min_price` / `max_price` - Price range filters 36 /// 37 /// # Response 38 /// Returns a `ProductsResponse` with products array and pagination metadata. 39 pub async fn list_products( 40 State(pool): State<PgPool>, 41 Query(params): Query<ProductQueryParams>, 42 ) -> impl IntoResponse { 43 // Apply pagination defaults and constraints 44 let (page, page_size, offset) = calculate_pagination(params.page, params.page_size); 45 46 // Delegate to repository layer for database operations 47 match db::list_products(&pool, ¶ms, page, page_size, offset).await { 48 Ok((products, total_count)) => { 49 let total_pages = calculate_total_pages(total_count, page_size); 50 51 let response = ProductsResponse { 52 products, 53 total_count, 54 page, 55 page_size, 56 total_pages, 57 }; 58 59 (StatusCode::OK, Json(response)).into_response() 60 } 61 Err(e) => internal_error("Failed to fetch products", e.to_string()), 62 } 63 } 64 65 /// Get detailed product information by UUID 66 /// 67 /// # Endpoint 68 /// `GET /products/{uuid}` 69 /// 70 /// # Errors 71 /// - `404 NOT FOUND` - Product not found or has been deleted 72 /// - `500 INTERNAL SERVER ERROR` - Database error 73 #[instrument( 74 name = "get_product_by_id", 75 skip(pool), 76 fields( 77 product.uuid = %uuid, 78 product.found = tracing::field::Empty 79 ) 80 )] 81 pub async fn get_product_by_id( 82 State(pool): State<PgPool>, 83 Path(uuid): Path<Uuid>, 84 ) -> impl IntoResponse { 85 // Delegate to repository layer for database query 86 match db::get_product_by_uuid(&pool, uuid).await { 87 Ok(Some(mut product)) => { 88 tracing::Span::current().record("product.found", true); 89 tracing::debug!(product.uuid = true, product.name = %product.product_name, "Product found"); 90 91 // Set the string representation of the product ID 92 product.set_product_id(); 93 (StatusCode::OK, Json(product)).into_response() 94 } 95 Ok(None) => { 96 tracing::Span::current().record("product.found", false); 97 tracing::info!(product.found = false, "Product not found"); // INFO not ERROR 98 99 not_found_error( 100 "Product not found", 101 serde_json::json!({"uuid": uuid.to_string()}), 102 ) 103 } 104 Err(e) => internal_error("Failed to fetch product", e.to_string()), 105 } 106 }