product.rs (9944B)
1 //! Product models and API response structures 2 //! 3 //! This module defines the core product data models used throughout the service. 4 //! Models are designed to match the Angular client's TypeScript interfaces. 5 6 use bigdecimal::BigDecimal; 7 use chrono::{DateTime, Utc}; 8 use serde::{Deserialize, Serialize}; 9 use sqlx::FromRow; 10 use uuid::Uuid; 11 12 // ============================================================================ 13 // Database Entities (not currently used directly, kept for future extensibility) 14 // ============================================================================ 15 16 /// Complete product entity as stored in the database 17 /// 18 /// This struct represents the full product record from the database. 19 /// Currently not used directly by handlers, but kept for future CRUD operations. 20 /// Handlers use specialized view models (ProductDetail, ProductWithRating) instead. 21 #[allow(dead_code)] 22 #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] 23 pub struct Product { 24 // Primary keys 25 pub id: i32, 26 pub uuid: Uuid, 27 28 // External identifiers 29 pub asin: Option<String>, 30 pub sku: Option<String>, 31 pub gtin: Option<String>, 32 33 // Basic information 34 pub product_name: String, 35 pub brand: Option<String>, 36 pub description: Option<String>, 37 38 // Category relationship 39 pub category_id: Option<i32>, 40 41 // Pricing 42 pub price: BigDecimal, 43 44 // Product attributes 45 pub sizes: Option<Vec<String>>, 46 pub colors: Option<Vec<String>>, 47 48 // URLs and media 49 pub url: Option<String>, 50 pub image_url: Option<String>, 51 52 // Inventory 53 pub stock_quantity: i32, 54 55 // Feature flags 56 pub available_for_delivery: bool, 57 pub available_for_pickup: bool, 58 pub free_returns: bool, 59 pub is_active: bool, 60 61 // Audit timestamps 62 pub deleted_at: Option<DateTime<Utc>>, 63 pub created_at: DateTime<Utc>, 64 pub updated_at: DateTime<Utc>, 65 pub data_timestamp: Option<DateTime<Utc>>, 66 } 67 68 // ============================================================================ 69 // API Response Models 70 // ============================================================================ 71 72 /// Detailed product information for GET /products/{id} 73 /// 74 /// This model includes: 75 /// - Full product information 76 /// - Category details (name, uuid, slug) 77 /// - Aggregated rating data (average rating, review count) 78 /// - Client compatibility fields (eid instead of uuid, final_price instead of price) 79 /// 80 /// # Field Mappings (Database → JSON) 81 /// - `uuid` → `eid` (external ID for the client) 82 /// - `price` → `final_price` (matches client TypeScript interface) 83 /// - `stock_quantity` → `stock` (shorter field name for API) 84 /// - `id`, `asin`, `gtin`, `category_uuid`, `category_slug`, `rating_count` are skipped in JSON 85 #[derive(Debug, Serialize, FromRow)] 86 pub struct ProductDetail { 87 // Product core fields 88 #[serde(skip)] 89 pub id: i32, // Internal DB ID, not exposed to client 90 91 #[serde(rename = "eid")] 92 pub uuid: Uuid, // External UUID identifier 93 94 #[serde(skip)] 95 pub asin: Option<String>, // Amazon ID, kept for internal use only 96 97 pub sku: Option<String>, 98 99 #[serde(skip)] 100 pub gtin: Option<String>, // Global Trade Item Number, internal use only 101 102 pub product_name: String, 103 pub brand: Option<String>, 104 pub description: Option<String>, 105 pub url: Option<String>, 106 107 #[serde(rename = "final_price")] 108 pub price: BigDecimal, // Renamed to match client expectation 109 110 #[serde(rename = "stock")] 111 pub stock_quantity: i32, // Shortened for API consistency 112 113 pub sizes: Option<Vec<String>>, 114 pub colors: Option<Vec<String>>, 115 pub image_url: Option<String>, 116 117 // Delivery and return flags 118 pub available_for_delivery: bool, 119 pub available_for_pickup: bool, 120 pub free_returns: bool, 121 pub is_active: bool, 122 123 // Timestamps 124 pub created_at: DateTime<Utc>, 125 pub updated_at: DateTime<Utc>, 126 pub data_timestamp: Option<DateTime<Utc>>, 127 128 // Category information (joined from categories table) 129 pub category_id: Option<i32>, 130 pub category_name: Option<String>, 131 132 #[serde(skip)] 133 pub category_uuid: Option<Uuid>, // Internal use only 134 135 #[serde(skip)] 136 pub category_slug: Option<String>, // Internal use only 137 138 // Rating aggregates (computed from ratings table) 139 pub average_rating: Option<f64>, 140 141 #[serde(skip)] 142 pub rating_count: i64, // Internal use, client uses reviews_count in ProductCard 143 144 // Additional fields for client compatibility 145 #[serde(rename = "product_id")] 146 pub product_id_str: String, // String representation of ID 147 148 #[serde(skip_serializing_if = "Option::is_none")] 149 pub initial_price: Option<BigDecimal>, // Original price before discount (future use) 150 151 #[serde(skip_serializing_if = "Option::is_none")] 152 pub discount: Option<String>, // Discount percentage/amount (future use) 153 154 pub currency: String, // Currency code (e.g., "USD") 155 156 #[serde(skip_serializing_if = "Option::is_none")] 157 pub root_category_name: Option<String>, // Top-level category (future use) 158 159 pub deleted_at: Option<DateTime<Utc>>, 160 } 161 162 impl ProductDetail { 163 /// Convert internal ID to string for the product_id field 164 /// 165 /// This helper method sets the product_id_str field from the numeric id. 166 /// Called after fetching from database before returning to client. 167 pub fn set_product_id(&mut self) { 168 self.product_id_str = self.id.to_string(); 169 } 170 } 171 172 /// Paginated list response for GET /products 173 /// 174 /// Matches the client's PaginatedResponse<ProductCard> interface. 175 /// The `total_count` field is renamed to `total` in JSON to match client expectations. 176 #[derive(Debug, Serialize)] 177 pub struct ProductsResponse { 178 /// List of products with basic info and ratings 179 pub products: Vec<ProductWithRating>, 180 181 /// Total number of products matching the filter criteria 182 #[serde(rename = "total")] 183 pub total_count: i64, 184 185 /// Current page number (1-indexed) 186 pub page: i32, 187 188 /// Number of items per page 189 pub page_size: i32, 190 191 /// Total number of pages available 192 pub total_pages: i32, 193 } 194 195 /// Product card with rating information for product listings 196 /// 197 /// This is a lighter model than ProductDetail, used for the product list view. 198 /// Includes aggregated rating data but omits detailed fields like sizes, colors, etc. 199 /// 200 /// # Field Mappings (Database → JSON) 201 /// - `uuid` → `eid` 202 /// - `price` → `final_price` 203 /// - `stock_quantity` → `stock` 204 /// - `rating_count` → `reviews_count` 205 /// - `id`, `created_at`, `updated_at` are skipped in JSON 206 #[derive(Debug, Serialize, FromRow)] 207 pub struct ProductWithRating { 208 // Product core fields 209 #[serde(skip)] 210 pub id: i32, // Internal ID, not exposed 211 212 #[serde(rename = "eid")] 213 pub uuid: Uuid, // External UUID identifier 214 215 pub product_name: String, 216 pub brand: Option<String>, 217 pub description: Option<String>, 218 219 #[serde(rename = "final_price")] 220 pub price: BigDecimal, 221 222 #[serde(rename = "stock")] 223 pub stock_quantity: i32, 224 225 pub image_url: Option<String>, 226 227 // Category info 228 pub category_id: Option<i32>, 229 pub category_name: Option<String>, 230 231 // Timestamps (internal use only, not serialized) 232 #[serde(skip)] 233 pub created_at: DateTime<Utc>, 234 235 #[serde(skip)] 236 pub updated_at: DateTime<Utc>, 237 238 // Rating aggregates 239 pub average_rating: Option<f64>, 240 241 #[serde(rename = "reviews_count")] 242 pub rating_count: i64, // Total number of reviews 243 244 // Future pricing features (currently always None) 245 #[serde(skip_serializing_if = "Option::is_none")] 246 pub initial_price: Option<BigDecimal>, // Original price before discount 247 248 #[serde(skip_serializing_if = "Option::is_none")] 249 pub discount: Option<String>, // Discount label (e.g., "20% off") 250 } 251 252 // ============================================================================ 253 // Query Parameters 254 // ============================================================================ 255 256 /// Query parameters for filtering and paginating product lists 257 /// 258 /// All parameters are optional. If not provided, sensible defaults are used: 259 /// - `page`: 1 260 /// - `page_size`: 20 261 /// 262 /// # Example Query 263 /// ```text 264 /// GET /products?page=2&page_size=50&brand=TechPro&min_price=100&max_price=500 265 /// ``` 266 #[derive(Debug, Deserialize)] 267 pub struct ProductQueryParams { 268 // Pagination 269 /// Page number (1-indexed, default: 1) 270 pub page: Option<i32>, 271 272 /// Items per page (default: 20, max: 100) 273 pub page_size: Option<i32>, 274 275 // Text filters 276 /// Filter by product name (case-insensitive partial match) 277 pub name: Option<String>, 278 279 /// Filter by category ID (exact match) 280 pub category_id: Option<i32>, 281 282 /// Filter by brand (case-insensitive partial match) 283 pub brand: Option<String>, 284 285 // Date range filters 286 /// Filter products updated after this date 287 pub start_date: Option<DateTime<Utc>>, 288 289 /// Filter products updated before this date 290 pub end_date: Option<DateTime<Utc>>, 291 292 // Rating filters (can be combined) 293 /// Filter products with rating greater than this value 294 pub rating_gt: Option<f64>, 295 296 /// Filter products with rating less than this value 297 pub rating_lt: Option<f64>, 298 299 /// Filter products with rating equal to this value 300 pub rating_eq: Option<f64>, 301 302 // Price range filters 303 /// Minimum price filter 304 pub min_price: Option<f64>, 305 306 /// Maximum price filter 307 pub max_price: Option<f64>, 308 } 309 310 impl Default for ProductQueryParams { 311 fn default() -> Self { 312 Self { 313 page: Some(1), 314 page_size: Some(20), 315 name: None, 316 category_id: None, 317 brand: None, 318 start_date: None, 319 end_date: None, 320 rating_gt: None, 321 rating_lt: None, 322 rating_eq: None, 323 min_price: None, 324 max_price: None, 325 } 326 } 327 }