order_detail.rs (7530B)
1 use radroots_app_view::{ 2 OrderDetailItemRow, OrderId, ProductPricePresentation, TradeEconomicsProjection, 3 TradeValidationReceiptProjection, TradeValidationReceiptProofSystem, 4 TradeValidationReceiptResult, TradeValidationReceiptType, 5 }; 6 use rusqlite::{Connection, params}; 7 8 use crate::AppSqliteError; 9 10 pub(super) fn order_detail_item_row( 11 title: String, 12 quantity_display: String, 13 quantity_value: i64, 14 quantity_unit_label: String, 15 unit_price_minor_units: Option<u32>, 16 price_currency: Option<String>, 17 ) -> Result<OrderDetailItemRow, AppSqliteError> { 18 let quantity = 19 u32::try_from(quantity_value).map_err(|_| AppSqliteError::InvalidProjection { 20 reason: "order detail item quantity must be non-negative", 21 })?; 22 let currency_code = price_currency 23 .as_deref() 24 .map(normalize_currency_code) 25 .unwrap_or_else(|| normalize_currency_code("")); 26 let line_total_minor_units = unit_price_minor_units 27 .map(|amount| { 28 amount 29 .checked_mul(quantity) 30 .ok_or(AppSqliteError::InvalidProjection { 31 reason: "order detail line total overflowed", 32 }) 33 }) 34 .transpose()?; 35 let unit_price = unit_price_minor_units.map(|amount_minor_units| ProductPricePresentation { 36 amount_minor_units, 37 currency_code, 38 unit_label: quantity_unit_label.trim().to_owned(), 39 }); 40 41 Ok(OrderDetailItemRow { 42 title, 43 quantity_display, 44 unit_price, 45 line_total_minor_units, 46 }) 47 } 48 49 pub(super) fn order_detail_economics( 50 items: &[OrderDetailItemRow], 51 ) -> Result<TradeEconomicsProjection, AppSqliteError> { 52 let mut total_minor_units = 0_u32; 53 let mut currency_code = None::<String>; 54 55 for item in items { 56 let (Some(unit_price), Some(line_total_minor_units)) = 57 (item.unit_price.as_ref(), item.line_total_minor_units) 58 else { 59 return Ok(TradeEconomicsProjection::default()); 60 }; 61 if let Some(existing_currency) = currency_code.as_deref() { 62 if existing_currency != unit_price.currency_code.as_str() { 63 return Ok(TradeEconomicsProjection::default()); 64 } 65 } else { 66 currency_code = Some(unit_price.currency_code.clone()); 67 } 68 total_minor_units = total_minor_units 69 .checked_add(line_total_minor_units) 70 .ok_or(AppSqliteError::InvalidProjection { 71 reason: "order detail total overflowed", 72 })?; 73 } 74 75 Ok( 76 currency_code.map_or_else(TradeEconomicsProjection::default, |currency_code| { 77 TradeEconomicsProjection { 78 subtotal_minor_units: Some(total_minor_units), 79 discount_total_minor_units: None, 80 adjustment_total_minor_units: None, 81 total_minor_units: Some(total_minor_units), 82 currency_code: Some(currency_code), 83 } 84 }), 85 ) 86 } 87 88 pub(super) fn order_validation_receipts( 89 connection: &Connection, 90 order_id: OrderId, 91 ) -> Result<Vec<TradeValidationReceiptProjection>, AppSqliteError> { 92 let mut statement = connection 93 .prepare( 94 "SELECT 95 event_id, 96 result, 97 receipt_type, 98 proof_system, 99 event_set_root, 100 reducer_output_root, 101 public_values_hash, 102 target_event_id, 103 event_created_at 104 FROM order_validation_receipts 105 WHERE order_id = ?1 106 ORDER BY event_created_at DESC, event_id DESC", 107 ) 108 .map_err(|source| AppSqliteError::Query { 109 operation: "prepare order validation receipts", 110 source, 111 })?; 112 let rows = statement 113 .query_map(params![order_id.to_string()], |row| { 114 Ok(( 115 row.get::<_, String>(0)?, 116 row.get::<_, String>(1)?, 117 row.get::<_, String>(2)?, 118 row.get::<_, String>(3)?, 119 row.get::<_, String>(4)?, 120 row.get::<_, String>(5)?, 121 row.get::<_, String>(6)?, 122 row.get::<_, String>(7)?, 123 row.get::<_, i64>(8)?, 124 )) 125 }) 126 .map_err(|source| AppSqliteError::Query { 127 operation: "query order validation receipts", 128 source, 129 })?; 130 let mut receipts = Vec::new(); 131 132 for row in rows { 133 let ( 134 event_id, 135 result, 136 receipt_type, 137 proof_system, 138 event_set_root, 139 reducer_output_root, 140 public_values_hash, 141 target_event_id, 142 event_created_at, 143 ) = row.map_err(|source| AppSqliteError::Query { 144 operation: "read order validation receipt", 145 source, 146 })?; 147 148 receipts.push(TradeValidationReceiptProjection { 149 event_id, 150 result: parse_validation_receipt_result(result)?, 151 receipt_type: parse_validation_receipt_type(receipt_type)?, 152 proof_system: parse_validation_receipt_proof_system(proof_system)?, 153 event_set_root, 154 reducer_output_root, 155 public_values_hash, 156 target_event_id, 157 recorded_at: u64::try_from(event_created_at).map_err(|_| { 158 AppSqliteError::InvalidProjection { 159 reason: "order_validation_receipts.event_created_at must be non-negative", 160 } 161 })?, 162 }); 163 } 164 165 Ok(receipts) 166 } 167 168 fn normalize_currency_code(value: &str) -> String { 169 let trimmed = value.trim(); 170 if trimmed.is_empty() { 171 "USD".to_owned() 172 } else { 173 trimmed.to_ascii_uppercase() 174 } 175 } 176 177 fn parse_validation_receipt_result( 178 value: String, 179 ) -> Result<TradeValidationReceiptResult, AppSqliteError> { 180 match value.as_str() { 181 "valid" => Ok(TradeValidationReceiptResult::Valid), 182 "needs_review" => Ok(TradeValidationReceiptResult::NeedsReview), 183 _ => Err(AppSqliteError::DecodeEnum { 184 field: "order_validation_receipts.result", 185 value, 186 }), 187 } 188 } 189 190 fn parse_validation_receipt_type( 191 value: String, 192 ) -> Result<TradeValidationReceiptType, AppSqliteError> { 193 match value.as_str() { 194 "listing_validation" => Ok(TradeValidationReceiptType::ListingValidation), 195 "trade_transition" => Ok(TradeValidationReceiptType::TradeTransition), 196 "inventory_state" => Ok(TradeValidationReceiptType::InventoryState), 197 "state_checkpoint" => Ok(TradeValidationReceiptType::StateCheckpoint), 198 _ => Err(AppSqliteError::DecodeEnum { 199 field: "order_validation_receipts.receipt_type", 200 value, 201 }), 202 } 203 } 204 205 fn parse_validation_receipt_proof_system( 206 value: String, 207 ) -> Result<TradeValidationReceiptProofSystem, AppSqliteError> { 208 match value.as_str() { 209 "none" => Ok(TradeValidationReceiptProofSystem::None), 210 "sp1_core" => Ok(TradeValidationReceiptProofSystem::Sp1Core), 211 "sp1_compressed" => Ok(TradeValidationReceiptProofSystem::Sp1Compressed), 212 "sp1_groth16" => Ok(TradeValidationReceiptProofSystem::Sp1Groth16), 213 "sp1_plonk" => Ok(TradeValidationReceiptProofSystem::Sp1Plonk), 214 _ => Err(AppSqliteError::DecodeEnum { 215 field: "order_validation_receipts.proof_system", 216 value, 217 }), 218 } 219 }