app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

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 }