exercises

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

pricing.rs (5614B)


      1 //! Pricing repository functions with OpenTelemetry instrumentation
      2 //!
      3 //! This module contains database operations for product pricing,
      4 //! separated from HTTP handlers.
      5 
      6 use bigdecimal::BigDecimal;
      7 use sqlx::{PgPool, Postgres, QueryBuilder};
      8 use tracing::instrument;
      9 use uuid::Uuid;
     10 
     11 use crate::models::{PricingQueryParams, ProductPricing};
     12 
     13 /// List pricing with pagination and filtering
     14 ///
     15 /// Queries active pricing records with optional filters for
     16 /// product UUID, price range, and discount presence.
     17 #[instrument(
     18     name = "SELECT product_pricing",
     19     skip(pool, params),
     20     fields(
     21         otel.kind = "client",
     22         db.system.name = "postgresql",
     23         db.namespace = "inventory",
     24         db.operation.name = "SELECT",
     25         db.collection.name = "product_pricing",
     26         db.query.text = "SELECT * FROM product_pricing WHERE is_active = true ... LIMIT ... OFFSET ...",
     27         otelmart.page = page,
     28         otelmart.page_size = page_size
     29     )
     30 )]
     31 pub async fn list_pricing(
     32     pool: &PgPool,
     33     params: &PricingQueryParams,
     34     page: i32,
     35     page_size: i32,
     36     offset: i32,
     37 ) -> Result<(Vec<ProductPricing>, i64), sqlx::Error> {
     38     // Build count query
     39     let mut count_builder: QueryBuilder<Postgres> =
     40         QueryBuilder::new("SELECT COUNT(*) FROM product_pricing WHERE is_active = true");
     41 
     42     // Apply filters (all use AND since WHERE is_active is already present)
     43     apply_pricing_filters(&mut count_builder, params);
     44 
     45     let total_count: i64 = count_builder.build_query_scalar().fetch_one(pool).await?;
     46 
     47     // Build main query with same filters
     48     let mut query_builder: QueryBuilder<Postgres> =
     49         QueryBuilder::new("SELECT * FROM product_pricing WHERE is_active = true");
     50 
     51     apply_pricing_filters(&mut query_builder, params);
     52 
     53     // Add ordering and pagination
     54     query_builder.push(" ORDER BY updated_at DESC LIMIT ");
     55     query_builder.push_bind(page_size);
     56     query_builder.push(" OFFSET ");
     57     query_builder.push_bind(offset);
     58 
     59     let pricing = query_builder.build_query_as().fetch_all(pool).await?;
     60 
     61     Ok((pricing, total_count))
     62 }
     63 
     64 /// Get pricing for a specific product by UUID
     65 #[instrument(
     66     name = "SELECT product_pricing",
     67     skip(pool),
     68     fields(
     69         otel.kind = "client",
     70         db.system.name = "postgresql",
     71         db.namespace = "inventory",
     72         db.operation.name = "SELECT",
     73         db.collection.name = "product_pricing",
     74         db.query.text = "SELECT * FROM product_pricing WHERE product_uuid = $1 AND is_active = true",
     75         otelmart.product.uuid = %product_uuid
     76     )
     77 )]
     78 pub async fn get_pricing_by_product(
     79     pool: &PgPool,
     80     product_uuid: Uuid,
     81 ) -> Result<Option<ProductPricing>, sqlx::Error> {
     82     sqlx::query_as::<_, ProductPricing>(
     83         r#"
     84         SELECT * FROM product_pricing
     85         WHERE product_uuid = $1 AND is_active = true
     86         "#,
     87     )
     88     .bind(product_uuid)
     89     .fetch_optional(pool)
     90     .await
     91 }
     92 
     93 /// Deactivate existing pricing and insert new pricing for a product
     94 ///
     95 /// First deactivates any active pricing record, then inserts a new one.
     96 #[instrument(
     97     name = "UPSERT product_pricing",
     98     skip(pool),
     99     fields(
    100         db.system.name = "postgresql",
    101         db.namespace = "inventory",
    102         db.operation.name = "INSERT",
    103         db.collection.name = "product_pricing",
    104         db.query.text = "INSERT INTO product_pricing (...) VALUES (...) RETURNING *",
    105         otelmart.product.uuid = %product_uuid
    106     )
    107 )]
    108 pub async fn upsert_pricing(
    109     pool: &PgPool,
    110     product_uuid: Uuid,
    111     final_price: BigDecimal,
    112     initial_price: Option<BigDecimal>,
    113     currency: String,
    114     price_valid_from: Option<chrono::DateTime<chrono::Utc>>,
    115     price_valid_until: Option<chrono::DateTime<chrono::Utc>>,
    116 ) -> Result<ProductPricing, sqlx::Error> {
    117     // Deactivate existing active pricing
    118     if let Err(e) = sqlx::query(
    119         r#"
    120         UPDATE product_pricing
    121         SET is_active = false
    122         WHERE product_uuid = $1 AND is_active = true
    123         "#,
    124     )
    125     .bind(product_uuid)
    126     .execute(pool)
    127     .await
    128     {
    129         tracing::error!(error = %e, "Error deactivating old pricing");
    130     }
    131 
    132     // Insert new pricing
    133     sqlx::query_as::<_, ProductPricing>(
    134         r#"
    135         INSERT INTO product_pricing (
    136             product_uuid,
    137             final_price,
    138             initial_price,
    139             currency,
    140             price_valid_from,
    141             price_valid_until,
    142             is_active
    143         )
    144         VALUES ($1, $2, $3, $4, $5, $6, true)
    145         RETURNING *
    146         "#,
    147     )
    148     .bind(product_uuid)
    149     .bind(final_price)
    150     .bind(initial_price)
    151     .bind(currency)
    152     .bind(price_valid_from)
    153     .bind(price_valid_until)
    154     .fetch_one(pool)
    155     .await
    156 }
    157 
    158 /// Apply pricing filters to a query builder
    159 ///
    160 /// Shared filter logic for count and main queries.
    161 fn apply_pricing_filters<'a>(
    162     query: &mut QueryBuilder<'a, Postgres>,
    163     params: &'a PricingQueryParams,
    164 ) {
    165     if let Some(product_uuid) = params.product_uuid {
    166         query.push(" AND product_uuid = ");
    167         query.push_bind(product_uuid);
    168     }
    169 
    170     if let Some(ref min_price) = params.min_price {
    171         query.push(" AND final_price >= ");
    172         query.push_bind(min_price);
    173     }
    174 
    175     if let Some(ref max_price) = params.max_price {
    176         query.push(" AND final_price <= ");
    177         query.push_bind(max_price);
    178     }
    179 
    180     if let Some(has_discount) = params.has_discount {
    181         if has_discount {
    182             query.push(" AND discount_percentage > 0");
    183         } else {
    184             query.push(" AND (discount_percentage IS NULL OR discount_percentage = 0)");
    185         }
    186     }
    187 }