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 }