exercises

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

inventory.rs (9480B)


      1 //! Inventory/stock management API handlers
      2 //!
      3 //! This module contains the HTTP request handlers for inventory-related endpoints.
      4 //! Database operations are delegated to the repository layer in `db::repository`.
      5 
      6 use axum::{
      7     extract::{Path, Query, State},
      8     http::StatusCode,
      9     response::{IntoResponse, Json},
     10 };
     11 use opentelemetry::KeyValue;
     12 use sqlx::PgPool;
     13 use std::time::Instant;
     14 use tracing::{error, info, instrument, warn};
     15 use uuid::Uuid;
     16 
     17 use crate::db;
     18 use crate::metrics::metrics;
     19 use crate::models::{
     20     ConfirmSaleRequest, InventoryQueryParams, InventoryResponse, ReleaseStockRequest,
     21     ReserveStockRequest, StockOperationResponse, UpdateStockRequest,
     22 };
     23 use crate::utils::{calculate_pagination, calculate_total_pages, internal_error, not_found_error};
     24 
     25 /// List inventory with pagination and filtering
     26 ///
     27 /// # Endpoint
     28 /// `GET /inventory`
     29 ///
     30 /// # Query Parameters
     31 /// - `page` (default: 1) - Page number (1-indexed)
     32 /// - `page_size` (default: 20, max: 100) - Items per page
     33 /// - `stock_status` - Filter by status ('in_stock', 'low_stock', 'out_of_stock')
     34 /// - `product_uuid` - Filter by specific product
     35 /// - `min_stock` / `max_stock` - Filter by available quantity range
     36 pub async fn list_inventory(
     37     State(pool): State<PgPool>,
     38     Query(params): Query<InventoryQueryParams>,
     39 ) -> impl IntoResponse {
     40     // Apply pagination defaults and constraints
     41     let (page, page_size, offset) = calculate_pagination(params.page, params.page_size);
     42 
     43     // Delegate to repository layer for database operations
     44     let (inventory, total_count) =
     45         match db::list_inventory(&pool, &params, page, page_size, offset).await {
     46             Ok(result) => result,
     47             Err(e) => {
     48                 return internal_error("Failed to fetch inventory", e.to_string());
     49             }
     50         };
     51 
     52     let total_pages = calculate_total_pages(total_count, page_size);
     53 
     54     let response = InventoryResponse {
     55         inventory,
     56         total_count,
     57         page,
     58         page_size,
     59         total_pages,
     60     };
     61 
     62     (StatusCode::OK, Json(response)).into_response()
     63 }
     64 
     65 /// Get inventory for a specific product by UUID
     66 ///
     67 /// # Endpoint
     68 /// `GET /inventory/{product_uuid}`
     69 pub async fn get_inventory_by_product(
     70     State(pool): State<PgPool>,
     71     Path(product_uuid): Path<Uuid>,
     72 ) -> impl IntoResponse {
     73     // Delegate to repository layer for database lookup
     74     match db::get_inventory_by_product(&pool, product_uuid).await {
     75         Ok(Some(inventory)) => (StatusCode::OK, Json(inventory)).into_response(),
     76         Ok(None) => not_found_error(
     77             "Inventory not found for product",
     78             serde_json::json!({"product_uuid": product_uuid.to_string()}),
     79         ),
     80         Err(e) => internal_error("Failed to fetch inventory", e.to_string()),
     81     }
     82 }
     83 
     84 /// Update stock quantity for a product
     85 ///
     86 /// # Endpoint
     87 /// `PUT /inventory/{product_uuid}`
     88 pub async fn update_stock(
     89     State(pool): State<PgPool>,
     90     Path(product_uuid): Path<Uuid>,
     91     Json(request): Json<UpdateStockRequest>,
     92 ) -> impl IntoResponse {
     93     // Delegate to repository layer for stock update
     94     match db::update_stock(
     95         &pool,
     96         product_uuid,
     97         request.quantity,
     98         request.reorder_level,
     99         request.reorder_quantity,
    100     )
    101     .await
    102     {
    103         Ok(Some(available_quantity)) => (
    104             StatusCode::OK,
    105             Json(StockOperationResponse {
    106                 success: true,
    107                 message: "Stock updated successfully".to_string(),
    108                 product_uuid,
    109                 available_quantity: Some(available_quantity),
    110             }),
    111         )
    112             .into_response(),
    113         Ok(None) => not_found_error(
    114             "Product not found in inventory",
    115             serde_json::json!({"product_uuid": product_uuid.to_string()}),
    116         ),
    117         Err(e) => internal_error("Failed to update stock", e.to_string()),
    118     }
    119 }
    120 
    121 /// Reserve stock for an order
    122 ///
    123 /// # Endpoint
    124 /// `POST /inventory/reserve`
    125 #[instrument(
    126     name = "reserve_stock",
    127     skip(pool),
    128     fields(
    129         otel.kind = "client",
    130         db.system.name = "postgresql",
    131         db.namespace = "inventory",
    132         product.uuid = %request.product_uuid,
    133         operation.result = tracing::field::Empty,
    134     )
    135 )]
    136 pub async fn reserve_stock(
    137     State(pool): State<PgPool>,
    138     Json(request): Json<ReserveStockRequest>,
    139 ) -> impl IntoResponse {
    140     // Start timing and record a reservation attempt
    141     let start = Instant::now();
    142     metrics().reservation_attempts.add(1, &[]);
    143 
    144     // Delegate to repository layer for stock reservation
    145     match db::reserve_stock(&pool, request.product_uuid, request.quantity).await {
    146         Ok(success) => {
    147             let duration = start.elapsed().as_secs_f64();
    148 
    149             if success {
    150                 // Record successful reservation logs and metrics
    151 
    152                 tracing::Span::current().record("operation.result", "success");
    153                 // also on the log record
    154                 info!(operation.result = "success", "Stock reserved successfully");
    155 
    156                 metrics()
    157                     .reservation_duration
    158                     .record(duration, &[KeyValue::new("outcome", "success")]);
    159                 metrics()
    160                     .reserved_quantity
    161                     .add(request.quantity as u64, &[]);
    162 
    163                 (
    164                     StatusCode::OK,
    165                     Json(StockOperationResponse {
    166                         success: true,
    167                         message: format!("Reserved {} units", request.quantity),
    168                         product_uuid: request.product_uuid,
    169                         available_quantity: None,
    170                     }),
    171                 )
    172                     .into_response()
    173             } else {
    174                 // Insufficient stock — record failure logs and metrics
    175                 tracing::Span::current().record("operation.result", "insufficient_stock");
    176                 warn!(
    177                     operation.result = "insufficient_stock",
    178                     quantity.requested = request.quantity,
    179                     "Insufficient stock"
    180                 );
    181 
    182                 metrics()
    183                     .reservation_failures
    184                     .add(1, &[KeyValue::new("failure.reason", "insufficient_stock")]);
    185                 metrics().reservation_duration.record(
    186                     duration,
    187                     &[
    188                         KeyValue::new("outcome", "failure"),
    189                         KeyValue::new("failure.reason", "insufficient_stock"),
    190                     ],
    191                 );
    192 
    193                 (
    194                     StatusCode::CONFLICT,
    195                     Json(StockOperationResponse {
    196                         success: false,
    197                         message: "Insufficient stock available".to_string(),
    198                         product_uuid: request.product_uuid,
    199                         available_quantity: None,
    200                     }),
    201                 )
    202                     .into_response()
    203             }
    204         }
    205         Err(e) => {
    206             // Database error — record failure logs and metrics
    207             tracing::Span::current().record("operation.result", "error");
    208             error!(operation.result = "error", error.r#type = "database", error.message = %e, "Database error during stock reservation");
    209 
    210             let duration = start.elapsed().as_secs_f64();
    211             metrics()
    212                 .reservation_failures
    213                 .add(1, &[KeyValue::new("failure.reason", "database_error")]);
    214             metrics().reservation_duration.record(
    215                 duration,
    216                 &[
    217                     KeyValue::new("outcome", "failure"),
    218                     KeyValue::new("failure.reason", "database_error"),
    219                 ],
    220             );
    221 
    222             internal_error("Failed to reserve stock", e.to_string())
    223         }
    224     }
    225 }
    226 
    227 /// Release reserved stock
    228 ///
    229 /// # Endpoint
    230 /// `POST /inventory/release`
    231 pub async fn release_stock(
    232     State(pool): State<PgPool>,
    233     Json(request): Json<ReleaseStockRequest>,
    234 ) -> impl IntoResponse {
    235     // Delegate to repository layer for stock release
    236     match db::release_stock(&pool, request.product_uuid, request.quantity).await {
    237         Ok(_) => (
    238             StatusCode::OK,
    239             Json(StockOperationResponse {
    240                 success: true,
    241                 message: format!("Released {} units", request.quantity),
    242                 product_uuid: request.product_uuid,
    243                 available_quantity: None,
    244             }),
    245         )
    246             .into_response(),
    247         Err(e) => internal_error("Failed to release stock", e.to_string()),
    248     }
    249 }
    250 
    251 /// Confirm a sale and decrease stock
    252 ///
    253 /// # Endpoint
    254 /// `POST /inventory/confirm-sale`
    255 pub async fn confirm_sale(
    256     State(pool): State<PgPool>,
    257     Json(request): Json<ConfirmSaleRequest>,
    258 ) -> impl IntoResponse {
    259     // Delegate to repository layer for sale confirmation
    260     match db::confirm_sale(
    261         &pool,
    262         request.product_uuid,
    263         request.quantity,
    264         request.order_uuid,
    265     )
    266     .await
    267     {
    268         Ok(_) => (
    269             StatusCode::OK,
    270             Json(StockOperationResponse {
    271                 success: true,
    272                 message: format!("Confirmed sale of {} units", request.quantity),
    273                 product_uuid: request.product_uuid,
    274                 available_quantity: None,
    275             }),
    276         )
    277             .into_response(),
    278         Err(e) => internal_error("Failed to confirm sale", e.to_string()),
    279     }
    280 }