exercises

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

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 }