exercises

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

orders.rs (27440B)


      1 //! Order management API handlers
      2 
      3 use std::time::Instant;
      4 
      5 use axum::{
      6     extract::{Path, Query, State},
      7     http::StatusCode,
      8     response::{IntoResponse, Json},
      9 };
     10 use bigdecimal::BigDecimal;
     11 use opentelemetry::KeyValue;
     12 use sqlx::PgPool;
     13 use tracing::{debug, error, info, instrument, warn};
     14 use uuid::Uuid;
     15 
     16 use crate::logging::hash_email;
     17 use crate::models::{CreateOrderRequest, OrderQueryParams, OrdersResponse};
     18 use crate::AppState;
     19 use crate::{
     20     db::{self, with_transaction, OrderTotals},
     21     metrics::metrics,
     22 };
     23 
     24 /// Product detail response from products service
     25 #[derive(Debug, serde::Deserialize)]
     26 #[allow(dead_code)] // Fields deserialized from products service response, not all used directly
     27 struct ProductDetail {
     28     eid: Uuid,
     29     product_name: String,
     30     final_price: BigDecimal,
     31     stock: i32,
     32     is_active: bool,
     33 }
     34 
     35 /// Request to reserve stock in inventory service
     36 #[derive(Debug, serde::Serialize)]
     37 struct ReserveStockRequest {
     38     product_uuid: Uuid,
     39     quantity: i32,
     40 }
     41 
     42 /// Request to release reserved stock in inventory service
     43 #[derive(Debug, serde::Serialize)]
     44 struct ReleaseStockRequest {
     45     product_uuid: Uuid,
     46     quantity: i32,
     47 }
     48 
     49 /// Request to confirm sale in inventory service
     50 #[derive(Debug, serde::Serialize)]
     51 struct ConfirmSaleRequest {
     52     product_uuid: Uuid,
     53     quantity: i32,
     54     order_uuid: Uuid,
     55 }
     56 
     57 /// Response from inventory stock operations
     58 #[derive(Debug, serde::Deserialize)]
     59 #[allow(dead_code)] // Fields deserialized from inventory service response, not all used directly
     60 struct StockOperationResponse {
     61     success: bool,
     62     message: String,
     63     product_uuid: Uuid,
     64     available_quantity: Option<i32>,
     65 }
     66 
     67 /// Release reserved stock for a product in the inventory service
     68 ///
     69 /// Calls the inventory service to release previously reserved stock.
     70 /// Used for rollback when order creation fails.
     71 async fn release_stock(state: &AppState, product_uuid: Uuid, quantity: i32) {
     72     let url = format!("{}/inventory/release", state.inventory_service_url());
     73 
     74     let request_body = ReleaseStockRequest {
     75         product_uuid,
     76         quantity,
     77     };
     78 
     79     let request = state.http_client().post(&url).json(&request_body);
     80 
     81     let response = request.send().await;
     82 
     83     match response {
     84         Ok(resp) => {
     85             if resp.status().is_success() {
     86                 debug!(
     87                     product.uuid = %product_uuid,
     88                     quantity = quantity,
     89                     "Stock released successfully"
     90                 );
     91             } else {
     92                 // This is serious — we have leaked inventory
     93                 error!(
     94                     product.uuid = %product_uuid,
     95                     quantity = quantity,
     96                     error.r#type = "compensation_failure",
     97                     downstream.status = %resp.status(),
     98                     "CRITICAL: Failed to release reserved stock"
     99                 );
    100             }
    101         }
    102         Err(e) => {
    103             error!(
    104                 product.uuid = %product_uuid,
    105                 quantity = quantity,
    106                 error.r#type = "compensation_failure",
    107                 error.message = %e,
    108                 "CRITICAL: Failed to release reserved stock"
    109             );
    110         }
    111     }
    112 }
    113 
    114 /// Release all reserved stock (for rollback scenarios)
    115 ///
    116 /// Iterates through all reserved items and releases their stock.
    117 /// Logs errors but doesn't fail - this is a best-effort cleanup.
    118 async fn release_all_reserved_stock(state: &AppState, reserved_items: &[(Uuid, i32)]) {
    119     if reserved_items.is_empty() {
    120         return;
    121     }
    122 
    123     info!(
    124         item_count = reserved_items.len(),
    125         "Releasing reserved stock"
    126     );
    127 
    128     for (product_uuid, quantity) in reserved_items {
    129         release_stock(state, *product_uuid, *quantity).await;
    130     }
    131 
    132     info!(item_count = reserved_items.len(), "Stock release complete");
    133 }
    134 
    135 /// Confirm a sale with the inventory service
    136 ///
    137 /// Calls the inventory service to confirm the sale and convert reserved stock to sold.
    138 /// This is called after an order is successfully created and committed.
    139 async fn confirm_sale(
    140     state: &AppState,
    141     product_uuid: Uuid,
    142     quantity: i32,
    143     order_uuid: Uuid,
    144 ) -> Result<(), String> {
    145     let url = format!("{}/inventory/confirm-sale", state.inventory_service_url());
    146 
    147     let request_body = ConfirmSaleRequest {
    148         product_uuid,
    149         quantity,
    150         order_uuid,
    151     };
    152 
    153     let request = state.http_client().post(&url).json(&request_body);
    154 
    155     let response = request
    156         .send()
    157         .await
    158         .map_err(|e| format!("Failed to call inventory service: {}", e))?;
    159 
    160     if response.status().is_success() {
    161         Ok(())
    162     } else {
    163         Err(format!(
    164             "Inventory service returned error status: {}",
    165             response.status()
    166         ))
    167     }
    168 }
    169 
    170 /// Confirm all sales for order items
    171 ///
    172 /// Iterates through all order items and confirms the sale with inventory service.
    173 /// Logs errors but doesn't fail the order since it's already committed.
    174 async fn confirm_all_sales(state: &AppState, order_uuid: Uuid, items: &[(Uuid, i32)]) {
    175     if items.is_empty() {
    176         return;
    177     }
    178 
    179     info!(
    180         item_count = items.len(),
    181         order_uuid = %order_uuid,
    182         "Confirming sales with inventory service"
    183     );
    184 
    185     for (product_uuid, quantity) in items {
    186         match confirm_sale(state, *product_uuid, *quantity, order_uuid).await {
    187             Ok(_) => {
    188                 info!(
    189                     product_uuid = %product_uuid,
    190                     quantity = quantity,
    191                     order_uuid = %order_uuid,
    192                     "Successfully confirmed sale"
    193                 );
    194             }
    195             Err(e) => {
    196                 // Log error but don't fail - order is already committed
    197                 warn!(
    198                     product_uuid = %product_uuid,
    199                     order_uuid = %order_uuid,
    200                     error = %e,
    201                     "Failed to confirm sale - manual intervention may be required"
    202                 );
    203             }
    204         }
    205     }
    206 }
    207 
    208 /// Reserve stock for a product in the inventory service
    209 ///
    210 /// Calls the inventory service to reserve stock for an order.
    211 /// Returns error if insufficient stock is available.
    212 async fn reserve_stock(
    213     state: &AppState,
    214     product_uuid: Uuid,
    215     quantity: i32,
    216 ) -> Result<StockOperationResponse, axum::response::Response> {
    217     let url = format!("{}/inventory/reserve", state.inventory_service_url());
    218 
    219     let request_body = ReserveStockRequest {
    220         product_uuid,
    221         quantity,
    222     };
    223 
    224     let request = state.http_client().post(&url).json(&request_body);
    225 
    226     let response = match request.send().await {
    227         Ok(resp) => resp,
    228         Err(e) => {
    229             eprintln!("Failed to call inventory service: {}", e);
    230             return Err((
    231                 StatusCode::SERVICE_UNAVAILABLE,
    232                 Json(serde_json::json!({
    233                     "error": "Inventory service unavailable",
    234                     "details": e.to_string()
    235                 })),
    236             )
    237                 .into_response());
    238         }
    239     };
    240 
    241     match response.status() {
    242         reqwest::StatusCode::OK => {
    243             let stock_response: StockOperationResponse = match response.json().await {
    244                 Ok(r) => r,
    245                 Err(e) => {
    246                     eprintln!("Failed to parse inventory response: {}", e);
    247                     return Err((
    248                         StatusCode::INTERNAL_SERVER_ERROR,
    249                         Json(serde_json::json!({
    250                             "error": "Failed to parse inventory response",
    251                             "details": e.to_string()
    252                         })),
    253                     )
    254                         .into_response());
    255                 }
    256             };
    257 
    258             if !stock_response.success {
    259                 return Err((
    260                     StatusCode::CONFLICT,
    261                     Json(serde_json::json!({
    262                         "error": "Insufficient stock",
    263                         "product_uuid": product_uuid.to_string(),
    264                         "requested_quantity": quantity,
    265                         "available_quantity": stock_response.available_quantity,
    266                         "message": stock_response.message
    267                     })),
    268                 )
    269                     .into_response());
    270             }
    271 
    272             Ok(stock_response)
    273         }
    274         reqwest::StatusCode::CONFLICT => {
    275             // Inventory service returned 409 - insufficient stock
    276             let error_response: serde_json::Value = response.json().await.unwrap_or_else(|_| {
    277                 serde_json::json!({
    278                     "error": "Insufficient stock",
    279                     "product_uuid": product_uuid.to_string()
    280                 })
    281             });
    282 
    283             Err((StatusCode::CONFLICT, Json(error_response)).into_response())
    284         }
    285         status => {
    286             eprintln!("Inventory service returned status: {}", status);
    287             Err((
    288                 StatusCode::INTERNAL_SERVER_ERROR,
    289                 Json(serde_json::json!({
    290                     "error": "Failed to reserve stock",
    291                     "status": status.as_u16()
    292                 })),
    293             )
    294                 .into_response())
    295         }
    296     }
    297 }
    298 
    299 /// Validate that a product exists and is available
    300 ///
    301 /// Calls the products service to verify the product exists and is active.
    302 /// Returns the product details for further validation (price, stock, etc.)
    303 async fn validate_product(
    304     state: &AppState,
    305     product_uuid: Uuid,
    306 ) -> Result<ProductDetail, axum::response::Response> {
    307     let url = format!("{}/products/{}", state.products_service_url(), product_uuid);
    308 
    309     let request = state.http_client().get(&url);
    310 
    311     let response = match request.send().await {
    312         Ok(resp) => resp,
    313         Err(e) => {
    314             eprintln!("Failed to call products service: {}", e);
    315             return Err((
    316                 StatusCode::SERVICE_UNAVAILABLE,
    317                 Json(serde_json::json!({
    318                     "error": "Products service unavailable",
    319                     "details": e.to_string()
    320                 })),
    321             )
    322                 .into_response());
    323         }
    324     };
    325 
    326     match response.status() {
    327         reqwest::StatusCode::OK => {
    328             let product: ProductDetail = match response.json().await {
    329                 Ok(p) => p,
    330                 Err(e) => {
    331                     eprintln!("Failed to parse product response: {}", e);
    332                     return Err((
    333                         StatusCode::INTERNAL_SERVER_ERROR,
    334                         Json(serde_json::json!({
    335                             "error": "Failed to parse product data",
    336                             "details": e.to_string()
    337                         })),
    338                     )
    339                         .into_response());
    340                 }
    341             };
    342 
    343             // Verify product is active
    344             if !product.is_active {
    345                 return Err((
    346                     StatusCode::BAD_REQUEST,
    347                     Json(serde_json::json!({
    348                         "error": "Product is not available",
    349                         "product_uuid": product_uuid.to_string(),
    350                         "product_name": product.product_name
    351                     })),
    352                 )
    353                     .into_response());
    354             }
    355 
    356             Ok(product)
    357         }
    358         reqwest::StatusCode::NOT_FOUND => Err((
    359             StatusCode::BAD_REQUEST,
    360             Json(serde_json::json!({
    361                 "error": "Product not found",
    362                 "product_uuid": product_uuid.to_string()
    363             })),
    364         )
    365             .into_response()),
    366         status => {
    367             eprintln!("Products service returned status: {}", status);
    368             Err((
    369                 StatusCode::INTERNAL_SERVER_ERROR,
    370                 Json(serde_json::json!({
    371                     "error": "Failed to validate product",
    372                     "status": status.as_u16()
    373                 })),
    374             )
    375                 .into_response())
    376         }
    377     }
    378 }
    379 
    380 /// Order creation error type
    381 #[derive(Debug)]
    382 pub enum OrderError {
    383     Database(sqlx::Error),
    384 }
    385 
    386 impl From<sqlx::Error> for OrderError {
    387     fn from(err: sqlx::Error) -> Self {
    388         OrderError::Database(err)
    389     }
    390 }
    391 
    392 /// Create a new order
    393 ///
    394 /// # Endpoint
    395 /// `POST /orders`
    396 ///
    397 /// Creates a new order with items, shipping address, and payment info.
    398 /// Automatically generates order number and calculates totals.
    399 ///
    400 /// Uses the `with_transaction` wrapper to ensure all database operations
    401 /// are atomic and instrumented with OpenTelemetry semantic conventions.
    402 #[instrument(
    403     name = "create_order",
    404     skip(state, request),
    405     fields(
    406         customer.email_hash = %hash_email(&request.customer_email),
    407         order.item_count = request.items.len(),
    408         order.number = tracing::field::Empty,
    409     )
    410 )]
    411 pub async fn create_order(
    412     State(state): State<AppState>,
    413     Json(request): Json<CreateOrderRequest>,
    414 ) -> impl IntoResponse {
    415     // Start timing and record a checkout attempt
    416     let start = Instant::now();
    417     metrics().checkout_attempts.add(1, &[]);
    418 
    419     // Business KPI: Conversion funnel started
    420     metrics().funnel_started.add(1, &[]);
    421 
    422     // Validate all products exist, are available, and have correct prices
    423     for item in &request.items {
    424         let product = match validate_product(&state, item.product_uuid).await {
    425             Ok(product) => product,
    426             Err(response) => {
    427                 // Product validation failed — record checkout failure
    428                 record_checkout_failure("product_validation", &start);
    429                 // and then return error immediately
    430                 return response;
    431             }
    432         };
    433 
    434         // Validate price matches actual product price (prevent price manipulation)
    435         if item.unit_price != product.final_price {
    436             record_checkout_failure("price_mismatch", &start);
    437             return (
    438                 StatusCode::BAD_REQUEST,
    439                 Json(serde_json::json!({
    440                     "error": "Price mismatch",
    441                     "product_uuid": item.product_uuid.to_string(),
    442                     "product_name": product.product_name,
    443                     "submitted_price": item.unit_price.to_string(),
    444                     "actual_price": product.final_price.to_string(),
    445                     "message": "The submitted price does not match the current product price"
    446                 })),
    447             )
    448                 .into_response();
    449         }
    450     }
    451 
    452     // Business KPI: Payment info validated (all products and prices confirmed)
    453     metrics().funnel_payment_info.add(1, &[]);
    454 
    455     // Reserve stock for all items
    456     // Track reserved items so we can release them if something fails
    457     let mut reserved_items: Vec<(Uuid, i32)> = Vec::new();
    458 
    459     for item in &request.items {
    460         match reserve_stock(&state, item.product_uuid, item.quantity).await {
    461             Ok(_) => {
    462                 reserved_items.push((item.product_uuid, item.quantity));
    463             }
    464             Err(response) => {
    465                 // Stock reservation failed - release any already reserved stock
    466                 release_all_reserved_stock(&state, &reserved_items).await;
    467                 return response;
    468             }
    469         }
    470     }
    471 
    472     // Calculate totals
    473     let subtotal: BigDecimal = request
    474         .items
    475         .iter()
    476         .map(|item| &item.unit_price * BigDecimal::from(item.quantity))
    477         .sum();
    478 
    479     // Simple tax calculation (8% for demo)
    480     let tax_amount = &subtotal * BigDecimal::from(8) / BigDecimal::from(100);
    481 
    482     // Flat shipping rate for demo
    483     let shipping_amount = BigDecimal::from(10);
    484 
    485     let total = &subtotal + &tax_amount + &shipping_amount;
    486 
    487     let totals = OrderTotals {
    488         subtotal,
    489         tax_amount,
    490         shipping_amount,
    491         total: total.clone(),
    492     };
    493 
    494     // Clone data needed for the transaction closure
    495     let customer_email = request.customer_email.clone();
    496     let customer_phone = request.customer_phone.clone();
    497     let items = request.items.clone();
    498     let shipping_address = request.shipping_address.clone();
    499     let payment = request.payment.clone();
    500 
    501     // Execute all database operations within an instrumented transaction
    502     let result = with_transaction(state.pool(), "checkout", |tx| {
    503         // Move owned values into the closure
    504         let customer_email = customer_email.clone();
    505         let customer_phone = customer_phone.clone();
    506         let items = items.clone();
    507         let shipping_address = shipping_address.clone();
    508         let payment = payment.clone();
    509         let totals = totals.clone();
    510         let total = total.clone();
    511 
    512         Box::pin(async move {
    513             // Generate order number
    514             let order_number = db::generate_order_number(tx).await?;
    515             // This fills in the order.number which was empty, tracing::field::Empty,
    516             // at instrumentation macro
    517             tracing::Span::current().record("order.number", &order_number);
    518             // From this point, every Log in the span includes order.number
    519 
    520             // Create order record
    521             let created_order = db::create_order(
    522                 tx,
    523                 &order_number,
    524                 &customer_email,
    525                 customer_phone.as_deref(),
    526                 &totals,
    527             )
    528             .await?;
    529 
    530             // Create order items
    531             db::create_order_items(tx, created_order.id, &items).await?;
    532 
    533             // Create shipping address
    534             db::create_shipping_address(tx, created_order.id, &shipping_address).await?;
    535 
    536             // Generate payment reference and create payment
    537             let payment_reference = db::generate_payment_reference(tx).await?;
    538             db::create_payment(tx, created_order.id, &payment, &payment_reference, &total).await?;
    539 
    540             // Update order status to processing after successful payment
    541             db::update_order_payment_status(tx, created_order.id, "paid", "processing").await?;
    542 
    543             Ok::<_, OrderError>(created_order)
    544         })
    545     })
    546     .await;
    547 
    548     match result {
    549         Ok(created_order) => {
    550             // Transaction committed successfully!
    551 
    552             // Record successful checkout metrics
    553             let duration = start.elapsed().as_secs_f64();
    554             metrics()
    555                 .checkout_duration
    556                 .record(duration, &[KeyValue::new("outcome", "success")]);
    557             // Business KPI: Conversion funnel completed
    558             metrics().funnel_completed.add(1, &[]);
    559             // Business KPI: Time from checkout start to completion
    560             metrics().time_to_checkout.record(duration, &[]);
    561             // Record the order total as a dollar-value histogram
    562             metrics()
    563                 .order_total_amount
    564                 .record(bigdecimal_to_f64(&total), &[]);
    565             // Record how many line items were in this order
    566             metrics()
    567                 .order_items_count
    568                 .record(request.items.len() as u64, &[]);
    569 
    570             // Now confirm the sale with inventory service to convert reserved stock to sold
    571             confirm_all_sales(&state, created_order.uuid, &reserved_items).await;
    572 
    573             tracing::info!(
    574                 order.number = %created_order.order_number,
    575                 order.uuid = %created_order.uuid,
    576                 order.total = %total,
    577                 order.status = "processing",
    578                 duration_ms = (duration * 1000.0) as u64,
    579                 "Order created successfully"
    580             );
    581 
    582             (
    583                 StatusCode::CREATED,
    584                 Json(serde_json::json!({
    585                     "success": true,
    586                     "order_number": created_order.order_number,
    587                     "order_uuid": created_order.uuid,
    588                     "message": "Order created successfully"
    589                 })),
    590             )
    591                 .into_response()
    592         }
    593 
    594         Err(e) => {
    595             // Transaction failed (rolled back automatically)
    596             tracing::warn!(
    597                 reserved_count = reserved_items.len(),
    598                 "Order creation failed, releasing reserved stock"
    599             );
    600             // Record failure
    601             record_checkout_failure("order_creation", &start);
    602             // Release reserved stock
    603             release_all_reserved_stock(&state, &reserved_items).await;
    604 
    605             let duration = start.elapsed().as_secs_f64();
    606 
    607             let error_msg = match e {
    608                 OrderError::Database(db_err) => {
    609                     tracing::error!(
    610                         error.r#type = "database",
    611                         error.message = %db_err,
    612                         duration_ms = (duration * 1000.0) as u64,
    613                         "Database error during order creation"
    614                     );
    615                     format!("Database error: {}", db_err)
    616                 }
    617             };
    618 
    619             (
    620                 StatusCode::INTERNAL_SERVER_ERROR,
    621                 Json(serde_json::json!({
    622                     "error": "Failed to create order",
    623                     "details": error_msg
    624                 })),
    625             )
    626                 .into_response()
    627         }
    628     }
    629 }
    630 
    631 /// List orders with pagination and filtering
    632 ///
    633 /// # Endpoint
    634 /// `GET /orders`
    635 ///
    636 /// Supports two modes:
    637 /// 1. Authenticated user: Requires X-User-Email header, returns all orders for that user
    638 /// 2. Guest user: Requires both email and order_number query params, returns single order
    639 pub async fn list_orders(
    640     State(state): State<AppState>,
    641     headers: axum::http::HeaderMap,
    642     Query(params): Query<OrderQueryParams>,
    643 ) -> impl IntoResponse {
    644     // Check for authenticated user via X-User-Email header
    645     let user_email = headers
    646         .get("X-User-Email")
    647         .and_then(|h| h.to_str().ok())
    648         .map(String::from);
    649 
    650     if let Some(email) = user_email {
    651         // Authenticated user: return all their orders
    652         return list_user_orders(state.pool(), &email, &params).await;
    653     }
    654 
    655     // Guest user: require both email and order_number
    656     match (&params.customer_email, &params.order_number) {
    657         (Some(email), Some(order_number)) => {
    658             get_guest_order(state.pool(), email, order_number).await
    659         }
    660         _ => (
    661             StatusCode::BAD_REQUEST,
    662             Json(serde_json::json!({
    663                 "error": "Guest users must provide both 'customer_email' and 'order_number' query parameters"
    664             })),
    665         )
    666             .into_response(),
    667     }
    668 }
    669 
    670 /// List all orders for an authenticated user
    671 async fn list_user_orders(
    672     pool: &PgPool,
    673     user_email: &str,
    674     params: &OrderQueryParams,
    675 ) -> axum::response::Response {
    676     let page = params.page.unwrap_or(1).max(1);
    677     let page_size = params.page_size.unwrap_or(20).clamp(1, 100);
    678     let result = db::list_orders_by_email(
    679         pool,
    680         user_email,
    681         params.status.as_deref(),
    682         params.payment_status.as_deref(),
    683         page,
    684         page_size,
    685     )
    686     .await;
    687 
    688     match result {
    689         Ok((orders, total_count)) => {
    690             let total_pages = ((total_count as f64) / (page_size as f64)).ceil() as i32;
    691 
    692             let response = OrdersResponse {
    693                 orders,
    694                 total_count,
    695                 page,
    696                 page_size,
    697                 total_pages,
    698             };
    699 
    700             (StatusCode::OK, Json(response)).into_response()
    701         }
    702         Err(e) => {
    703             tracing::error!(error = %e, "Database error fetching orders");
    704             (
    705                 StatusCode::INTERNAL_SERVER_ERROR,
    706                 Json(serde_json::json!({
    707                     "error": "Failed to fetch orders",
    708                     "details": e.to_string()
    709                 })),
    710             )
    711                 .into_response()
    712         }
    713     }
    714 }
    715 
    716 /// Get a single order for a guest user
    717 async fn get_guest_order(
    718     pool: &PgPool,
    719     email: &str,
    720     order_number: &str,
    721 ) -> axum::response::Response {
    722     let result = db::get_order_by_email_and_number(pool, email, order_number).await;
    723 
    724     match result {
    725         Ok(Some(order)) => {
    726             // Return as single-item list for consistency
    727             let response = OrdersResponse {
    728                 orders: vec![order],
    729                 total_count: 1,
    730                 page: 1,
    731                 page_size: 1,
    732                 total_pages: 1,
    733             };
    734             (StatusCode::OK, Json(response)).into_response()
    735         }
    736         Ok(None) => (
    737             StatusCode::NOT_FOUND,
    738             Json(serde_json::json!({
    739                 "error": "Order not found or email does not match"
    740             })),
    741         )
    742             .into_response(),
    743         Err(e) => {
    744             tracing::error!(error = %e, "Database error fetching order");
    745             (
    746                 StatusCode::INTERNAL_SERVER_ERROR,
    747                 Json(serde_json::json!({
    748                     "error": "Failed to fetch order",
    749                     "details": e.to_string()
    750                 })),
    751             )
    752                 .into_response()
    753         }
    754     }
    755 }
    756 
    757 /// Get order details by UUID
    758 ///
    759 /// # Endpoint
    760 /// `GET /orders/{uuid}`
    761 pub async fn get_order_by_id(
    762     State(state): State<AppState>,
    763     Path(uuid): Path<Uuid>,
    764 ) -> impl IntoResponse {
    765     let result = db::get_order_by_uuid(state.pool(), uuid).await;
    766 
    767     match result {
    768         Ok(Some(order)) => (StatusCode::OK, Json(order)).into_response(),
    769         Ok(None) => (
    770             StatusCode::NOT_FOUND,
    771             Json(serde_json::json!({
    772                 "error": "Order not found",
    773                 "uuid": uuid.to_string()
    774             })),
    775         )
    776             .into_response(),
    777         Err(e) => {
    778             tracing::error!(error = %e, "Database error fetching order");
    779             (
    780                 StatusCode::INTERNAL_SERVER_ERROR,
    781                 Json(serde_json::json!({
    782                     "error": "Failed to fetch order",
    783                     "details": e.to_string()
    784                 })),
    785             )
    786                 .into_response()
    787         }
    788     }
    789 }
    790 
    791 // ---------------------------------------------------------------------------
    792 // Metric helper functions
    793 // ---------------------------------------------------------------------------
    794 
    795 /// Records a checkout failure with the given reason.
    796 ///
    797 /// Increments the failure counter and records the duration histogram
    798 /// with `outcome=failure` and a `failure.reason` attribute.
    799 fn record_checkout_failure(reason: &str, start: &Instant) {
    800     let duration = start.elapsed().as_secs_f64();
    801     metrics()
    802         .checkout_failures
    803         .add(1, &[KeyValue::new("failure.reason", reason.to_string())]);
    804     metrics().checkout_duration.record(
    805         duration,
    806         &[
    807             KeyValue::new("outcome", "failure"),
    808             KeyValue::new("failure.reason", reason.to_string()),
    809         ],
    810     );
    811 }
    812 
    813 /// Produces a short hex hash of the input string (e.g. an email address)
    814 /// for use as a PII-safe span attribute.
    815 #[allow(dead_code)]
    816 fn hash_short(input: &str) -> String {
    817     use std::collections::hash_map::DefaultHasher;
    818     use std::hash::{Hash, Hasher};
    819     let mut hasher = DefaultHasher::new();
    820     input.hash(&mut hasher);
    821     format!("{:x}", hasher.finish())
    822 }
    823 
    824 /// Converts a `BigDecimal` to `f64` for metric recording.
    825 /// Falls back to 0.0 if the conversion is lossy.
    826 fn bigdecimal_to_f64(value: &BigDecimal) -> f64 {
    827     use std::str::FromStr;
    828     f64::from_str(&value.to_string()).unwrap_or(0.0)
    829 }