app

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

buyer.rs (159072B)


      1 use std::collections::{BTreeMap, BTreeSet};
      2 
      3 use radroots_app_view::{
      4     BuyerCartLineProjection, BuyerCartProjection, BuyerCartReplaceConfirmationProjection,
      5     BuyerContext, BuyerListingRow, BuyerListingsProjection, BuyerOrderDetailProjection,
      6     BuyerOrderReviewDisabledReason, BuyerOrderReviewDraft, BuyerOrderReviewProjection,
      7     BuyerOrderReviewSummaryProjection, BuyerOrderStatus, BuyerOrdersListRow, BuyerOrdersProjection,
      8     BuyerProductDetailProjection, FarmId, FarmOrderMethod, FulfillmentWindowId, OrderDetailItemRow,
      9     OrderId, OrderStatus, ProductAvailabilityState, ProductAvailabilitySummary, ProductId,
     10     ProductPricePresentation, ProductStatus, ProductStockState, ProductStockSummary,
     11     RepeatDemandEligibility, RepeatDemandHandoffProjection,
     12 };
     13 use rusqlite::{Connection, OptionalExtension, params, params_from_iter};
     14 use serde_json::Value;
     15 
     16 use super::{
     17     order_detail::{order_detail_economics, order_detail_item_row, order_validation_receipts},
     18     parse_trade_revision_status,
     19     workflow::{StoredTradeWorkflowSnapshot, trade_workflow_projection_from_storage},
     20 };
     21 use crate::AppSqliteError;
     22 
     23 const BUYER_LOW_STOCK_THRESHOLD: u32 = 3;
     24 
     25 #[derive(Clone, Debug, Eq, PartialEq)]
     26 pub enum BuyerRepeatDemandApplyOutcome {
     27     Applied,
     28     ConfirmationRequired(BuyerCartReplaceConfirmationProjection),
     29     Unavailable,
     30 }
     31 
     32 #[derive(Clone, Debug, Default, Eq, PartialEq)]
     33 pub struct SelectedBuyerOrderScope {
     34     context_keys: Vec<String>,
     35 }
     36 
     37 impl SelectedBuyerOrderScope {
     38     pub fn from_buyer_context(context: &BuyerContext) -> Self {
     39         Self::from_context_keys([context.storage_key()])
     40     }
     41 
     42     pub fn for_selected_account(
     43         account_id: impl AsRef<str>,
     44         selected_account_pubkey: Option<&str>,
     45     ) -> Self {
     46         let mut context_keys = Vec::new();
     47         let account_id = account_id.as_ref().trim();
     48         if !account_id.is_empty() {
     49             context_keys.push(format!("account:{account_id}"));
     50         }
     51         if let Some(pubkey) = selected_account_pubkey.and_then(normalized_nostr_pubkey) {
     52             context_keys.push(format!("nostr:{pubkey}"));
     53         }
     54         Self::from_context_keys(context_keys)
     55     }
     56 
     57     fn from_context_keys(context_keys: impl IntoIterator<Item = String>) -> Self {
     58         let mut unique = BTreeSet::new();
     59         let context_keys = context_keys
     60             .into_iter()
     61             .map(|key| key.trim().to_owned())
     62             .filter(|key| !key.is_empty())
     63             .filter(|key| unique.insert(key.clone()))
     64             .collect();
     65         Self { context_keys }
     66     }
     67 
     68     fn context_keys(&self) -> &[String] {
     69         self.context_keys.as_slice()
     70     }
     71 }
     72 
     73 fn normalized_nostr_pubkey(pubkey: &str) -> Option<String> {
     74     let pubkey = pubkey.trim().to_ascii_lowercase();
     75     if pubkey.len() == 64 && pubkey.bytes().all(|byte| byte.is_ascii_hexdigit()) {
     76         Some(pubkey)
     77     } else {
     78         None
     79     }
     80 }
     81 
     82 #[derive(Clone, Debug, Eq, PartialEq)]
     83 pub struct BuyerOrderLocalEventExport {
     84     pub order_id: OrderId,
     85     pub farm_id: FarmId,
     86     pub farm_display_name: String,
     87     pub order_number: String,
     88     pub status: String,
     89     pub buyer_context_key: String,
     90     pub buyer_name: String,
     91     pub buyer_email: String,
     92     pub buyer_phone: String,
     93     pub buyer_order_note: String,
     94     pub updated_at: String,
     95     pub fulfillment_window_id: Option<FulfillmentWindowId>,
     96     pub fulfillment_window_label: Option<String>,
     97     pub fulfillment_starts_at: Option<String>,
     98     pub fulfillment_ends_at: Option<String>,
     99     pub lines: Vec<BuyerOrderLocalEventLine>,
    100 }
    101 
    102 #[derive(Clone, Debug, Eq, PartialEq)]
    103 pub struct BuyerOrderLocalEventLine {
    104     pub product_id: ProductId,
    105     pub title: String,
    106     pub quantity: u32,
    107     pub quantity_unit_label: String,
    108     pub quantity_display: String,
    109     pub unit_price_minor_units: Option<u32>,
    110     pub price_currency: String,
    111     pub listing_bin_id: Option<String>,
    112     pub farm_key: Option<String>,
    113     pub listing_addr: Option<String>,
    114     pub listing_event_id: Option<String>,
    115     pub listing_relays: Vec<String>,
    116     pub seller_pubkey: Option<String>,
    117 }
    118 
    119 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
    120 pub enum BuyerOrderCoordinationState {
    121     Pending,
    122     Synced,
    123     Failed,
    124 }
    125 
    126 impl BuyerOrderCoordinationState {
    127     fn from_storage_key(field: &'static str, value: String) -> Result<Self, AppSqliteError> {
    128         match value.as_str() {
    129             "pending" => Ok(Self::Pending),
    130             "synced" => Ok(Self::Synced),
    131             "failed" => Ok(Self::Failed),
    132             _ => Err(AppSqliteError::DecodeEnum { field, value }),
    133         }
    134     }
    135 }
    136 
    137 #[derive(Clone, Debug, Eq, PartialEq)]
    138 pub struct BuyerOrderCoordinationRecord {
    139     pub order_id: OrderId,
    140     pub buyer_context_key: String,
    141     pub record_id: Option<String>,
    142     pub state: BuyerOrderCoordinationState,
    143     pub payload_json: Option<String>,
    144     pub attempt_count: u32,
    145     pub last_error_message: Option<String>,
    146 }
    147 
    148 pub struct AppBuyerRepository<'a> {
    149     connection: &'a Connection,
    150 }
    151 
    152 impl<'a> AppBuyerRepository<'a> {
    153     pub const fn new(connection: &'a Connection) -> Self {
    154         Self { connection }
    155     }
    156 
    157     pub fn load_buyer_listings(
    158         &self,
    159         search_query: &str,
    160         fulfillment_methods: &BTreeSet<FarmOrderMethod>,
    161     ) -> Result<BuyerListingsProjection, AppSqliteError> {
    162         let now_utc = self.current_utc_timestamp()?;
    163         let normalized_search = normalize_search_query(search_query);
    164         let mut records = self.load_listing_records()?;
    165 
    166         records.retain(|record| {
    167             record.is_buyer_visible(&now_utc)
    168                 && record.matches_search(normalized_search.as_deref())
    169                 && record.matches_fulfillment_methods(fulfillment_methods)
    170         });
    171         sort_listing_records(&mut records, &now_utc);
    172 
    173         Ok(BuyerListingsProjection {
    174             rows: records
    175                 .into_iter()
    176                 .map(|record| record.into_listing_row(&now_utc))
    177                 .collect::<Result<Vec<_>, _>>()?,
    178         })
    179     }
    180 
    181     pub fn load_buyer_product_detail(
    182         &self,
    183         product_id: ProductId,
    184     ) -> Result<Option<BuyerProductDetailProjection>, AppSqliteError> {
    185         let now_utc = self.current_utc_timestamp()?;
    186 
    187         self.load_listing_record_by_id(product_id)?
    188             .filter(|record| record.is_buyer_visible(&now_utc))
    189             .map(|record| {
    190                 Ok(BuyerProductDetailProjection {
    191                     detail_text: record.detail_text(),
    192                     listing: record.into_listing_row(&now_utc)?,
    193                     selected_quantity: 1,
    194                 })
    195             })
    196             .transpose()
    197     }
    198 
    199     pub fn load_buyer_cart(
    200         &self,
    201         context: &BuyerContext,
    202     ) -> Result<BuyerCartProjection, AppSqliteError> {
    203         let context_key = context.storage_key();
    204         let header = self.load_cart_header(&context_key)?;
    205         let line_records = self.load_cart_line_records(&context_key)?;
    206 
    207         self.build_cart_projection(header, line_records)
    208     }
    209 
    210     pub fn replace_buyer_cart(
    211         &self,
    212         context: &BuyerContext,
    213         cart: &BuyerCartProjection,
    214     ) -> Result<(), AppSqliteError> {
    215         validate_cart_projection(cart)?;
    216         let context_key = context.storage_key();
    217         let farm_id = if cart.lines.is_empty() {
    218             None
    219         } else {
    220             cart.farm_id
    221         };
    222 
    223         self.connection
    224             .execute(
    225                 "insert into buyer_carts (
    226                     buyer_context_key,
    227                     farm_id,
    228                     updated_at
    229                  ) values (?1, ?2, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
    230                  on conflict(buyer_context_key) do update set
    231                     farm_id = excluded.farm_id,
    232                     updated_at = excluded.updated_at",
    233                 params![context_key.as_str(), farm_id.map(|id| id.to_string())],
    234             )
    235             .map_err(|source| AppSqliteError::Query {
    236                 operation: "save buyer cart header",
    237                 source,
    238             })?;
    239         self.connection
    240             .execute(
    241                 "delete from buyer_cart_lines where buyer_context_key = ?1",
    242                 params![context_key.as_str()],
    243             )
    244             .map_err(|source| AppSqliteError::Query {
    245                 operation: "clear buyer cart lines",
    246                 source,
    247             })?;
    248 
    249         for line in &cart.lines {
    250             let snapshot = self.load_buyer_cart_line_snapshot(line.product_id)?;
    251             let listing_relays_json = encode_listing_relays(&snapshot.listing_relays)?;
    252             self.connection
    253                 .execute(
    254                     "insert into buyer_cart_lines (
    255                         buyer_context_key,
    256                         product_id,
    257                         quantity,
    258                         listing_bin_id,
    259                         quantity_unit_label,
    260                         unit_price_minor_units,
    261                         price_currency,
    262                         farm_key,
    263                         listing_addr,
    264                         listing_event_id,
    265                         listing_relays_json,
    266                         seller_pubkey,
    267                         updated_at
    268                      ) values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))",
    269                     params![
    270                         context_key.as_str(),
    271                         line.product_id.to_string(),
    272                         i64::from(line.quantity),
    273                         snapshot.listing_bin_id.as_deref(),
    274                         line.unit_price.unit_label.as_str(),
    275                         line.unit_price.amount_minor_units,
    276                         normalize_currency_code(&line.unit_price.currency_code),
    277                         snapshot.farm_key.as_deref(),
    278                         snapshot.listing_addr.as_deref(),
    279                         snapshot.listing_event_id.as_deref(),
    280                         listing_relays_json.as_deref(),
    281                         snapshot.seller_pubkey.as_deref(),
    282                     ],
    283                 )
    284                 .map_err(|source| AppSqliteError::Query {
    285                     operation: "save buyer cart line",
    286                     source,
    287                 })?;
    288         }
    289 
    290         Ok(())
    291     }
    292 
    293     pub fn clear_buyer_cart(&self, context: &BuyerContext) -> Result<(), AppSqliteError> {
    294         let context_key = context.storage_key();
    295 
    296         self.connection
    297             .execute(
    298                 "delete from buyer_cart_lines where buyer_context_key = ?1",
    299                 params![context_key.as_str()],
    300             )
    301             .map_err(|source| AppSqliteError::Query {
    302                 operation: "delete buyer cart lines",
    303                 source,
    304             })?;
    305         self.connection
    306             .execute(
    307                 "update buyer_carts
    308                  set
    309                     farm_id = null,
    310                     buyer_order_note = '',
    311                     updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
    312                  where buyer_context_key = ?1",
    313                 params![context_key.as_str()],
    314             )
    315             .map_err(|source| AppSqliteError::Query {
    316                 operation: "clear buyer cart header",
    317                 source,
    318             })?;
    319 
    320         Ok(())
    321     }
    322 
    323     pub fn load_buyer_order_review(
    324         &self,
    325         context: &BuyerContext,
    326     ) -> Result<BuyerOrderReviewProjection, AppSqliteError> {
    327         let context_key = context.storage_key();
    328         let header = self.load_cart_header(&context_key)?;
    329         let cart =
    330             self.build_cart_projection(header.clone(), self.load_cart_line_records(&context_key)?)?;
    331         let draft = header
    332             .map(BuyerCartHeader::into_order_review_draft)
    333             .unwrap_or_default();
    334         let fulfillment_summary = shared_fulfillment_summary(&cart.lines);
    335         let place_order_disabled_reason = buyer_order_review_disabled_reason(
    336             context,
    337             &cart,
    338             fulfillment_summary.as_ref(),
    339             &draft,
    340         );
    341 
    342         Ok(BuyerOrderReviewProjection {
    343             draft: draft.clone(),
    344             summary: BuyerOrderReviewSummaryProjection {
    345                 farm_display_name: cart.farm_display_name.clone(),
    346                 fulfillment_summary: fulfillment_summary.clone(),
    347                 line_count: cart.lines.len() as u32,
    348                 subtotal_minor_units: cart.subtotal_minor_units,
    349                 currency_code: cart.currency_code.clone(),
    350             },
    351             can_place_order: place_order_disabled_reason.is_none(),
    352             place_order_disabled_reason,
    353         })
    354     }
    355 
    356     pub fn save_buyer_order_review_draft(
    357         &self,
    358         context: &BuyerContext,
    359         draft: &BuyerOrderReviewDraft,
    360     ) -> Result<(), AppSqliteError> {
    361         let context_key = context.storage_key();
    362 
    363         self.connection
    364             .execute(
    365                 "insert into buyer_carts (
    366                     buyer_context_key,
    367                     farm_id,
    368                     updated_at
    369                  ) values (?1, null, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
    370                  on conflict(buyer_context_key) do nothing",
    371                 params![context_key.as_str()],
    372             )
    373             .map_err(|source| AppSqliteError::Query {
    374                 operation: "ensure buyer order review header",
    375                 source,
    376             })?;
    377         self.connection
    378             .execute(
    379                 "update buyer_carts
    380                  set
    381                     buyer_name = ?2,
    382                     buyer_email = ?3,
    383                     buyer_phone = ?4,
    384                     buyer_order_note = ?5,
    385                     updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
    386                  where buyer_context_key = ?1",
    387                 params![
    388                     context_key.as_str(),
    389                     draft.name.trim(),
    390                     draft.email.trim(),
    391                     draft.phone.trim(),
    392                     draft.order_note.trim(),
    393                 ],
    394             )
    395             .map_err(|source| AppSqliteError::Query {
    396                 operation: "save buyer order review draft",
    397                 source,
    398             })?;
    399 
    400         Ok(())
    401     }
    402 
    403     pub fn place_buyer_order(&self, context: &BuyerContext) -> Result<OrderId, AppSqliteError> {
    404         let context_key = context.storage_key();
    405         let header =
    406             self.load_cart_header(&context_key)?
    407                 .ok_or(AppSqliteError::InvalidProjection {
    408                     reason: "buyer cart header is missing",
    409                 })?;
    410         let line_records = self.load_cart_line_records(&context_key)?;
    411         let cart = self.build_cart_projection(Some(header.clone()), line_records.clone())?;
    412         let order_review = self.load_buyer_order_review(context)?;
    413 
    414         if let Some(disabled_reason) = order_review.place_order_disabled_reason {
    415             return Err(AppSqliteError::InvalidProjection {
    416                 reason: buyer_order_review_disabled_error(disabled_reason),
    417             });
    418         }
    419 
    420         let farm_id = cart.farm_id.ok_or(AppSqliteError::InvalidProjection {
    421             reason: "buyer cart farm is missing",
    422         })?;
    423         let fulfillment_window_id = shared_fulfillment_window_id(&line_records)?;
    424         let order_id = OrderId::new();
    425         let order_number = self.next_order_number(farm_id)?;
    426 
    427         self.connection
    428             .execute_batch("begin immediate transaction")
    429             .map_err(|source| AppSqliteError::Query {
    430                 operation: "begin buyer order review write",
    431                 source,
    432             })?;
    433 
    434         let result = (|| {
    435             self.connection
    436                 .execute(
    437                     "insert into orders (
    438                         id,
    439                         farm_id,
    440                         fulfillment_window_id,
    441                         order_number,
    442                         customer_display_name,
    443                         status,
    444                         updated_at,
    445                         buyer_context_key,
    446                         buyer_email,
    447                         buyer_phone,
    448                         buyer_order_note
    449                      ) values (
    450                         ?1,
    451                         ?2,
    452                         ?3,
    453                         ?4,
    454                         ?5,
    455                         'needs_action',
    456                         strftime('%Y-%m-%dT%H:%M:%SZ', 'now'),
    457                         ?6,
    458                         ?7,
    459                         ?8,
    460                         ?9
    461                      )",
    462                     params![
    463                         order_id.to_string(),
    464                         farm_id.to_string(),
    465                         fulfillment_window_id.map(|id| id.to_string()),
    466                         order_number,
    467                         order_review.draft.name.trim(),
    468                         context_key.as_str(),
    469                         order_review.draft.email.trim(),
    470                         order_review.draft.phone.trim(),
    471                         order_review.draft.order_note.trim(),
    472                     ],
    473                 )
    474                 .map_err(|source| AppSqliteError::Query {
    475                     operation: "insert buyer order",
    476                     source,
    477                 })?;
    478 
    479             for (index, line) in line_records.iter().enumerate() {
    480                 let listing_relays_json = encode_listing_relays(&line.listing.listing_relays)?;
    481                 self.connection
    482                     .execute(
    483                         "insert into order_lines (
    484                             id,
    485                             order_id,
    486                             title,
    487                             quantity_value,
    488                             quantity_unit_label,
    489                             quantity_display,
    490                             listing_bin_id,
    491                             unit_price_minor_units,
    492                             price_currency,
    493                             farm_key,
    494                             listing_addr,
    495                             listing_event_id,
    496                             listing_relays_json,
    497                             seller_pubkey,
    498                             sort_index
    499                          ) values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)",
    500                         params![
    501                             format!("{}:{}", order_id, line.listing.product_id),
    502                             order_id.to_string(),
    503                             line.listing.title,
    504                             i64::from(line.quantity),
    505                             line.listing.unit_label.as_str(),
    506                             format_quantity_display(line.quantity, &line.listing.unit_label),
    507                             line.listing.listing_bin_id.as_deref(),
    508                             line.listing.price_minor_units,
    509                             normalize_currency_code(&line.listing.price_currency),
    510                             line.listing.farm_key.as_deref(),
    511                             line.listing.listing_addr.as_deref(),
    512                             line.listing.listing_event_id.as_deref(),
    513                             listing_relays_json.as_deref(),
    514                             line.listing.seller_pubkey.as_deref(),
    515                             index as i64,
    516                         ],
    517                     )
    518                     .map_err(|source| AppSqliteError::Query {
    519                         operation: "insert buyer order line",
    520                         source,
    521                     })?;
    522             }
    523 
    524             self.connection
    525                 .execute(
    526                     "delete from buyer_cart_lines where buyer_context_key = ?1",
    527                     params![context_key.as_str()],
    528                 )
    529                 .map_err(|source| AppSqliteError::Query {
    530                     operation: "clear buyer cart lines after order review",
    531                     source,
    532                 })?;
    533             self.connection
    534                 .execute(
    535                     "update buyer_carts
    536                      set
    537                         farm_id = null,
    538                         buyer_order_note = '',
    539                         updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
    540                      where buyer_context_key = ?1",
    541                     params![context_key.as_str()],
    542                 )
    543                 .map_err(|source| AppSqliteError::Query {
    544                     operation: "reset buyer cart header after order review",
    545                     source,
    546                 })?;
    547             self.insert_pending_buyer_order_coordination(context_key.as_str(), order_id)?;
    548 
    549             Ok(order_id)
    550         })();
    551 
    552         match result {
    553             Ok(order_id) => {
    554                 self.connection.execute_batch("commit").map_err(|source| {
    555                     AppSqliteError::Query {
    556                         operation: "commit buyer order review write",
    557                         source,
    558                     }
    559                 })?;
    560                 Ok(order_id)
    561             }
    562             Err(error) => {
    563                 let _ = self.connection.execute_batch("rollback");
    564                 Err(error)
    565             }
    566         }
    567     }
    568 
    569     pub fn load_buyer_order_coordination_record(
    570         &self,
    571         context: &BuyerContext,
    572         order_id: OrderId,
    573     ) -> Result<Option<BuyerOrderCoordinationRecord>, AppSqliteError> {
    574         let context_key = context.storage_key();
    575 
    576         self.connection
    577             .query_row(
    578                 "select
    579                     order_id,
    580                     buyer_context_key,
    581                     record_id,
    582                     state,
    583                     payload_json,
    584                     attempt_count,
    585                     last_error_message
    586                  from buyer_order_coordination_records
    587                  where buyer_context_key = ?1 and order_id = ?2
    588                  limit 1",
    589                 params![context_key.as_str(), order_id.to_string()],
    590                 |row| {
    591                     Ok((
    592                         row.get::<_, String>(0)?,
    593                         row.get::<_, String>(1)?,
    594                         row.get::<_, Option<String>>(2)?,
    595                         row.get::<_, String>(3)?,
    596                         row.get::<_, Option<String>>(4)?,
    597                         row.get::<_, i64>(5)?,
    598                         row.get::<_, Option<String>>(6)?,
    599                     ))
    600                 },
    601             )
    602             .optional()
    603             .map_err(|source| AppSqliteError::Query {
    604                 operation: "load buyer order coordination record",
    605                 source,
    606             })?
    607             .map(buyer_order_coordination_record_from_row)
    608             .transpose()
    609     }
    610 
    611     pub fn load_recoverable_buyer_order_coordination_records(
    612         &self,
    613         context: &BuyerContext,
    614     ) -> Result<Vec<BuyerOrderCoordinationRecord>, AppSqliteError> {
    615         let context_key = context.storage_key();
    616         let mut statement = self
    617             .connection
    618             .prepare(
    619                 "select
    620                     order_id,
    621                     buyer_context_key,
    622                     record_id,
    623                     state,
    624                     payload_json,
    625                     attempt_count,
    626                     last_error_message
    627                  from buyer_order_coordination_records
    628                  where buyer_context_key = ?1 and state in ('pending', 'failed')
    629                  order by updated_at asc, order_id asc",
    630             )
    631             .map_err(|source| AppSqliteError::Query {
    632                 operation: "prepare recoverable buyer order coordination records",
    633                 source,
    634             })?;
    635         let rows = statement
    636             .query_map(params![context_key.as_str()], |row| {
    637                 Ok((
    638                     row.get::<_, String>(0)?,
    639                     row.get::<_, String>(1)?,
    640                     row.get::<_, Option<String>>(2)?,
    641                     row.get::<_, String>(3)?,
    642                     row.get::<_, Option<String>>(4)?,
    643                     row.get::<_, i64>(5)?,
    644                     row.get::<_, Option<String>>(6)?,
    645                 ))
    646             })
    647             .map_err(|source| AppSqliteError::Query {
    648                 operation: "query recoverable buyer order coordination records",
    649                 source,
    650             })?;
    651 
    652         rows.map(|row| {
    653             buyer_order_coordination_record_from_row(row.map_err(|source| {
    654                 AppSqliteError::Query {
    655                     operation: "read recoverable buyer order coordination record",
    656                     source,
    657                 }
    658             })?)
    659         })
    660         .collect()
    661     }
    662 
    663     pub fn buyer_order_coordination_is_synced(
    664         &self,
    665         context: &BuyerContext,
    666         order_id: OrderId,
    667     ) -> Result<bool, AppSqliteError> {
    668         Ok(self
    669             .load_buyer_order_coordination_record(context, order_id)?
    670             .is_some_and(|record| record.state == BuyerOrderCoordinationState::Synced))
    671     }
    672 
    673     pub fn prepare_buyer_order_coordination_attempt(
    674         &self,
    675         context: &BuyerContext,
    676         order_id: OrderId,
    677         record_id: &str,
    678         payload_json: &str,
    679     ) -> Result<bool, AppSqliteError> {
    680         let context_key = context.storage_key();
    681         let changed = self
    682             .connection
    683             .execute(
    684                 "insert into buyer_order_coordination_records (
    685                     order_id,
    686                     buyer_context_key,
    687                     record_id,
    688                     state,
    689                     payload_json,
    690                     attempt_count,
    691                     last_error_message,
    692                     created_at,
    693                     updated_at,
    694                     synced_at
    695                  ) values (
    696                     ?1,
    697                     ?2,
    698                     ?3,
    699                     'pending',
    700                     ?4,
    701                     1,
    702                     null,
    703                     strftime('%Y-%m-%dT%H:%M:%SZ', 'now'),
    704                     strftime('%Y-%m-%dT%H:%M:%SZ', 'now'),
    705                     null
    706                  )
    707                  on conflict(order_id) do update set
    708                     record_id = excluded.record_id,
    709                     state = 'pending',
    710                     payload_json = excluded.payload_json,
    711                     attempt_count = buyer_order_coordination_records.attempt_count + 1,
    712                     last_error_message = null,
    713                     updated_at = excluded.updated_at,
    714                     synced_at = null
    715                  where buyer_order_coordination_records.buyer_context_key = excluded.buyer_context_key
    716                     and buyer_order_coordination_records.state <> 'synced'",
    717                 params![
    718                     order_id.to_string(),
    719                     context_key.as_str(),
    720                     record_id,
    721                     payload_json
    722                 ],
    723             )
    724             .map_err(|source| AppSqliteError::Query {
    725                 operation: "prepare buyer order coordination attempt",
    726                 source,
    727             })?;
    728 
    729         Ok(changed == 1)
    730     }
    731 
    732     pub fn mark_buyer_order_coordination_synced(
    733         &self,
    734         context: &BuyerContext,
    735         order_id: OrderId,
    736     ) -> Result<bool, AppSqliteError> {
    737         let context_key = context.storage_key();
    738         let changed = self
    739             .connection
    740             .execute(
    741                 "update buyer_order_coordination_records
    742                  set
    743                     state = 'synced',
    744                     last_error_message = null,
    745                     updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now'),
    746                     synced_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
    747                  where buyer_context_key = ?1 and order_id = ?2",
    748                 params![context_key.as_str(), order_id.to_string()],
    749             )
    750             .map_err(|source| AppSqliteError::Query {
    751                 operation: "mark buyer order coordination synced",
    752                 source,
    753             })?;
    754 
    755         Ok(changed == 1)
    756     }
    757 
    758     pub fn mark_buyer_order_coordination_failed(
    759         &self,
    760         context: &BuyerContext,
    761         order_id: OrderId,
    762         error_message: &str,
    763     ) -> Result<bool, AppSqliteError> {
    764         let context_key = context.storage_key();
    765         let changed = self
    766             .connection
    767             .execute(
    768                 "update buyer_order_coordination_records
    769                  set
    770                     state = 'failed',
    771                     last_error_message = ?3,
    772                     updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now'),
    773                     synced_at = null
    774                  where buyer_context_key = ?1 and order_id = ?2",
    775                 params![context_key.as_str(), order_id.to_string(), error_message],
    776             )
    777             .map_err(|source| AppSqliteError::Query {
    778                 operation: "mark buyer order coordination failed",
    779                 source,
    780             })?;
    781 
    782         Ok(changed == 1)
    783     }
    784 
    785     pub fn load_buyer_orders(
    786         &self,
    787         context: &BuyerContext,
    788     ) -> Result<BuyerOrdersProjection, AppSqliteError> {
    789         self.load_buyer_orders_for_scope(&SelectedBuyerOrderScope::from_buyer_context(context))
    790     }
    791 
    792     pub fn load_buyer_orders_for_scope(
    793         &self,
    794         scope: &SelectedBuyerOrderScope,
    795     ) -> Result<BuyerOrdersProjection, AppSqliteError> {
    796         let now_utc = self.current_utc_timestamp()?;
    797         let visible_listings = self.visible_listing_index(&now_utc)?;
    798         let context_keys = scope.context_keys();
    799         if context_keys.is_empty() {
    800             return Ok(BuyerOrdersProjection::default());
    801         }
    802         let placeholders = sql_placeholders(context_keys.len());
    803         let query = format!(
    804             "select
    805                 o.id,
    806                 o.farm_id,
    807                 o.order_number,
    808                 o.status,
    809                 o.workflow_revision,
    810                 o.workflow_agreement,
    811                 o.workflow_inventory,
    812                 o.workflow_provenance_source,
    813                 o.workflow_provenance_last_event_id,
    814                 f.display_name,
    815                 fw.label,
    816                 fw.starts_at,
    817                 fw.ends_at
    818              from orders o
    819              inner join farms f on f.id = o.farm_id
    820              left join fulfillment_windows fw on fw.id = o.fulfillment_window_id
    821              where o.buyer_context_key in ({placeholders})
    822              order by o.updated_at desc, o.id desc"
    823         );
    824         let mut statement =
    825             self.connection
    826                 .prepare(query.as_str())
    827                 .map_err(|source| AppSqliteError::Query {
    828                     operation: "prepare buyer orders list",
    829                     source,
    830                 })?;
    831         let rows = statement
    832             .query_map(params_from_iter(context_keys.iter()), |row| {
    833                 Ok((
    834                     row.get::<_, String>(0)?,
    835                     row.get::<_, String>(1)?,
    836                     row.get::<_, String>(2)?,
    837                     row.get::<_, String>(3)?,
    838                     row.get::<_, String>(4)?,
    839                     row.get::<_, String>(5)?,
    840                     row.get::<_, String>(6)?,
    841                     row.get::<_, String>(7)?,
    842                     row.get::<_, Option<String>>(8)?,
    843                     row.get::<_, String>(9)?,
    844                     row.get::<_, Option<String>>(10)?,
    845                     row.get::<_, Option<String>>(11)?,
    846                     row.get::<_, Option<String>>(12)?,
    847                 ))
    848             })
    849             .map_err(|source| AppSqliteError::Query {
    850                 operation: "query buyer orders list",
    851                 source,
    852             })?;
    853         let mut orders = Vec::new();
    854 
    855         for row in rows {
    856             let (
    857                 order_id,
    858                 farm_id,
    859                 order_number,
    860                 status,
    861                 workflow_revision,
    862                 workflow_agreement,
    863                 workflow_inventory,
    864                 workflow_provenance_source,
    865                 workflow_provenance_last_event_id,
    866                 farm_display_name,
    867                 fulfillment_label,
    868                 fulfillment_starts_at,
    869                 fulfillment_ends_at,
    870             ) = row.map_err(|source| AppSqliteError::Query {
    871                 operation: "read buyer orders list",
    872                 source,
    873             })?;
    874             let order_id: OrderId = parse_typed_id("orders.id", order_id)?;
    875             let farm_id: FarmId = parse_typed_id("orders.farm_id", farm_id)?;
    876             let buyer_status = BuyerOrderStatus::from(parse_order_status("orders.status", status)?);
    877             let revision =
    878                 parse_trade_revision_status("orders.workflow_revision", workflow_revision)?;
    879             let items = self.load_order_detail_items(order_id.to_string())?;
    880             let economics = order_detail_economics(&items)?;
    881             let workflow = trade_workflow_projection_from_storage(StoredTradeWorkflowSnapshot {
    882                 order_id,
    883                 revision,
    884                 economics,
    885                 agreement: workflow_agreement,
    886                 inventory: workflow_inventory,
    887                 provenance_source: workflow_provenance_source,
    888                 provenance_last_event_id: workflow_provenance_last_event_id,
    889             })?;
    890 
    891             orders.push(BuyerOrdersListRow {
    892                 order_id,
    893                 farm_id,
    894                 order_number,
    895                 repeat_demand: self.build_repeat_demand_handoff(
    896                     order_id,
    897                     farm_id,
    898                     farm_display_name.as_str(),
    899                     &visible_listings,
    900                 )?,
    901                 farm_display_name,
    902                 fulfillment_summary: format_fulfillment_summary(
    903                     fulfillment_label,
    904                     fulfillment_starts_at,
    905                     fulfillment_ends_at,
    906                 ),
    907                 status: buyer_status,
    908                 workflow,
    909             });
    910         }
    911 
    912         Ok(BuyerOrdersProjection { rows: orders })
    913     }
    914 
    915     pub fn load_buyer_order_detail(
    916         &self,
    917         context: &BuyerContext,
    918         order_id: OrderId,
    919     ) -> Result<Option<BuyerOrderDetailProjection>, AppSqliteError> {
    920         self.load_buyer_order_detail_for_scope(
    921             &SelectedBuyerOrderScope::from_buyer_context(context),
    922             order_id,
    923         )
    924     }
    925 
    926     pub fn load_buyer_order_detail_for_scope(
    927         &self,
    928         scope: &SelectedBuyerOrderScope,
    929         order_id: OrderId,
    930     ) -> Result<Option<BuyerOrderDetailProjection>, AppSqliteError> {
    931         let now_utc = self.current_utc_timestamp()?;
    932         let visible_listings = self.visible_listing_index(&now_utc)?;
    933         let context_keys = scope.context_keys();
    934         if context_keys.is_empty() {
    935             return Ok(None);
    936         }
    937         let placeholders = sql_placeholders(context_keys.len());
    938         let query = format!(
    939             "select
    940                 o.id,
    941                 o.farm_id,
    942                 o.order_number,
    943                 o.status,
    944                 o.buyer_order_note,
    945                 o.workflow_revision,
    946                 o.workflow_agreement,
    947                 o.workflow_inventory,
    948                 o.workflow_provenance_source,
    949                 o.workflow_provenance_last_event_id,
    950                 f.display_name,
    951                 fw.label,
    952                 fw.starts_at,
    953                 fw.ends_at
    954              from orders o
    955              inner join farms f on f.id = o.farm_id
    956              left join fulfillment_windows fw on fw.id = o.fulfillment_window_id
    957              where o.buyer_context_key in ({placeholders}) and o.id = ?
    958              limit 1"
    959         );
    960         let mut params = context_keys.to_vec();
    961         params.push(order_id.to_string());
    962         let record = self
    963             .connection
    964             .query_row(query.as_str(), params_from_iter(params.iter()), |row| {
    965                 Ok((
    966                     row.get::<_, String>(0)?,
    967                     row.get::<_, String>(1)?,
    968                     row.get::<_, String>(2)?,
    969                     row.get::<_, String>(3)?,
    970                     row.get::<_, String>(4)?,
    971                     row.get::<_, String>(5)?,
    972                     row.get::<_, String>(6)?,
    973                     row.get::<_, String>(7)?,
    974                     row.get::<_, String>(8)?,
    975                     row.get::<_, Option<String>>(9)?,
    976                     row.get::<_, String>(10)?,
    977                     row.get::<_, Option<String>>(11)?,
    978                     row.get::<_, Option<String>>(12)?,
    979                     row.get::<_, Option<String>>(13)?,
    980                 ))
    981             })
    982             .optional()
    983             .map_err(|source| AppSqliteError::Query {
    984                 operation: "load buyer order detail",
    985                 source,
    986             })?;
    987 
    988         record
    989             .map(
    990                 |(
    991                     order_id,
    992                     farm_id,
    993                     order_number,
    994                     status,
    995                     order_note,
    996                     workflow_revision,
    997                     workflow_agreement,
    998                     workflow_inventory,
    999                     workflow_provenance_source,
   1000                     workflow_provenance_last_event_id,
   1001                     farm_display_name,
   1002                     fulfillment_label,
   1003                     fulfillment_starts_at,
   1004                     fulfillment_ends_at,
   1005                 )| {
   1006                     let order_id: OrderId = parse_typed_id("orders.id", order_id)?;
   1007                     let farm_id: FarmId = parse_typed_id("orders.farm_id", farm_id)?;
   1008                     let status =
   1009                         BuyerOrderStatus::from(parse_order_status("orders.status", status)?);
   1010                     let revision =
   1011                         parse_trade_revision_status("orders.workflow_revision", workflow_revision)?;
   1012                     let items = self.load_order_detail_items(order_id.to_string())?;
   1013                     let economics = order_detail_economics(&items)?;
   1014                     let workflow =
   1015                         trade_workflow_projection_from_storage(StoredTradeWorkflowSnapshot {
   1016                             order_id,
   1017                             revision,
   1018                             economics: economics.clone(),
   1019                             agreement: workflow_agreement,
   1020                             inventory: workflow_inventory,
   1021                             provenance_source: workflow_provenance_source,
   1022                             provenance_last_event_id: workflow_provenance_last_event_id,
   1023                         })?;
   1024                     let validation_receipts = order_validation_receipts(self.connection, order_id)?;
   1025                     Ok(BuyerOrderDetailProjection {
   1026                         order_id,
   1027                         farm_id,
   1028                         order_number,
   1029                         farm_display_name: farm_display_name.clone(),
   1030                         fulfillment_summary: format_fulfillment_summary(
   1031                             fulfillment_label,
   1032                             fulfillment_starts_at,
   1033                             fulfillment_ends_at,
   1034                         ),
   1035                         status,
   1036                         items,
   1037                         economics,
   1038                         workflow,
   1039                         validation_receipts,
   1040                         order_note: empty_string_to_none(order_note),
   1041                         repeat_demand: self.build_repeat_demand_handoff(
   1042                             order_id,
   1043                             farm_id,
   1044                             farm_display_name.as_str(),
   1045                             &visible_listings,
   1046                         )?,
   1047                     })
   1048                 },
   1049             )
   1050             .transpose()
   1051     }
   1052 
   1053     pub fn load_buyer_order_local_event_export(
   1054         &self,
   1055         context: &BuyerContext,
   1056         order_id: OrderId,
   1057     ) -> Result<Option<BuyerOrderLocalEventExport>, AppSqliteError> {
   1058         let context_key = context.storage_key();
   1059         let Some(record) = self
   1060             .connection
   1061             .query_row(
   1062                 "select
   1063                     o.id,
   1064                     o.farm_id,
   1065                     o.order_number,
   1066                     o.status,
   1067                     o.buyer_context_key,
   1068                     o.customer_display_name,
   1069                     o.buyer_email,
   1070                     o.buyer_phone,
   1071                     o.buyer_order_note,
   1072                     o.updated_at,
   1073                     f.display_name,
   1074                     fw.id,
   1075                     fw.label,
   1076                     fw.starts_at,
   1077                     fw.ends_at
   1078                  from orders o
   1079                  inner join farms f on f.id = o.farm_id
   1080                  left join fulfillment_windows fw on fw.id = o.fulfillment_window_id
   1081                  where o.buyer_context_key = ?1 and o.id = ?2
   1082                  limit 1",
   1083                 params![context_key.as_str(), order_id.to_string()],
   1084                 |row| {
   1085                     Ok((
   1086                         row.get::<_, String>(0)?,
   1087                         row.get::<_, String>(1)?,
   1088                         row.get::<_, String>(2)?,
   1089                         row.get::<_, String>(3)?,
   1090                         row.get::<_, Option<String>>(4)?,
   1091                         row.get::<_, String>(5)?,
   1092                         row.get::<_, String>(6)?,
   1093                         row.get::<_, String>(7)?,
   1094                         row.get::<_, String>(8)?,
   1095                         row.get::<_, String>(9)?,
   1096                         row.get::<_, String>(10)?,
   1097                         row.get::<_, Option<String>>(11)?,
   1098                         row.get::<_, Option<String>>(12)?,
   1099                         row.get::<_, Option<String>>(13)?,
   1100                         row.get::<_, Option<String>>(14)?,
   1101                     ))
   1102                 },
   1103             )
   1104             .optional()
   1105             .map_err(|source| AppSqliteError::Query {
   1106                 operation: "load buyer order local event export header",
   1107                 source,
   1108             })?
   1109         else {
   1110             return Ok(None);
   1111         };
   1112         let (
   1113             order_id,
   1114             farm_id,
   1115             order_number,
   1116             status,
   1117             buyer_context_key,
   1118             buyer_name,
   1119             buyer_email,
   1120             buyer_phone,
   1121             buyer_order_note,
   1122             updated_at,
   1123             farm_display_name,
   1124             fulfillment_window_id,
   1125             fulfillment_window_label,
   1126             fulfillment_starts_at,
   1127             fulfillment_ends_at,
   1128         ) = record;
   1129         let order_id = parse_typed_id("orders.id", order_id)?;
   1130         let farm_id = parse_typed_id("orders.farm_id", farm_id)?;
   1131         let lines = self.load_buyer_order_local_event_lines(order_id)?;
   1132 
   1133         Ok(Some(BuyerOrderLocalEventExport {
   1134             order_id,
   1135             farm_id,
   1136             farm_display_name,
   1137             order_number,
   1138             status,
   1139             buyer_context_key: buyer_context_key.unwrap_or_else(|| context_key.clone()),
   1140             buyer_name,
   1141             buyer_email,
   1142             buyer_phone,
   1143             buyer_order_note,
   1144             updated_at,
   1145             fulfillment_window_id: parse_optional_typed_id(
   1146                 "orders.fulfillment_window_id",
   1147                 fulfillment_window_id,
   1148             )?,
   1149             fulfillment_window_label: empty_string_to_none_option(fulfillment_window_label),
   1150             fulfillment_starts_at,
   1151             fulfillment_ends_at,
   1152             lines,
   1153         }))
   1154     }
   1155 
   1156     pub fn apply_buyer_repeat_demand_to_cart(
   1157         &self,
   1158         context: &BuyerContext,
   1159         order_id: OrderId,
   1160         replace_existing: bool,
   1161     ) -> Result<BuyerRepeatDemandApplyOutcome, AppSqliteError> {
   1162         self.apply_buyer_repeat_demand_from_scope_to_cart(
   1163             &SelectedBuyerOrderScope::from_buyer_context(context),
   1164             context,
   1165             order_id,
   1166             replace_existing,
   1167         )
   1168     }
   1169 
   1170     pub fn apply_buyer_repeat_demand_from_scope_to_cart(
   1171         &self,
   1172         source_scope: &SelectedBuyerOrderScope,
   1173         cart_context: &BuyerContext,
   1174         order_id: OrderId,
   1175         replace_existing: bool,
   1176     ) -> Result<BuyerRepeatDemandApplyOutcome, AppSqliteError> {
   1177         let Some((farm_id, farm_display_name)) =
   1178             self.load_buyer_order_repeat_demand_header_for_scope(source_scope, order_id)?
   1179         else {
   1180             return Ok(BuyerRepeatDemandApplyOutcome::Unavailable);
   1181         };
   1182         let now_utc = self.current_utc_timestamp()?;
   1183         let visible_listings = self.visible_listing_index(&now_utc)?;
   1184         let Some(candidate) = self.build_repeat_demand_candidate(
   1185             order_id,
   1186             farm_id,
   1187             farm_display_name.as_str(),
   1188             &visible_listings,
   1189         )?
   1190         else {
   1191             return Ok(BuyerRepeatDemandApplyOutcome::Unavailable);
   1192         };
   1193         if candidate.available_lines.is_empty() {
   1194             return Ok(BuyerRepeatDemandApplyOutcome::Unavailable);
   1195         }
   1196 
   1197         let current_cart = self.load_buyer_cart(cart_context)?;
   1198         if !replace_existing
   1199             && !current_cart.is_empty()
   1200             && current_cart.farm_id != Some(candidate.farm_id)
   1201         {
   1202             let current_farm_display_name = current_cart
   1203                 .farm_display_name
   1204                 .clone()
   1205                 .or_else(|| {
   1206                     current_cart
   1207                         .lines
   1208                         .first()
   1209                         .map(|line| line.farm_display_name.clone())
   1210                 })
   1211                 .ok_or(AppSqliteError::InvalidProjection {
   1212                     reason: "buyer cart farm display name is missing",
   1213                 })?;
   1214 
   1215             return Ok(BuyerRepeatDemandApplyOutcome::ConfirmationRequired(
   1216                 BuyerCartReplaceConfirmationProjection {
   1217                     current_farm_display_name,
   1218                     incoming_farm_display_name: candidate.farm_display_name,
   1219                 },
   1220             ));
   1221         }
   1222 
   1223         let next_cart = next_buyer_cart_for_repeat_demand(
   1224             current_cart,
   1225             candidate.farm_id,
   1226             candidate.farm_display_name.as_str(),
   1227             &candidate.available_lines,
   1228             replace_existing,
   1229         )?;
   1230         self.replace_buyer_cart(cart_context, &next_cart)?;
   1231 
   1232         Ok(BuyerRepeatDemandApplyOutcome::Applied)
   1233     }
   1234 
   1235     fn build_cart_projection(
   1236         &self,
   1237         header: Option<BuyerCartHeader>,
   1238         line_records: Vec<BuyerCartLineRecord>,
   1239     ) -> Result<BuyerCartProjection, AppSqliteError> {
   1240         let mut lines = Vec::with_capacity(line_records.len());
   1241         let mut subtotal_minor_units = 0_u32;
   1242         let mut currency_code = None;
   1243 
   1244         for line_record in line_records {
   1245             let line_projection = line_record.into_projection()?;
   1246             subtotal_minor_units = subtotal_minor_units
   1247                 .checked_add(line_projection.line_total_minor_units)
   1248                 .ok_or(AppSqliteError::InvalidProjection {
   1249                     reason: "buyer cart subtotal overflowed",
   1250                 })?;
   1251 
   1252             if currency_code.is_none() {
   1253                 currency_code = Some(line_projection.unit_price.currency_code.clone());
   1254             }
   1255             lines.push(line_projection);
   1256         }
   1257 
   1258         let farm_id = if lines.is_empty() {
   1259             None
   1260         } else {
   1261             lines.first().map(|line| line.farm_id)
   1262         }
   1263         .or(header.as_ref().and_then(|header| header.farm_id));
   1264         let farm_display_name = if let Some(line) = lines.first() {
   1265             Some(line.farm_display_name.clone())
   1266         } else if let Some(farm_id) = farm_id {
   1267             self.load_farm_display_name(farm_id)?
   1268         } else {
   1269             None
   1270         };
   1271         let has_lines = !lines.is_empty();
   1272 
   1273         Ok(BuyerCartProjection {
   1274             farm_id,
   1275             farm_display_name,
   1276             lines,
   1277             subtotal_minor_units: has_lines.then_some(subtotal_minor_units),
   1278             currency_code: has_lines.then_some(currency_code.unwrap_or_default()),
   1279             replace_confirmation: None,
   1280         })
   1281     }
   1282 
   1283     fn current_utc_timestamp(&self) -> Result<String, AppSqliteError> {
   1284         self.connection
   1285             .query_row("select strftime('%Y-%m-%dT%H:%M:%SZ', 'now')", [], |row| {
   1286                 row.get(0)
   1287             })
   1288             .map_err(|source| AppSqliteError::Query {
   1289                 operation: "load buyer current utc timestamp",
   1290                 source,
   1291             })
   1292     }
   1293 
   1294     fn insert_pending_buyer_order_coordination(
   1295         &self,
   1296         context_key: &str,
   1297         order_id: OrderId,
   1298     ) -> Result<(), AppSqliteError> {
   1299         self.connection
   1300             .execute(
   1301                 "insert into buyer_order_coordination_records (
   1302                     order_id,
   1303                     buyer_context_key,
   1304                     state,
   1305                     attempt_count,
   1306                     created_at,
   1307                     updated_at
   1308                  ) values (
   1309                     ?1,
   1310                     ?2,
   1311                     'pending',
   1312                     0,
   1313                     strftime('%Y-%m-%dT%H:%M:%SZ', 'now'),
   1314                     strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
   1315                  )",
   1316                 params![order_id.to_string(), context_key],
   1317             )
   1318             .map_err(|source| AppSqliteError::Query {
   1319                 operation: "insert pending buyer order coordination record",
   1320                 source,
   1321             })?;
   1322 
   1323         Ok(())
   1324     }
   1325 
   1326     fn load_listing_records(&self) -> Result<Vec<BuyerListingRecord>, AppSqliteError> {
   1327         let mut statement = self
   1328             .connection
   1329             .prepare(
   1330                 "select
   1331                     p.id,
   1332                     p.farm_id,
   1333                     f.display_name,
   1334                     f.readiness,
   1335                     p.title,
   1336                     p.subtitle,
   1337                     p.status,
   1338                     p.unit_label,
   1339                     p.price_minor_units,
   1340                     p.price_currency,
   1341                     p.listing_bin_id,
   1342                     (
   1343                         select li.farm_key
   1344                         from local_interop_imports li
   1345                         where li.projected_kind = 'listing'
   1346                            and li.projected_id = p.id
   1347                         order by li.local_seq desc
   1348                         limit 1
   1349                     ),
   1350                     (
   1351                         select li.listing_addr
   1352                         from local_interop_imports li
   1353                         where li.projected_kind = 'listing'
   1354                            and li.projected_id = p.id
   1355                            and li.listing_addr is not null
   1356                            and trim(li.listing_addr) <> ''
   1357                         order by li.local_seq desc
   1358                         limit 1
   1359                     ),
   1360                     (
   1361                         select li.event_id
   1362                         from local_interop_imports li
   1363                         where li.projected_kind = 'listing'
   1364                            and li.projected_id = p.id
   1365                            and li.event_id is not null
   1366                            and trim(li.event_id) <> ''
   1367                         order by li.local_seq desc
   1368                         limit 1
   1369                     ),
   1370                     (
   1371                         select li.owner_pubkey
   1372                         from local_interop_imports li
   1373                         where li.projected_kind = 'listing'
   1374                            and li.projected_id = p.id
   1375                            and li.owner_pubkey is not null
   1376                            and trim(li.owner_pubkey) <> ''
   1377                         order by li.local_seq desc
   1378                         limit 1
   1379                     ),
   1380                     (
   1381                         select li.relay_delivery_json
   1382                         from local_interop_imports li
   1383                         where li.projected_kind = 'listing'
   1384                            and li.projected_id = p.id
   1385                            and li.relay_delivery_json is not null
   1386                            and trim(li.relay_delivery_json) <> ''
   1387                         order by li.local_seq desc
   1388                         limit 1
   1389                     ),
   1390                     p.stock_count,
   1391                     fw.id,
   1392                     fw.label,
   1393                     fw.starts_at,
   1394                     fw.ends_at,
   1395                     fw.pickup_location_id,
   1396                     coalesce((
   1397                         select max(afs.pickup_enabled)
   1398                         from account_farm_setups afs
   1399                         where afs.saved_farm_id = p.farm_id
   1400                     ), 0),
   1401                     coalesce((
   1402                         select max(afs.delivery_enabled)
   1403                         from account_farm_setups afs
   1404                         where afs.saved_farm_id = p.farm_id
   1405                     ), 0),
   1406                     coalesce((
   1407                         select max(afs.shipping_enabled)
   1408                         from account_farm_setups afs
   1409                         where afs.saved_farm_id = p.farm_id
   1410                     ), 0)
   1411                  from products p
   1412                  inner join farms f on f.id = p.farm_id
   1413                  left join fulfillment_windows fw on fw.id = p.availability_window_id",
   1414             )
   1415             .map_err(|source| AppSqliteError::Query {
   1416                 operation: "prepare buyer listings query",
   1417                 source,
   1418             })?;
   1419         let rows = statement
   1420             .query_map([], |row| {
   1421                 Ok((
   1422                     row.get::<_, String>(0)?,
   1423                     row.get::<_, String>(1)?,
   1424                     row.get::<_, String>(2)?,
   1425                     row.get::<_, String>(3)?,
   1426                     row.get::<_, String>(4)?,
   1427                     row.get::<_, String>(5)?,
   1428                     row.get::<_, String>(6)?,
   1429                     row.get::<_, String>(7)?,
   1430                     row.get::<_, Option<u32>>(8)?,
   1431                     row.get::<_, String>(9)?,
   1432                     row.get::<_, Option<String>>(10)?,
   1433                     row.get::<_, Option<String>>(11)?,
   1434                     row.get::<_, Option<String>>(12)?,
   1435                     row.get::<_, Option<String>>(13)?,
   1436                     row.get::<_, Option<String>>(14)?,
   1437                     row.get::<_, Option<String>>(15)?,
   1438                     row.get::<_, Option<u32>>(16)?,
   1439                     row.get::<_, Option<String>>(17)?,
   1440                     row.get::<_, Option<String>>(18)?,
   1441                     row.get::<_, Option<String>>(19)?,
   1442                     row.get::<_, Option<String>>(20)?,
   1443                     row.get::<_, Option<String>>(21)?,
   1444                     row.get::<_, i64>(22)?,
   1445                     row.get::<_, i64>(23)?,
   1446                     row.get::<_, i64>(24)?,
   1447                 ))
   1448             })
   1449             .map_err(|source| AppSqliteError::Query {
   1450                 operation: "query buyer listings",
   1451                 source,
   1452             })?;
   1453         let mut records = Vec::new();
   1454 
   1455         for row in rows {
   1456             let (
   1457                 product_id,
   1458                 farm_id,
   1459                 farm_display_name,
   1460                 farm_readiness,
   1461                 title,
   1462                 subtitle,
   1463                 status,
   1464                 unit_label,
   1465                 price_minor_units,
   1466                 price_currency,
   1467                 listing_bin_id,
   1468                 farm_key,
   1469                 listing_addr,
   1470                 listing_event_id,
   1471                 seller_pubkey,
   1472                 listing_relay_delivery_json,
   1473                 stock_count,
   1474                 fulfillment_window_id,
   1475                 fulfillment_window_label,
   1476                 fulfillment_starts_at,
   1477                 fulfillment_ends_at,
   1478                 pickup_location_id,
   1479                 pickup_enabled,
   1480                 delivery_enabled,
   1481                 shipping_enabled,
   1482             ) = row.map_err(|source| AppSqliteError::Query {
   1483                 operation: "read buyer listings",
   1484                 source,
   1485             })?;
   1486 
   1487             records.push(BuyerListingRecord {
   1488                 product_id: parse_typed_id("products.id", product_id)?,
   1489                 farm_id: parse_typed_id("products.farm_id", farm_id)?,
   1490                 farm_display_name,
   1491                 farm_is_ready: farm_readiness == "ready",
   1492                 title,
   1493                 subtitle: empty_string_to_none(subtitle),
   1494                 status: parse_product_status("products.status", status)?,
   1495                 unit_label,
   1496                 price_minor_units,
   1497                 price_currency,
   1498                 listing_bin_id: listing_bin_id.and_then(empty_string_to_none),
   1499                 farm_key: farm_key.and_then(empty_string_to_none),
   1500                 listing_addr: listing_addr.and_then(empty_string_to_none),
   1501                 listing_event_id: listing_event_id.and_then(empty_string_to_none),
   1502                 listing_relays: listing_relays_from_json(listing_relay_delivery_json)?,
   1503                 seller_pubkey: seller_pubkey.and_then(empty_string_to_none),
   1504                 stock_count,
   1505                 fulfillment_window_id: parse_optional_typed_id(
   1506                     "products.availability_window_id",
   1507                     fulfillment_window_id,
   1508                 )?,
   1509                 fulfillment_window_label: empty_string_to_none_option(fulfillment_window_label),
   1510                 fulfillment_starts_at,
   1511                 fulfillment_ends_at,
   1512                 pickup_location_present: pickup_location_id.is_some(),
   1513                 pickup_enabled: parse_sqlite_bool(
   1514                     "account_farm_setups.pickup_enabled",
   1515                     pickup_enabled,
   1516                 )?,
   1517                 delivery_enabled: parse_sqlite_bool(
   1518                     "account_farm_setups.delivery_enabled",
   1519                     delivery_enabled,
   1520                 )?,
   1521                 shipping_enabled: parse_sqlite_bool(
   1522                     "account_farm_setups.shipping_enabled",
   1523                     shipping_enabled,
   1524                 )?,
   1525             });
   1526         }
   1527 
   1528         Ok(records)
   1529     }
   1530 
   1531     fn load_listing_record_by_id(
   1532         &self,
   1533         product_id: ProductId,
   1534     ) -> Result<Option<BuyerListingRecord>, AppSqliteError> {
   1535         Ok(self
   1536             .load_listing_records()?
   1537             .into_iter()
   1538             .find(|record| record.product_id == product_id))
   1539     }
   1540 
   1541     fn load_buyer_cart_line_snapshot(
   1542         &self,
   1543         product_id: ProductId,
   1544     ) -> Result<BuyerCartLineSnapshot, AppSqliteError> {
   1545         Ok(self
   1546             .load_listing_record_by_id(product_id)?
   1547             .map(|listing| BuyerCartLineSnapshot {
   1548                 listing_bin_id: listing.listing_bin_id,
   1549                 farm_key: listing.farm_key,
   1550                 listing_addr: listing.listing_addr,
   1551                 listing_event_id: listing.listing_event_id,
   1552                 listing_relays: listing.listing_relays,
   1553                 seller_pubkey: listing.seller_pubkey,
   1554             })
   1555             .unwrap_or_default())
   1556     }
   1557 
   1558     fn load_cart_header(
   1559         &self,
   1560         context_key: &str,
   1561     ) -> Result<Option<BuyerCartHeader>, AppSqliteError> {
   1562         self.connection
   1563             .query_row(
   1564                 "select
   1565                     farm_id,
   1566                     buyer_name,
   1567                     buyer_email,
   1568                     buyer_phone,
   1569                     buyer_order_note
   1570                  from buyer_carts
   1571                  where buyer_context_key = ?1
   1572                  limit 1",
   1573                 params![context_key],
   1574                 |row| {
   1575                     Ok((
   1576                         row.get::<_, Option<String>>(0)?,
   1577                         row.get::<_, String>(1)?,
   1578                         row.get::<_, String>(2)?,
   1579                         row.get::<_, String>(3)?,
   1580                         row.get::<_, String>(4)?,
   1581                     ))
   1582                 },
   1583             )
   1584             .optional()
   1585             .map_err(|source| AppSqliteError::Query {
   1586                 operation: "load buyer cart header",
   1587                 source,
   1588             })?
   1589             .map(
   1590                 |(farm_id, buyer_name, buyer_email, buyer_phone, buyer_order_note)| {
   1591                     Ok(BuyerCartHeader {
   1592                         farm_id: parse_optional_typed_id("buyer_carts.farm_id", farm_id)?,
   1593                         buyer_name,
   1594                         buyer_email,
   1595                         buyer_phone,
   1596                         buyer_order_note,
   1597                     })
   1598                 },
   1599             )
   1600             .transpose()
   1601     }
   1602 
   1603     fn load_cart_line_records(
   1604         &self,
   1605         context_key: &str,
   1606     ) -> Result<Vec<BuyerCartLineRecord>, AppSqliteError> {
   1607         let now_utc = self.current_utc_timestamp()?;
   1608         let mut statement = self
   1609             .connection
   1610             .prepare(
   1611                 "select
   1612                     bcl.quantity,
   1613                     p.id,
   1614                     p.farm_id,
   1615                     f.display_name,
   1616                     f.readiness,
   1617                     p.title,
   1618                     p.subtitle,
   1619                     p.status,
   1620                     coalesce(nullif(bcl.quantity_unit_label, ''), p.unit_label),
   1621                     coalesce(bcl.unit_price_minor_units, p.price_minor_units),
   1622                     coalesce(nullif(bcl.price_currency, ''), p.price_currency),
   1623                     coalesce(nullif(bcl.listing_bin_id, ''), p.listing_bin_id),
   1624                     coalesce(nullif(bcl.farm_key, ''), (
   1625                         select li.farm_key
   1626                         from local_interop_imports li
   1627                         where li.projected_kind = 'listing'
   1628                            and li.projected_id = p.id
   1629                         order by li.local_seq desc
   1630                         limit 1
   1631                     )),
   1632                     coalesce(nullif(bcl.listing_addr, ''), (
   1633                         select li.listing_addr
   1634                         from local_interop_imports li
   1635                         where li.projected_kind = 'listing'
   1636                            and li.projected_id = p.id
   1637                            and li.listing_addr is not null
   1638                            and trim(li.listing_addr) <> ''
   1639                         order by li.local_seq desc
   1640                         limit 1
   1641                     )),
   1642                     coalesce(nullif(bcl.listing_event_id, ''), (
   1643                         select li.event_id
   1644                         from local_interop_imports li
   1645                         where li.projected_kind = 'listing'
   1646                            and li.projected_id = p.id
   1647                            and li.event_id is not null
   1648                            and trim(li.event_id) <> ''
   1649                         order by li.local_seq desc
   1650                         limit 1
   1651                     )),
   1652                     coalesce(nullif(bcl.listing_relays_json, ''), (
   1653                         select li.relay_delivery_json
   1654                         from local_interop_imports li
   1655                         where li.projected_kind = 'listing'
   1656                            and li.projected_id = p.id
   1657                            and li.relay_delivery_json is not null
   1658                            and trim(li.relay_delivery_json) <> ''
   1659                         order by li.local_seq desc
   1660                         limit 1
   1661                     )),
   1662                     coalesce(nullif(bcl.seller_pubkey, ''), (
   1663                         select li.owner_pubkey
   1664                         from local_interop_imports li
   1665                         where li.projected_kind = 'listing'
   1666                            and li.projected_id = p.id
   1667                            and li.owner_pubkey is not null
   1668                            and trim(li.owner_pubkey) <> ''
   1669                         order by li.local_seq desc
   1670                         limit 1
   1671                     )),
   1672                     p.stock_count,
   1673                     fw.id,
   1674                     fw.label,
   1675                     fw.starts_at,
   1676                     fw.ends_at,
   1677                     fw.pickup_location_id,
   1678                     coalesce((
   1679                         select max(afs.pickup_enabled)
   1680                         from account_farm_setups afs
   1681                         where afs.saved_farm_id = p.farm_id
   1682                     ), 0),
   1683                     coalesce((
   1684                         select max(afs.delivery_enabled)
   1685                         from account_farm_setups afs
   1686                         where afs.saved_farm_id = p.farm_id
   1687                     ), 0),
   1688                     coalesce((
   1689                         select max(afs.shipping_enabled)
   1690                         from account_farm_setups afs
   1691                         where afs.saved_farm_id = p.farm_id
   1692                     ), 0)
   1693                  from buyer_cart_lines bcl
   1694                  inner join products p on p.id = bcl.product_id
   1695                  inner join farms f on f.id = p.farm_id
   1696                  left join fulfillment_windows fw on fw.id = p.availability_window_id
   1697                  where bcl.buyer_context_key = ?1
   1698                  order by bcl.updated_at desc, p.id desc",
   1699             )
   1700             .map_err(|source| AppSqliteError::Query {
   1701                 operation: "prepare buyer cart lines",
   1702                 source,
   1703             })?;
   1704         let rows = statement
   1705             .query_map(params![context_key], |row| {
   1706                 Ok((
   1707                     row.get::<_, u32>(0)?,
   1708                     row.get::<_, String>(1)?,
   1709                     row.get::<_, String>(2)?,
   1710                     row.get::<_, String>(3)?,
   1711                     row.get::<_, String>(4)?,
   1712                     row.get::<_, String>(5)?,
   1713                     row.get::<_, String>(6)?,
   1714                     row.get::<_, String>(7)?,
   1715                     row.get::<_, String>(8)?,
   1716                     row.get::<_, Option<u32>>(9)?,
   1717                     row.get::<_, String>(10)?,
   1718                     row.get::<_, Option<String>>(11)?,
   1719                     row.get::<_, Option<String>>(12)?,
   1720                     row.get::<_, Option<String>>(13)?,
   1721                     row.get::<_, Option<String>>(14)?,
   1722                     row.get::<_, Option<String>>(15)?,
   1723                     row.get::<_, Option<String>>(16)?,
   1724                     row.get::<_, Option<u32>>(17)?,
   1725                     row.get::<_, Option<String>>(18)?,
   1726                     row.get::<_, Option<String>>(19)?,
   1727                     row.get::<_, Option<String>>(20)?,
   1728                     row.get::<_, Option<String>>(21)?,
   1729                     row.get::<_, Option<String>>(22)?,
   1730                     row.get::<_, i64>(23)?,
   1731                     row.get::<_, i64>(24)?,
   1732                     row.get::<_, i64>(25)?,
   1733                 ))
   1734             })
   1735             .map_err(|source| AppSqliteError::Query {
   1736                 operation: "query buyer cart lines",
   1737                 source,
   1738             })?;
   1739         let mut line_records = Vec::new();
   1740 
   1741         for row in rows {
   1742             let (
   1743                 quantity,
   1744                 product_id,
   1745                 farm_id,
   1746                 farm_display_name,
   1747                 farm_readiness,
   1748                 title,
   1749                 subtitle,
   1750                 status,
   1751                 unit_label,
   1752                 price_minor_units,
   1753                 price_currency,
   1754                 listing_bin_id,
   1755                 farm_key,
   1756                 listing_addr,
   1757                 listing_event_id,
   1758                 listing_relays_json,
   1759                 seller_pubkey,
   1760                 stock_count,
   1761                 fulfillment_window_id,
   1762                 fulfillment_window_label,
   1763                 fulfillment_starts_at,
   1764                 fulfillment_ends_at,
   1765                 pickup_location_id,
   1766                 pickup_enabled,
   1767                 delivery_enabled,
   1768                 shipping_enabled,
   1769             ) = row.map_err(|source| AppSqliteError::Query {
   1770                 operation: "read buyer cart lines",
   1771                 source,
   1772             })?;
   1773             let listing = BuyerListingRecord {
   1774                 product_id: parse_typed_id("products.id", product_id)?,
   1775                 farm_id: parse_typed_id("products.farm_id", farm_id)?,
   1776                 farm_display_name,
   1777                 farm_is_ready: farm_readiness == "ready",
   1778                 title,
   1779                 subtitle: empty_string_to_none(subtitle),
   1780                 status: parse_product_status("products.status", status)?,
   1781                 unit_label,
   1782                 price_minor_units,
   1783                 price_currency,
   1784                 listing_bin_id: listing_bin_id.and_then(empty_string_to_none),
   1785                 farm_key: farm_key.and_then(empty_string_to_none),
   1786                 listing_addr: listing_addr.and_then(empty_string_to_none),
   1787                 listing_event_id: listing_event_id.and_then(empty_string_to_none),
   1788                 listing_relays: listing_relays_from_json(listing_relays_json)?,
   1789                 seller_pubkey: seller_pubkey.and_then(empty_string_to_none),
   1790                 stock_count,
   1791                 fulfillment_window_id: parse_optional_typed_id(
   1792                     "products.availability_window_id",
   1793                     fulfillment_window_id,
   1794                 )?,
   1795                 fulfillment_window_label: empty_string_to_none_option(fulfillment_window_label),
   1796                 fulfillment_starts_at,
   1797                 fulfillment_ends_at,
   1798                 pickup_location_present: pickup_location_id.is_some(),
   1799                 pickup_enabled: parse_sqlite_bool(
   1800                     "account_farm_setups.pickup_enabled",
   1801                     pickup_enabled,
   1802                 )?,
   1803                 delivery_enabled: parse_sqlite_bool(
   1804                     "account_farm_setups.delivery_enabled",
   1805                     delivery_enabled,
   1806                 )?,
   1807                 shipping_enabled: parse_sqlite_bool(
   1808                     "account_farm_setups.shipping_enabled",
   1809                     shipping_enabled,
   1810                 )?,
   1811             };
   1812 
   1813             if listing.is_buyer_visible(&now_utc) {
   1814                 line_records.push(BuyerCartLineRecord { listing, quantity });
   1815             }
   1816         }
   1817 
   1818         Ok(line_records)
   1819     }
   1820 
   1821     fn load_order_detail_items(
   1822         &self,
   1823         order_id: String,
   1824     ) -> Result<Vec<OrderDetailItemRow>, AppSqliteError> {
   1825         let mut statement = self
   1826             .connection
   1827             .prepare(
   1828                 "select
   1829                     title,
   1830                     quantity_display,
   1831                     quantity_value,
   1832                     quantity_unit_label,
   1833                     unit_price_minor_units,
   1834                     price_currency
   1835                  from order_lines
   1836                  where order_id = ?1
   1837                  order by sort_index asc, id asc",
   1838             )
   1839             .map_err(|source| AppSqliteError::Query {
   1840                 operation: "prepare buyer order detail items",
   1841                 source,
   1842             })?;
   1843         let rows = statement
   1844             .query_map(params![order_id], |row| {
   1845                 Ok((
   1846                     row.get::<_, String>(0)?,
   1847                     row.get::<_, String>(1)?,
   1848                     row.get::<_, i64>(2)?,
   1849                     row.get::<_, String>(3)?,
   1850                     row.get::<_, Option<u32>>(4)?,
   1851                     row.get::<_, Option<String>>(5)?,
   1852                 ))
   1853             })
   1854             .map_err(|source| AppSqliteError::Query {
   1855                 operation: "query buyer order detail items",
   1856                 source,
   1857             })?;
   1858         let mut items = Vec::new();
   1859 
   1860         for row in rows {
   1861             let (
   1862                 title,
   1863                 quantity_display,
   1864                 quantity_value,
   1865                 quantity_unit_label,
   1866                 unit_price_minor_units,
   1867                 price_currency,
   1868             ) = row.map_err(|source| AppSqliteError::Query {
   1869                 operation: "read buyer order detail items",
   1870                 source,
   1871             })?;
   1872             items.push(order_detail_item_row(
   1873                 title,
   1874                 quantity_display,
   1875                 quantity_value,
   1876                 quantity_unit_label,
   1877                 unit_price_minor_units,
   1878                 price_currency,
   1879             )?);
   1880         }
   1881 
   1882         Ok(items)
   1883     }
   1884 
   1885     fn load_buyer_order_local_event_lines(
   1886         &self,
   1887         order_id: OrderId,
   1888     ) -> Result<Vec<BuyerOrderLocalEventLine>, AppSqliteError> {
   1889         let order_id_string = order_id.to_string();
   1890         let mut statement = self
   1891             .connection
   1892             .prepare(
   1893                 "select
   1894                     ol.id,
   1895                     ol.title,
   1896                     ol.quantity_value,
   1897                     ol.quantity_unit_label,
   1898                     ol.quantity_display,
   1899                     ol.unit_price_minor_units,
   1900                     ol.price_currency,
   1901                     ol.listing_bin_id,
   1902                     ol.farm_key,
   1903                     ol.listing_addr,
   1904                     ol.listing_event_id,
   1905                     ol.listing_relays_json,
   1906                     ol.seller_pubkey
   1907                  from order_lines ol
   1908                  where ol.order_id = ?1
   1909                  order by ol.sort_index asc, ol.id asc",
   1910             )
   1911             .map_err(|source| AppSqliteError::Query {
   1912                 operation: "prepare buyer order local event lines",
   1913                 source,
   1914             })?;
   1915         let rows = statement
   1916             .query_map(params![order_id_string.as_str()], |row| {
   1917                 Ok((
   1918                     row.get::<_, String>(0)?,
   1919                     row.get::<_, String>(1)?,
   1920                     row.get::<_, i64>(2)?,
   1921                     row.get::<_, String>(3)?,
   1922                     row.get::<_, String>(4)?,
   1923                     row.get::<_, Option<u32>>(5)?,
   1924                     row.get::<_, Option<String>>(6)?,
   1925                     row.get::<_, Option<String>>(7)?,
   1926                     row.get::<_, Option<String>>(8)?,
   1927                     row.get::<_, Option<String>>(9)?,
   1928                     row.get::<_, Option<String>>(10)?,
   1929                     row.get::<_, Option<String>>(11)?,
   1930                     row.get::<_, Option<String>>(12)?,
   1931                 ))
   1932             })
   1933             .map_err(|source| AppSqliteError::Query {
   1934                 operation: "query buyer order local event lines",
   1935                 source,
   1936             })?;
   1937         let mut lines = Vec::new();
   1938 
   1939         for row in rows {
   1940             let (
   1941                 line_id,
   1942                 title,
   1943                 quantity,
   1944                 quantity_unit_label,
   1945                 quantity_display,
   1946                 unit_price_minor_units,
   1947                 price_currency,
   1948                 listing_bin_id,
   1949                 farm_key,
   1950                 listing_addr,
   1951                 listing_event_id,
   1952                 listing_relays_json,
   1953                 seller_pubkey,
   1954             ) = row.map_err(|source| AppSqliteError::Query {
   1955                 operation: "read buyer order local event line",
   1956                 source,
   1957             })?;
   1958             let product_id = parse_order_line_product_id(line_id.as_str(), order_id)?;
   1959             let quantity =
   1960                 u32::try_from(quantity).map_err(|_| AppSqliteError::InvalidProjection {
   1961                     reason: "buyer order local event quantity must be non-negative",
   1962                 })?;
   1963             if quantity == 0 {
   1964                 return Err(AppSqliteError::InvalidProjection {
   1965                     reason: "buyer order local event quantity must be positive",
   1966                 });
   1967             }
   1968 
   1969             lines.push(BuyerOrderLocalEventLine {
   1970                 product_id,
   1971                 title,
   1972                 quantity,
   1973                 quantity_unit_label,
   1974                 quantity_display,
   1975                 unit_price_minor_units,
   1976                 price_currency: price_currency.unwrap_or_else(|| "USD".to_owned()),
   1977                 listing_bin_id: listing_bin_id.and_then(empty_string_to_none),
   1978                 farm_key: farm_key.and_then(empty_string_to_none),
   1979                 listing_addr: listing_addr.and_then(empty_string_to_none),
   1980                 listing_event_id: listing_event_id.and_then(empty_string_to_none),
   1981                 listing_relays: listing_relays_from_json(listing_relays_json)?,
   1982                 seller_pubkey: seller_pubkey.and_then(empty_string_to_none),
   1983             });
   1984         }
   1985 
   1986         Ok(lines)
   1987     }
   1988 
   1989     fn load_repeat_demand_order_lines(
   1990         &self,
   1991         order_id: OrderId,
   1992     ) -> Result<Vec<RepeatDemandOrderLine>, AppSqliteError> {
   1993         let mut statement = self
   1994             .connection
   1995             .prepare(
   1996                 "select id, quantity_value, listing_addr
   1997                  from order_lines
   1998                  where order_id = ?1
   1999                  order by sort_index asc, id asc",
   2000             )
   2001             .map_err(|source| AppSqliteError::Query {
   2002                 operation: "prepare repeat demand order lines",
   2003                 source,
   2004             })?;
   2005         let rows = statement
   2006             .query_map(params![order_id.to_string()], |row| {
   2007                 Ok((
   2008                     row.get::<_, String>(0)?,
   2009                     row.get::<_, i64>(1)?,
   2010                     row.get::<_, Option<String>>(2)?,
   2011                 ))
   2012             })
   2013             .map_err(|source| AppSqliteError::Query {
   2014                 operation: "query repeat demand order lines",
   2015                 source,
   2016             })?;
   2017         let mut order_lines = Vec::new();
   2018 
   2019         for row in rows {
   2020             let (line_id, quantity_value, listing_addr) =
   2021                 row.map_err(|source| AppSqliteError::Query {
   2022                     operation: "read repeat demand order lines",
   2023                     source,
   2024                 })?;
   2025             let product_id = parse_repeat_demand_product_id(line_id.as_str())?;
   2026             let quantity =
   2027                 u32::try_from(quantity_value).map_err(|_| AppSqliteError::InvalidProjection {
   2028                     reason: "repeat demand quantity must be non-negative",
   2029                 })?;
   2030             if quantity == 0 {
   2031                 return Err(AppSqliteError::InvalidProjection {
   2032                     reason: "repeat demand quantity must be positive",
   2033                 });
   2034             }
   2035 
   2036             order_lines.push(RepeatDemandOrderLine {
   2037                 product_id,
   2038                 quantity,
   2039                 listing_addr: listing_addr.and_then(empty_string_to_none),
   2040             });
   2041         }
   2042 
   2043         Ok(order_lines)
   2044     }
   2045 
   2046     fn load_buyer_order_repeat_demand_header_for_scope(
   2047         &self,
   2048         scope: &SelectedBuyerOrderScope,
   2049         order_id: OrderId,
   2050     ) -> Result<Option<(FarmId, String)>, AppSqliteError> {
   2051         let context_keys = scope.context_keys();
   2052         if context_keys.is_empty() {
   2053             return Ok(None);
   2054         }
   2055         let placeholders = sql_placeholders(context_keys.len());
   2056         let query = format!(
   2057             "select o.farm_id, f.display_name
   2058              from orders o
   2059              inner join farms f on f.id = o.farm_id
   2060              where o.buyer_context_key in ({placeholders}) and o.id = ?
   2061              limit 1"
   2062         );
   2063         let mut params = context_keys.to_vec();
   2064         params.push(order_id.to_string());
   2065         self.connection
   2066             .query_row(query.as_str(), params_from_iter(params.iter()), |row| {
   2067                 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
   2068             })
   2069             .optional()
   2070             .map_err(|source| AppSqliteError::Query {
   2071                 operation: "load buyer repeat demand header",
   2072                 source,
   2073             })?
   2074             .map(|(farm_id, farm_display_name)| {
   2075                 Ok((
   2076                     parse_typed_id("orders.farm_id", farm_id)?,
   2077                     farm_display_name,
   2078                 ))
   2079             })
   2080             .transpose()
   2081     }
   2082 
   2083     fn visible_listing_index(&self, now_utc: &str) -> Result<VisibleListingIndex, AppSqliteError> {
   2084         Ok(VisibleListingIndex::from_records(
   2085             self.load_listing_records()?
   2086                 .into_iter()
   2087                 .filter(|record| record.is_buyer_visible(now_utc)),
   2088         ))
   2089     }
   2090 
   2091     fn build_repeat_demand_handoff(
   2092         &self,
   2093         order_id: OrderId,
   2094         farm_id: FarmId,
   2095         farm_display_name: &str,
   2096         visible_listings: &VisibleListingIndex,
   2097     ) -> Result<Option<RepeatDemandHandoffProjection>, AppSqliteError> {
   2098         Ok(self
   2099             .build_repeat_demand_candidate(order_id, farm_id, farm_display_name, visible_listings)?
   2100             .map(|candidate| candidate.handoff))
   2101     }
   2102 
   2103     fn build_repeat_demand_candidate(
   2104         &self,
   2105         order_id: OrderId,
   2106         farm_id: FarmId,
   2107         farm_display_name: &str,
   2108         visible_listings: &VisibleListingIndex,
   2109     ) -> Result<Option<RepeatDemandCandidate>, AppSqliteError> {
   2110         let order_lines = self.load_repeat_demand_order_lines(order_id)?;
   2111         if order_lines.is_empty() {
   2112             return Ok(None);
   2113         }
   2114 
   2115         let mut available_lines = Vec::new();
   2116         let mut unavailable_item_count = 0u32;
   2117 
   2118         for order_line in &order_lines {
   2119             if let Some(listing) = visible_listings.resolve(order_line).filter(|listing| {
   2120                 listing
   2121                     .stock_count
   2122                     .is_some_and(|quantity| quantity >= order_line.quantity)
   2123             }) {
   2124                 available_lines.push(BuyerCartLineRecord {
   2125                     listing,
   2126                     quantity: order_line.quantity,
   2127                 });
   2128             } else {
   2129                 unavailable_item_count = unavailable_item_count.checked_add(1).ok_or(
   2130                     AppSqliteError::InvalidProjection {
   2131                         reason: "repeat demand unavailable count overflowed",
   2132                     },
   2133                 )?;
   2134             }
   2135         }
   2136 
   2137         let available_item_count = available_lines.len() as u32;
   2138         let eligibility = if available_item_count == 0 {
   2139             RepeatDemandEligibility::Unavailable
   2140         } else if unavailable_item_count == 0 {
   2141             RepeatDemandEligibility::Eligible
   2142         } else {
   2143             RepeatDemandEligibility::Partial
   2144         };
   2145         let current_farm = available_lines
   2146             .first()
   2147             .map(|line| (line.listing.farm_id, line.listing.farm_display_name.clone()))
   2148             .unwrap_or_else(|| (farm_id, farm_display_name.to_owned()));
   2149         let (current_farm_id, current_farm_display_name) = current_farm;
   2150 
   2151         Ok(Some(RepeatDemandCandidate {
   2152             farm_id: current_farm_id,
   2153             farm_display_name: current_farm_display_name,
   2154             available_lines,
   2155             handoff: RepeatDemandHandoffProjection {
   2156                 order_id,
   2157                 farm_id: current_farm_id,
   2158                 eligibility,
   2159                 available_item_count,
   2160                 unavailable_item_count,
   2161             },
   2162         }))
   2163     }
   2164 
   2165     fn load_farm_display_name(&self, farm_id: FarmId) -> Result<Option<String>, AppSqliteError> {
   2166         self.connection
   2167             .query_row(
   2168                 "select display_name from farms where id = ?1 limit 1",
   2169                 params![farm_id.to_string()],
   2170                 |row| row.get::<_, String>(0),
   2171             )
   2172             .optional()
   2173             .map_err(|source| AppSqliteError::Query {
   2174                 operation: "load buyer cart farm display name",
   2175                 source,
   2176             })
   2177     }
   2178 
   2179     fn next_order_number(&self, farm_id: FarmId) -> Result<String, AppSqliteError> {
   2180         let max_suffix = self
   2181             .connection
   2182             .query_row(
   2183                 "select coalesce(max(cast(substr(order_number, 3) as integer)), 999)
   2184                  from orders
   2185                  where farm_id = ?1
   2186                    and order_number like 'R-%'
   2187                    and substr(order_number, 3) glob '[0-9]*'",
   2188                 params![farm_id.to_string()],
   2189                 |row| row.get::<_, i64>(0),
   2190             )
   2191             .map_err(|source| AppSqliteError::Query {
   2192                 operation: "load next buyer order number",
   2193                 source,
   2194             })?;
   2195 
   2196         Ok(format!("R-{}", max_suffix + 1))
   2197     }
   2198 }
   2199 
   2200 #[derive(Clone, Debug, Eq, PartialEq)]
   2201 struct BuyerCartHeader {
   2202     farm_id: Option<FarmId>,
   2203     buyer_name: String,
   2204     buyer_email: String,
   2205     buyer_phone: String,
   2206     buyer_order_note: String,
   2207 }
   2208 
   2209 impl BuyerCartHeader {
   2210     fn into_order_review_draft(self) -> BuyerOrderReviewDraft {
   2211         BuyerOrderReviewDraft {
   2212             name: self.buyer_name,
   2213             email: self.buyer_email,
   2214             phone: self.buyer_phone,
   2215             order_note: self.buyer_order_note,
   2216         }
   2217     }
   2218 }
   2219 
   2220 #[derive(Clone, Debug, Eq, PartialEq)]
   2221 struct BuyerListingRecord {
   2222     product_id: ProductId,
   2223     farm_id: FarmId,
   2224     farm_display_name: String,
   2225     farm_is_ready: bool,
   2226     title: String,
   2227     subtitle: Option<String>,
   2228     status: ProductStatus,
   2229     unit_label: String,
   2230     price_minor_units: Option<u32>,
   2231     price_currency: String,
   2232     listing_bin_id: Option<String>,
   2233     farm_key: Option<String>,
   2234     listing_addr: Option<String>,
   2235     listing_event_id: Option<String>,
   2236     listing_relays: Vec<String>,
   2237     seller_pubkey: Option<String>,
   2238     stock_count: Option<u32>,
   2239     fulfillment_window_id: Option<FulfillmentWindowId>,
   2240     fulfillment_window_label: Option<String>,
   2241     fulfillment_starts_at: Option<String>,
   2242     fulfillment_ends_at: Option<String>,
   2243     pickup_location_present: bool,
   2244     pickup_enabled: bool,
   2245     delivery_enabled: bool,
   2246     shipping_enabled: bool,
   2247 }
   2248 
   2249 impl BuyerListingRecord {
   2250     fn is_buyer_visible(&self, now_utc: &str) -> bool {
   2251         self.farm_is_ready
   2252             && self.status == ProductStatus::Published
   2253             && self.stock_count.is_some_and(|quantity| quantity > 0)
   2254             && self.price_minor_units.is_some_and(|amount| amount > 0)
   2255             && !self.unit_label.trim().is_empty()
   2256             && self.fulfillment_window_id.is_some()
   2257             && self
   2258                 .fulfillment_ends_at
   2259                 .as_deref()
   2260                 .is_some_and(|ends_at| ends_at >= now_utc)
   2261             && !self.fulfillment_methods().is_empty()
   2262     }
   2263 
   2264     fn matches_search(&self, search_query: Option<&str>) -> bool {
   2265         let Some(search_query) = search_query else {
   2266             return true;
   2267         };
   2268 
   2269         self.title.to_lowercase().contains(search_query)
   2270             || self
   2271                 .subtitle
   2272                 .as_deref()
   2273                 .is_some_and(|subtitle| subtitle.to_lowercase().contains(search_query))
   2274             || self.farm_display_name.to_lowercase().contains(search_query)
   2275     }
   2276 
   2277     fn matches_fulfillment_methods(&self, selected: &BTreeSet<FarmOrderMethod>) -> bool {
   2278         selected.is_empty()
   2279             || self
   2280                 .fulfillment_methods()
   2281                 .iter()
   2282                 .any(|method| selected.contains(method))
   2283     }
   2284 
   2285     fn detail_text(&self) -> Option<String> {
   2286         self.subtitle.clone()
   2287     }
   2288 
   2289     fn into_listing_row(self, now_utc: &str) -> Result<BuyerListingRow, AppSqliteError> {
   2290         let price = self
   2291             .price_presentation()
   2292             .ok_or(AppSqliteError::InvalidProjection {
   2293                 reason: "buyer listing price is missing",
   2294             })?;
   2295         let availability = self.availability_summary(now_utc)?;
   2296         let stock = self.stock_summary();
   2297         let fulfillment_methods = self.fulfillment_methods();
   2298         let next_fulfillment_window_label = Some(self.fulfillment_summary_label()?);
   2299 
   2300         Ok(BuyerListingRow {
   2301             product_id: self.product_id,
   2302             farm_id: self.farm_id,
   2303             farm_display_name: self.farm_display_name,
   2304             listing_relays: self.listing_relays,
   2305             title: self.title,
   2306             subtitle: self.subtitle,
   2307             price,
   2308             availability,
   2309             stock,
   2310             fulfillment_methods,
   2311             next_fulfillment_window_label,
   2312         })
   2313     }
   2314 
   2315     fn availability_summary(
   2316         &self,
   2317         now_utc: &str,
   2318     ) -> Result<ProductAvailabilitySummary, AppSqliteError> {
   2319         let starts_at =
   2320             self.fulfillment_starts_at
   2321                 .clone()
   2322                 .ok_or(AppSqliteError::InvalidProjection {
   2323                     reason: "buyer listing fulfillment start is missing",
   2324                 })?;
   2325         let ends_at =
   2326             self.fulfillment_ends_at
   2327                 .clone()
   2328                 .ok_or(AppSqliteError::InvalidProjection {
   2329                     reason: "buyer listing fulfillment end is missing",
   2330                 })?;
   2331         let state = if starts_at.as_str() <= now_utc {
   2332             ProductAvailabilityState::Open
   2333         } else {
   2334             ProductAvailabilityState::Scheduled
   2335         };
   2336 
   2337         Ok(ProductAvailabilitySummary {
   2338             state,
   2339             label: self
   2340                 .fulfillment_window_label
   2341                 .clone()
   2342                 .unwrap_or_else(|| format_window_label(&starts_at, &ends_at)),
   2343         })
   2344     }
   2345 
   2346     fn fulfillment_summary_label(&self) -> Result<String, AppSqliteError> {
   2347         match (
   2348             self.fulfillment_window_label.clone(),
   2349             self.fulfillment_starts_at.as_deref(),
   2350             self.fulfillment_ends_at.as_deref(),
   2351         ) {
   2352             (Some(label), _, _) => Ok(label),
   2353             (None, Some(starts_at), Some(ends_at)) => Ok(format_window_label(starts_at, ends_at)),
   2354             _ => Err(AppSqliteError::InvalidProjection {
   2355                 reason: "buyer listing fulfillment summary is missing",
   2356             }),
   2357         }
   2358     }
   2359 
   2360     fn stock_summary(&self) -> ProductStockSummary {
   2361         let quantity = self.stock_count;
   2362         let state = match quantity {
   2363             Some(0) => ProductStockState::SoldOut,
   2364             Some(quantity) if quantity <= BUYER_LOW_STOCK_THRESHOLD => ProductStockState::LowStock,
   2365             Some(_) => ProductStockState::InStock,
   2366             None => ProductStockState::Unset,
   2367         };
   2368 
   2369         ProductStockSummary {
   2370             quantity,
   2371             unit_label: Some(self.unit_label.clone()),
   2372             state,
   2373         }
   2374     }
   2375 
   2376     fn price_presentation(&self) -> Option<ProductPricePresentation> {
   2377         self.price_minor_units
   2378             .filter(|amount| *amount > 0)
   2379             .map(|amount_minor_units| ProductPricePresentation {
   2380                 amount_minor_units,
   2381                 currency_code: normalize_currency_code(&self.price_currency),
   2382                 unit_label: self.unit_label.clone(),
   2383             })
   2384     }
   2385 
   2386     fn fulfillment_methods(&self) -> BTreeSet<FarmOrderMethod> {
   2387         let mut methods = BTreeSet::new();
   2388         if self.pickup_enabled
   2389             || (!self.delivery_enabled && !self.shipping_enabled && self.pickup_location_present)
   2390         {
   2391             methods.insert(FarmOrderMethod::Pickup);
   2392         }
   2393         if self.delivery_enabled {
   2394             methods.insert(FarmOrderMethod::Delivery);
   2395         }
   2396         if self.shipping_enabled {
   2397             methods.insert(FarmOrderMethod::Shipping);
   2398         }
   2399 
   2400         methods
   2401     }
   2402 }
   2403 
   2404 #[derive(Clone, Debug, Eq, PartialEq)]
   2405 struct BuyerCartLineRecord {
   2406     listing: BuyerListingRecord,
   2407     quantity: u32,
   2408 }
   2409 
   2410 #[derive(Clone, Debug, Default, Eq, PartialEq)]
   2411 struct VisibleListingIndex {
   2412     by_product_id: BTreeMap<ProductId, BuyerListingRecord>,
   2413     by_listing_addr: BTreeMap<String, BuyerListingRecord>,
   2414 }
   2415 
   2416 impl VisibleListingIndex {
   2417     fn from_records(records: impl IntoIterator<Item = BuyerListingRecord>) -> Self {
   2418         let mut index = Self::default();
   2419         for record in records {
   2420             if let Some(listing_addr) = record.listing_addr.as_deref() {
   2421                 index
   2422                     .by_listing_addr
   2423                     .insert(listing_addr.to_owned(), record.clone());
   2424             }
   2425             index.by_product_id.insert(record.product_id, record);
   2426         }
   2427         index
   2428     }
   2429 
   2430     fn resolve(&self, order_line: &RepeatDemandOrderLine) -> Option<BuyerListingRecord> {
   2431         self.by_product_id
   2432             .get(&order_line.product_id)
   2433             .or_else(|| {
   2434                 order_line
   2435                     .listing_addr
   2436                     .as_deref()
   2437                     .and_then(|listing_addr| self.by_listing_addr.get(listing_addr))
   2438             })
   2439             .cloned()
   2440     }
   2441 }
   2442 
   2443 #[derive(Clone, Debug, Default, Eq, PartialEq)]
   2444 struct BuyerCartLineSnapshot {
   2445     listing_bin_id: Option<String>,
   2446     farm_key: Option<String>,
   2447     listing_addr: Option<String>,
   2448     listing_event_id: Option<String>,
   2449     listing_relays: Vec<String>,
   2450     seller_pubkey: Option<String>,
   2451 }
   2452 
   2453 impl BuyerCartLineRecord {
   2454     fn into_projection(self) -> Result<BuyerCartLineProjection, AppSqliteError> {
   2455         let unit_price =
   2456             self.listing
   2457                 .price_presentation()
   2458                 .ok_or(AppSqliteError::InvalidProjection {
   2459                     reason: "buyer cart line price is missing",
   2460                 })?;
   2461         let line_total_minor_units = unit_price
   2462             .amount_minor_units
   2463             .checked_mul(self.quantity)
   2464             .ok_or(AppSqliteError::InvalidProjection {
   2465                 reason: "buyer cart line total overflowed",
   2466             })?;
   2467 
   2468         Ok(BuyerCartLineProjection {
   2469             product_id: self.listing.product_id,
   2470             farm_id: self.listing.farm_id,
   2471             farm_display_name: self.listing.farm_display_name.clone(),
   2472             title: self.listing.title.clone(),
   2473             quantity: self.quantity,
   2474             unit_price,
   2475             line_total_minor_units,
   2476             fulfillment_summary: self.listing.fulfillment_summary_label()?,
   2477         })
   2478     }
   2479 }
   2480 
   2481 #[derive(Clone, Debug, Eq, PartialEq)]
   2482 struct RepeatDemandOrderLine {
   2483     product_id: ProductId,
   2484     quantity: u32,
   2485     listing_addr: Option<String>,
   2486 }
   2487 
   2488 #[derive(Clone, Debug, Eq, PartialEq)]
   2489 struct RepeatDemandCandidate {
   2490     farm_id: FarmId,
   2491     farm_display_name: String,
   2492     available_lines: Vec<BuyerCartLineRecord>,
   2493     handoff: RepeatDemandHandoffProjection,
   2494 }
   2495 
   2496 fn validate_cart_projection(cart: &BuyerCartProjection) -> Result<(), AppSqliteError> {
   2497     if cart.lines.is_empty() {
   2498         return Ok(());
   2499     }
   2500 
   2501     let farm_id = cart.farm_id.ok_or(AppSqliteError::InvalidProjection {
   2502         reason: "buyer cart farm is required when cart has lines",
   2503     })?;
   2504 
   2505     for line in &cart.lines {
   2506         if line.quantity == 0 {
   2507             return Err(AppSqliteError::InvalidProjection {
   2508                 reason: "buyer cart quantities must stay positive",
   2509             });
   2510         }
   2511         if line.farm_id != farm_id {
   2512             return Err(AppSqliteError::InvalidProjection {
   2513                 reason: "buyer cart must remain single farm",
   2514             });
   2515         }
   2516     }
   2517 
   2518     Ok(())
   2519 }
   2520 
   2521 fn next_buyer_cart_for_repeat_demand(
   2522     mut current_cart: BuyerCartProjection,
   2523     farm_id: FarmId,
   2524     farm_display_name: &str,
   2525     lines: &[BuyerCartLineRecord],
   2526     replace_existing: bool,
   2527 ) -> Result<BuyerCartProjection, AppSqliteError> {
   2528     if replace_existing || current_cart.is_empty() || current_cart.farm_id != Some(farm_id) {
   2529         current_cart.lines.clear();
   2530     }
   2531 
   2532     current_cart.farm_id = Some(farm_id);
   2533     current_cart.farm_display_name = Some(farm_display_name.to_owned());
   2534     current_cart.replace_confirmation = None;
   2535 
   2536     for line in lines {
   2537         let incoming_line = line.clone().into_projection()?;
   2538 
   2539         if let Some(existing_line) = current_cart
   2540             .lines
   2541             .iter_mut()
   2542             .find(|existing| existing.product_id == incoming_line.product_id)
   2543         {
   2544             existing_line.quantity = existing_line
   2545                 .quantity
   2546                 .checked_add(incoming_line.quantity)
   2547                 .ok_or(AppSqliteError::InvalidProjection {
   2548                     reason: "buyer cart quantity overflow",
   2549                 })?;
   2550             existing_line.line_total_minor_units = existing_line
   2551                 .unit_price
   2552                 .amount_minor_units
   2553                 .checked_mul(existing_line.quantity)
   2554                 .ok_or(AppSqliteError::InvalidProjection {
   2555                     reason: "buyer cart line total overflow",
   2556                 })?;
   2557             existing_line.fulfillment_summary = incoming_line.fulfillment_summary.clone();
   2558             continue;
   2559         }
   2560 
   2561         current_cart.lines.push(incoming_line);
   2562     }
   2563 
   2564     refresh_buyer_cart_summary(&mut current_cart)?;
   2565 
   2566     Ok(current_cart)
   2567 }
   2568 
   2569 fn sql_placeholders(count: usize) -> String {
   2570     std::iter::repeat_n("?", count)
   2571         .collect::<Vec<_>>()
   2572         .join(", ")
   2573 }
   2574 
   2575 fn parse_repeat_demand_product_id(line_id: &str) -> Result<ProductId, AppSqliteError> {
   2576     let Some((_, product_id)) = line_id.rsplit_once(':') else {
   2577         return Err(AppSqliteError::InvalidProjection {
   2578             reason: "repeat demand order line is missing a product id",
   2579         });
   2580     };
   2581 
   2582     parse_typed_id("order_lines.id", product_id.to_owned())
   2583 }
   2584 
   2585 fn parse_order_line_product_id(
   2586     line_id: &str,
   2587     order_id: OrderId,
   2588 ) -> Result<ProductId, AppSqliteError> {
   2589     let order_id = order_id.to_string();
   2590     let prefix = format!("{order_id}:");
   2591     let Some(product_id) = line_id.strip_prefix(prefix.as_str()) else {
   2592         return Err(AppSqliteError::InvalidProjection {
   2593             reason: "buyer order local event line is missing its order id prefix",
   2594         });
   2595     };
   2596 
   2597     parse_typed_id("order_lines.id", product_id.to_owned())
   2598 }
   2599 
   2600 fn refresh_buyer_cart_summary(cart: &mut BuyerCartProjection) -> Result<(), AppSqliteError> {
   2601     if cart.lines.is_empty() {
   2602         cart.subtotal_minor_units = None;
   2603         cart.currency_code = None;
   2604         cart.replace_confirmation = None;
   2605         return Ok(());
   2606     }
   2607 
   2608     let mut subtotal_minor_units = 0_u32;
   2609     let mut currency_code = None;
   2610 
   2611     for line in &cart.lines {
   2612         subtotal_minor_units = subtotal_minor_units
   2613             .checked_add(line.line_total_minor_units)
   2614             .ok_or(AppSqliteError::InvalidProjection {
   2615                 reason: "buyer cart subtotal overflowed",
   2616             })?;
   2617         currency_code.get_or_insert_with(|| line.unit_price.currency_code.clone());
   2618     }
   2619 
   2620     cart.subtotal_minor_units = Some(subtotal_minor_units);
   2621     cart.currency_code = Some(currency_code.unwrap_or_default());
   2622 
   2623     Ok(())
   2624 }
   2625 
   2626 fn shared_fulfillment_summary(lines: &[BuyerCartLineProjection]) -> Option<String> {
   2627     let first = lines.first()?.fulfillment_summary.clone();
   2628 
   2629     lines
   2630         .iter()
   2631         .all(|line| line.fulfillment_summary == first)
   2632         .then_some(first)
   2633 }
   2634 
   2635 fn buyer_order_review_disabled_reason(
   2636     context: &BuyerContext,
   2637     cart: &BuyerCartProjection,
   2638     fulfillment_summary: Option<&String>,
   2639     draft: &BuyerOrderReviewDraft,
   2640 ) -> Option<BuyerOrderReviewDisabledReason> {
   2641     if cart.lines.is_empty() {
   2642         return Some(BuyerOrderReviewDisabledReason::EmptyCart);
   2643     }
   2644     if fulfillment_summary.is_none() {
   2645         return Some(BuyerOrderReviewDisabledReason::MissingFulfillment);
   2646     }
   2647     if draft.name.trim().is_empty() {
   2648         return Some(BuyerOrderReviewDisabledReason::MissingName);
   2649     }
   2650     if draft.email.trim().is_empty() {
   2651         return Some(BuyerOrderReviewDisabledReason::MissingEmail);
   2652     }
   2653     if matches!(context, BuyerContext::Guest) {
   2654         return Some(BuyerOrderReviewDisabledReason::AccountRequired);
   2655     }
   2656     None
   2657 }
   2658 
   2659 fn buyer_order_review_disabled_error(reason: BuyerOrderReviewDisabledReason) -> &'static str {
   2660     match reason {
   2661         BuyerOrderReviewDisabledReason::EmptyCart => "buyer order review cart is empty",
   2662         BuyerOrderReviewDisabledReason::MissingFulfillment => {
   2663             "buyer order review fulfillment is unavailable"
   2664         }
   2665         BuyerOrderReviewDisabledReason::MissingName => "buyer order review buyer name is missing",
   2666         BuyerOrderReviewDisabledReason::MissingEmail => "buyer order review buyer email is missing",
   2667         BuyerOrderReviewDisabledReason::AccountRequired => {
   2668             "buyer order review requires a selected account"
   2669         }
   2670     }
   2671 }
   2672 
   2673 fn shared_fulfillment_window_id(
   2674     lines: &[BuyerCartLineRecord],
   2675 ) -> Result<Option<FulfillmentWindowId>, AppSqliteError> {
   2676     let Some(first) = lines.first() else {
   2677         return Err(AppSqliteError::InvalidProjection {
   2678             reason: "buyer cart must contain at least one line",
   2679         });
   2680     };
   2681     let first_window_id = first.listing.fulfillment_window_id;
   2682 
   2683     if lines
   2684         .iter()
   2685         .all(|line| line.listing.fulfillment_window_id == first_window_id)
   2686     {
   2687         Ok(first_window_id)
   2688     } else {
   2689         Err(AppSqliteError::InvalidProjection {
   2690             reason: "buyer cart must share one fulfillment window at order review",
   2691         })
   2692     }
   2693 }
   2694 
   2695 fn normalize_search_query(search_query: &str) -> Option<String> {
   2696     let trimmed = search_query.trim();
   2697     if trimmed.is_empty() {
   2698         None
   2699     } else {
   2700         Some(trimmed.to_lowercase())
   2701     }
   2702 }
   2703 
   2704 fn sort_listing_records(records: &mut [BuyerListingRecord], now_utc: &str) {
   2705     records.sort_by(|left, right| {
   2706         left.fulfillment_starts_at
   2707             .cmp(&right.fulfillment_starts_at)
   2708             .then_with(|| {
   2709                 left.availability_state(now_utc)
   2710                     .cmp(&right.availability_state(now_utc))
   2711             })
   2712             .then_with(|| {
   2713                 left.farm_display_name
   2714                     .to_lowercase()
   2715                     .cmp(&right.farm_display_name.to_lowercase())
   2716             })
   2717             .then_with(|| left.title.to_lowercase().cmp(&right.title.to_lowercase()))
   2718             .then_with(|| left.product_id.cmp(&right.product_id))
   2719     });
   2720 }
   2721 
   2722 impl BuyerListingRecord {
   2723     fn availability_state(&self, now_utc: &str) -> u8 {
   2724         match self.fulfillment_starts_at.as_deref() {
   2725             Some(starts_at) if starts_at <= now_utc => 0,
   2726             Some(_) => 1,
   2727             None => 2,
   2728         }
   2729     }
   2730 }
   2731 
   2732 fn format_window_label(starts_at: &str, ends_at: &str) -> String {
   2733     let start_date = starts_at.get(0..10);
   2734     let start_time = starts_at.get(11..16);
   2735     let end_date = ends_at.get(0..10);
   2736     let end_time = ends_at.get(11..16);
   2737 
   2738     match (start_date, start_time, end_date, end_time) {
   2739         (Some(start_date), Some(start_time), Some(end_date), Some(end_time))
   2740             if start_date == end_date =>
   2741         {
   2742             format!("{start_date} {start_time}-{end_time} UTC")
   2743         }
   2744         (Some(start_date), Some(start_time), Some(end_date), Some(end_time)) => {
   2745             format!("{start_date} {start_time} UTC to {end_date} {end_time} UTC")
   2746         }
   2747         _ => starts_at.to_owned(),
   2748     }
   2749 }
   2750 
   2751 fn format_fulfillment_summary(
   2752     label: Option<String>,
   2753     starts_at: Option<String>,
   2754     ends_at: Option<String>,
   2755 ) -> String {
   2756     if let Some(label) = empty_string_to_none_option(label) {
   2757         return label;
   2758     }
   2759 
   2760     match (starts_at.as_deref(), ends_at.as_deref()) {
   2761         (Some(starts_at), Some(ends_at)) => format_window_label(starts_at, ends_at),
   2762         _ => "Fulfillment pending".to_owned(),
   2763     }
   2764 }
   2765 
   2766 fn format_quantity_display(quantity: u32, unit_label: &str) -> String {
   2767     let trimmed = unit_label.trim();
   2768     if trimmed.is_empty() {
   2769         quantity.to_string()
   2770     } else {
   2771         format!("{quantity} {trimmed}")
   2772     }
   2773 }
   2774 
   2775 fn normalize_currency_code(value: &str) -> String {
   2776     let trimmed = value.trim();
   2777     if trimmed.is_empty() {
   2778         "USD".to_owned()
   2779     } else {
   2780         trimmed.to_ascii_uppercase()
   2781     }
   2782 }
   2783 
   2784 fn parse_typed_id<T>(field: &'static str, value: String) -> Result<T, AppSqliteError>
   2785 where
   2786     T: std::str::FromStr,
   2787 {
   2788     value
   2789         .parse()
   2790         .map_err(|_| AppSqliteError::DecodeId { field, value })
   2791 }
   2792 
   2793 fn buyer_order_coordination_record_from_row(
   2794     row: (
   2795         String,
   2796         String,
   2797         Option<String>,
   2798         String,
   2799         Option<String>,
   2800         i64,
   2801         Option<String>,
   2802     ),
   2803 ) -> Result<BuyerOrderCoordinationRecord, AppSqliteError> {
   2804     let (order_id, buyer_context_key, record_id, state, payload_json, attempt_count, last_error) =
   2805         row;
   2806     let attempt_count =
   2807         u32::try_from(attempt_count).map_err(|_| AppSqliteError::InvalidProjection {
   2808             reason: "buyer order coordination attempt count must be non-negative",
   2809         })?;
   2810 
   2811     Ok(BuyerOrderCoordinationRecord {
   2812         order_id: parse_typed_id("buyer_order_coordination_records.order_id", order_id)?,
   2813         buyer_context_key,
   2814         record_id: record_id.and_then(empty_string_to_none),
   2815         state: BuyerOrderCoordinationState::from_storage_key(
   2816             "buyer_order_coordination_records.state",
   2817             state,
   2818         )?,
   2819         payload_json: payload_json.and_then(empty_string_to_none),
   2820         attempt_count,
   2821         last_error_message: last_error.and_then(empty_string_to_none),
   2822     })
   2823 }
   2824 
   2825 fn parse_optional_typed_id<T>(
   2826     field: &'static str,
   2827     value: Option<String>,
   2828 ) -> Result<Option<T>, AppSqliteError>
   2829 where
   2830     T: std::str::FromStr,
   2831 {
   2832     value.map(|value| parse_typed_id(field, value)).transpose()
   2833 }
   2834 
   2835 fn parse_sqlite_bool(field: &'static str, value: i64) -> Result<bool, AppSqliteError> {
   2836     match value {
   2837         0 => Ok(false),
   2838         1 => Ok(true),
   2839         _ => Err(AppSqliteError::DecodeEnum {
   2840             field,
   2841             value: value.to_string(),
   2842         }),
   2843     }
   2844 }
   2845 
   2846 fn parse_product_status(
   2847     field: &'static str,
   2848     value: String,
   2849 ) -> Result<ProductStatus, AppSqliteError> {
   2850     match value.as_str() {
   2851         "draft" => Ok(ProductStatus::Draft),
   2852         "published" => Ok(ProductStatus::Published),
   2853         "paused" => Ok(ProductStatus::Paused),
   2854         "archived" => Ok(ProductStatus::Archived),
   2855         _ => Err(AppSqliteError::DecodeEnum { field, value }),
   2856     }
   2857 }
   2858 
   2859 fn parse_order_status(field: &'static str, value: String) -> Result<OrderStatus, AppSqliteError> {
   2860     match value.as_str() {
   2861         "needs_action" => Ok(OrderStatus::NeedsAction),
   2862         "scheduled" => Ok(OrderStatus::Scheduled),
   2863         "packed" => Ok(OrderStatus::Packed),
   2864         "completed" => Ok(OrderStatus::Completed),
   2865         "declined" => Ok(OrderStatus::Declined),
   2866         "needs_review" => Ok(OrderStatus::NeedsReview),
   2867         _ => Err(AppSqliteError::DecodeEnum { field, value }),
   2868     }
   2869 }
   2870 
   2871 fn empty_string_to_none(value: String) -> Option<String> {
   2872     let trimmed = value.trim();
   2873     if trimmed.is_empty() {
   2874         None
   2875     } else {
   2876         Some(trimmed.to_owned())
   2877     }
   2878 }
   2879 
   2880 fn empty_string_to_none_option(value: Option<String>) -> Option<String> {
   2881     value.and_then(empty_string_to_none)
   2882 }
   2883 
   2884 fn encode_listing_relays(relays: &[String]) -> Result<Option<String>, AppSqliteError> {
   2885     let relays = normalized_listing_relays(relays.iter().map(String::as_str));
   2886     if relays.is_empty() {
   2887         return Ok(None);
   2888     }
   2889 
   2890     serde_json::to_string(&relays)
   2891         .map(Some)
   2892         .map_err(|_| AppSqliteError::InvalidProjection {
   2893             reason: "listing relay provenance must encode",
   2894         })
   2895 }
   2896 
   2897 fn listing_relays_from_json(value: Option<String>) -> Result<Vec<String>, AppSqliteError> {
   2898     let Some(value) = empty_string_to_none_option(value) else {
   2899         return Ok(Vec::new());
   2900     };
   2901     let value = serde_json::from_str::<Value>(value.as_str()).map_err(|_| {
   2902         AppSqliteError::InvalidProjection {
   2903             reason: "listing relay provenance json must decode",
   2904         }
   2905     })?;
   2906     if let Some(relays) = value.as_array() {
   2907         return Ok(relays_from_json_array(relays));
   2908     }
   2909 
   2910     let relay_key = match value.get("state").and_then(Value::as_str) {
   2911         Some("acknowledged") => Some("acknowledged_relays"),
   2912         Some("observed") => Some("observed_relays"),
   2913         _ => None,
   2914     };
   2915     if let Some(key) = relay_key {
   2916         return Ok(value
   2917             .get(key)
   2918             .and_then(Value::as_array)
   2919             .map(|relays| relays_from_json_array(relays))
   2920             .unwrap_or_default());
   2921     }
   2922 
   2923     Ok(Vec::new())
   2924 }
   2925 
   2926 fn relays_from_json_array(relays: &[Value]) -> Vec<String> {
   2927     normalized_listing_relays(relays.iter().filter_map(Value::as_str))
   2928 }
   2929 
   2930 fn normalized_listing_relays<'a>(relays: impl IntoIterator<Item = &'a str>) -> Vec<String> {
   2931     let mut seen = BTreeSet::new();
   2932     let mut normalized = Vec::new();
   2933     for relay in relays {
   2934         let relay = relay.trim();
   2935         if !relay.is_empty() && seen.insert(relay.to_owned()) {
   2936             normalized.push(relay.to_owned());
   2937         }
   2938     }
   2939     normalized
   2940 }
   2941 
   2942 #[cfg(test)]
   2943 mod tests {
   2944     use std::collections::BTreeSet;
   2945 
   2946     use radroots_app_view::{
   2947         BuyerContext, BuyerOrderReviewDisabledReason, BuyerOrderStatus, FarmId, FarmOrderMethod,
   2948         FulfillmentWindowId, OrderId, PickupLocationId, ProductId, TradeAgreementStatus,
   2949         TradeInventoryStatus, TradeRevisionStatus, TradeWorkflowSource,
   2950     };
   2951     use rusqlite::{Connection, params};
   2952     use serde_json::json;
   2953 
   2954     use crate::{
   2955         AppSqliteError, AppSqliteStore, BuyerRepeatDemandApplyOutcome, DatabaseTarget,
   2956         SelectedBuyerOrderScope,
   2957     };
   2958 
   2959     use super::AppBuyerRepository;
   2960 
   2961     const LINKED_BUYER_PUBKEY: &str =
   2962         "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
   2963 
   2964     #[test]
   2965     fn selected_buyer_order_scope_uses_only_valid_context_keys() {
   2966         let upper_pubkey = LINKED_BUYER_PUBKEY.to_ascii_uppercase();
   2967         let valid_scope = SelectedBuyerOrderScope::for_selected_account(
   2968             " acct_buyer ",
   2969             Some(upper_pubkey.as_str()),
   2970         );
   2971         let expected_valid = vec![
   2972             "account:acct_buyer".to_owned(),
   2973             format!("nostr:{LINKED_BUYER_PUBKEY}"),
   2974         ];
   2975 
   2976         let malformed_scope =
   2977             SelectedBuyerOrderScope::for_selected_account("acct_buyer", Some("buyer-pubkey"));
   2978         let expected_account_only = vec!["account:acct_buyer".to_owned()];
   2979 
   2980         let blank_scope = SelectedBuyerOrderScope::for_selected_account(" ", Some(" "));
   2981         let duplicate_scope = SelectedBuyerOrderScope::from_context_keys([
   2982             format!("nostr:{LINKED_BUYER_PUBKEY}"),
   2983             format!("nostr:{LINKED_BUYER_PUBKEY}"),
   2984         ]);
   2985         let expected_deduped = vec![format!("nostr:{LINKED_BUYER_PUBKEY}")];
   2986 
   2987         assert_eq!(valid_scope.context_keys(), expected_valid.as_slice());
   2988         assert_eq!(
   2989             malformed_scope.context_keys(),
   2990             expected_account_only.as_slice()
   2991         );
   2992         assert!(blank_scope.context_keys().is_empty());
   2993         assert_eq!(duplicate_scope.context_keys(), expected_deduped.as_slice());
   2994     }
   2995 
   2996     #[test]
   2997     fn listing_relays_from_json_uses_only_acknowledged_or_observed_relays() {
   2998         assert_eq!(
   2999             super::listing_relays_from_json(Some(
   3000                 json!({
   3001                     "state": "acknowledged",
   3002                     "target_relays": ["wss://target.example"],
   3003                     "connected_relays": ["wss://connected.example"],
   3004                     "acknowledged_relays": ["wss://ack.example"]
   3005                 })
   3006                 .to_string()
   3007             ))
   3008             .expect("acknowledged relays"),
   3009             vec!["wss://ack.example"]
   3010         );
   3011         assert_eq!(
   3012             super::listing_relays_from_json(Some(
   3013                 json!({
   3014                     "state": "observed",
   3015                     "target_relays": ["wss://target.example"],
   3016                     "connected_relays": ["wss://connected.example"],
   3017                     "observed_relays": ["wss://observed.example"]
   3018                 })
   3019                 .to_string()
   3020             ))
   3021             .expect("observed relays"),
   3022             vec!["wss://observed.example"]
   3023         );
   3024         assert!(
   3025             super::listing_relays_from_json(Some(
   3026                 json!({
   3027                     "state": "observed",
   3028                     "target_relays": ["wss://target.example"],
   3029                     "connected_relays": ["wss://connected.example"],
   3030                     "observed_relays": []
   3031                 })
   3032                 .to_string()
   3033             ))
   3034             .expect("unknown observed relays")
   3035             .is_empty()
   3036         );
   3037         assert!(
   3038             super::listing_relays_from_json(Some(
   3039                 json!({
   3040                     "state": "pending",
   3041                     "target_relays": ["wss://target.example"],
   3042                     "connected_relays": ["wss://connected.example"]
   3043                 })
   3044                 .to_string()
   3045             ))
   3046             .expect("pending relays")
   3047             .is_empty()
   3048         );
   3049     }
   3050 
   3051     #[test]
   3052     fn buyer_listings_and_product_detail_follow_catalog_truth() {
   3053         let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
   3054         let connection = store.connection();
   3055         let repository = AppBuyerRepository::new(connection);
   3056         let farm_id = insert_farm(connection, "Willow Farm", "ready");
   3057         let future_window_id = insert_window(
   3058             connection,
   3059             farm_id,
   3060             Some(insert_pickup_location(connection, farm_id, "Barn pickup")),
   3061             "Friday pickup",
   3062             "2099-04-18T16:00:00Z",
   3063             "2099-04-18T18:00:00Z",
   3064         );
   3065 
   3066         insert_farm_setup_binding(connection, "acct_farmer", farm_id, true, false, false);
   3067         let visible_product_id = insert_product(
   3068             connection,
   3069             farm_id,
   3070             SeedProduct {
   3071                 title: "Salad mix",
   3072                 subtitle: "Spring blend",
   3073                 status: "published",
   3074                 unit_label: "bag",
   3075                 price_minor_units: Some(650),
   3076                 price_currency: "USD",
   3077                 stock_count: Some(8),
   3078                 availability_window_id: Some(future_window_id),
   3079             },
   3080         );
   3081         insert_product(
   3082             connection,
   3083             farm_id,
   3084             SeedProduct {
   3085                 title: "Pea shoots",
   3086                 subtitle: "Tray-grown",
   3087                 status: "draft",
   3088                 unit_label: "bag",
   3089                 price_minor_units: Some(450),
   3090                 price_currency: "USD",
   3091                 stock_count: Some(4),
   3092                 availability_window_id: Some(future_window_id),
   3093             },
   3094         );
   3095         insert_product(
   3096             connection,
   3097             farm_id,
   3098             SeedProduct {
   3099                 title: "Sold out carrots",
   3100                 subtitle: "",
   3101                 status: "published",
   3102                 unit_label: "bunch",
   3103                 price_minor_units: Some(500),
   3104                 price_currency: "USD",
   3105                 stock_count: Some(0),
   3106                 availability_window_id: Some(future_window_id),
   3107             },
   3108         );
   3109 
   3110         let listings = repository
   3111             .load_buyer_listings("salad", &BTreeSet::from([FarmOrderMethod::Pickup]))
   3112             .expect("buyer listings should load");
   3113         let detail = repository
   3114             .load_buyer_product_detail(visible_product_id)
   3115             .expect("buyer detail should load")
   3116             .expect("buyer detail should exist");
   3117 
   3118         assert_eq!(listings.rows.len(), 1);
   3119         assert_eq!(listings.rows[0].title, "Salad mix");
   3120         assert_eq!(
   3121             listings.rows[0].fulfillment_methods,
   3122             BTreeSet::from([FarmOrderMethod::Pickup])
   3123         );
   3124         assert_eq!(
   3125             listings.rows[0].next_fulfillment_window_label.as_deref(),
   3126             Some("Friday pickup")
   3127         );
   3128         assert_eq!(detail.selected_quantity, 1);
   3129         assert_eq!(detail.detail_text.as_deref(), Some("Spring blend"));
   3130     }
   3131 
   3132     #[test]
   3133     fn buyer_orders_can_read_account_and_linked_nostr_contexts() {
   3134         let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
   3135         let connection = store.connection();
   3136         let repository = AppBuyerRepository::new(connection);
   3137         let farm_id = insert_farm(connection, "Willow Farm", "ready");
   3138         let account_order_id = OrderId::new();
   3139         let relay_order_id = OrderId::new();
   3140         let linked_context_key = format!("nostr:{LINKED_BUYER_PUBKEY}");
   3141         insert_order(
   3142             connection,
   3143             account_order_id,
   3144             farm_id,
   3145             "1001",
   3146             "scheduled",
   3147             Some("account:acct_buyer"),
   3148             "casey@example.com",
   3149             "555-0100",
   3150             "",
   3151         );
   3152         insert_order(
   3153             connection,
   3154             relay_order_id,
   3155             farm_id,
   3156             "1002",
   3157             "packed",
   3158             Some(linked_context_key.as_str()),
   3159             "",
   3160             "",
   3161             "",
   3162         );
   3163 
   3164         let scope =
   3165             SelectedBuyerOrderScope::for_selected_account("acct_buyer", Some(LINKED_BUYER_PUBKEY));
   3166         let linked_orders = repository
   3167             .load_buyer_orders_for_scope(&scope)
   3168             .expect("linked buyer orders should load");
   3169         let linked_detail = repository
   3170             .load_buyer_order_detail_for_scope(&scope, relay_order_id)
   3171             .expect("linked buyer order detail should load")
   3172             .expect("linked buyer order detail should exist");
   3173         let account_only_orders = repository
   3174             .load_buyer_orders(&BuyerContext::account("acct_buyer"))
   3175             .expect("account buyer orders should load");
   3176         let account_only_detail = repository
   3177             .load_buyer_order_detail(&BuyerContext::account("acct_buyer"), relay_order_id)
   3178             .expect("account buyer order detail should load");
   3179 
   3180         assert_eq!(linked_orders.rows.len(), 2);
   3181         assert!(
   3182             linked_orders
   3183                 .rows
   3184                 .iter()
   3185                 .any(|row| row.order_id == account_order_id)
   3186         );
   3187         assert!(
   3188             linked_orders
   3189                 .rows
   3190                 .iter()
   3191                 .any(|row| row.order_id == relay_order_id)
   3192         );
   3193         assert_eq!(linked_detail.order_id, relay_order_id);
   3194         assert_eq!(linked_detail.status, BuyerOrderStatus::Ready);
   3195         assert_eq!(account_only_orders.rows.len(), 1);
   3196         assert_eq!(account_only_orders.rows[0].order_id, account_order_id);
   3197         assert!(account_only_detail.is_none());
   3198     }
   3199 
   3200     #[test]
   3201     fn buyer_order_review_requires_account_before_order_write() {
   3202         let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
   3203         let connection = store.connection();
   3204         let repository = AppBuyerRepository::new(connection);
   3205         let context = BuyerContext::Guest;
   3206         let farm_id = insert_farm(connection, "Willow Farm", "ready");
   3207         let pickup_location_id = insert_pickup_location(connection, farm_id, "Barn pickup");
   3208         let future_window_id = insert_window(
   3209             connection,
   3210             farm_id,
   3211             Some(pickup_location_id),
   3212             "Friday pickup",
   3213             "2099-04-18T16:00:00Z",
   3214             "2099-04-18T18:00:00Z",
   3215         );
   3216 
   3217         insert_farm_setup_binding(connection, "acct_farmer", farm_id, true, false, false);
   3218         let product_id = insert_product(
   3219             connection,
   3220             farm_id,
   3221             SeedProduct {
   3222                 title: "Salad mix",
   3223                 subtitle: "Spring blend",
   3224                 status: "published",
   3225                 unit_label: "bag",
   3226                 price_minor_units: Some(650),
   3227                 price_currency: "USD",
   3228                 stock_count: Some(8),
   3229                 availability_window_id: Some(future_window_id),
   3230             },
   3231         );
   3232         let listing = repository
   3233             .load_buyer_product_detail(product_id)
   3234             .expect("buyer detail should load")
   3235             .expect("listing should exist")
   3236             .listing;
   3237 
   3238         repository
   3239             .replace_buyer_cart(
   3240                 &context,
   3241                 &radroots_app_view::BuyerCartProjection {
   3242                     farm_id: Some(farm_id),
   3243                     farm_display_name: Some("Willow Farm".to_owned()),
   3244                     lines: vec![radroots_app_view::BuyerCartLineProjection {
   3245                         product_id: listing.product_id,
   3246                         farm_id: listing.farm_id,
   3247                         farm_display_name: listing.farm_display_name.clone(),
   3248                         title: listing.title.clone(),
   3249                         quantity: 2,
   3250                         unit_price: listing.price.clone(),
   3251                         line_total_minor_units: 1300,
   3252                         fulfillment_summary: "Friday pickup".to_owned(),
   3253                     }],
   3254                     subtotal_minor_units: Some(1300),
   3255                     currency_code: Some("USD".to_owned()),
   3256                     replace_confirmation: None,
   3257                 },
   3258             )
   3259             .expect("buyer cart should save");
   3260         repository
   3261             .save_buyer_order_review_draft(
   3262                 &context,
   3263                 &radroots_app_view::BuyerOrderReviewDraft {
   3264                     name: "Casey Buyer".to_owned(),
   3265                     email: "casey@example.com".to_owned(),
   3266                     phone: "555-0101".to_owned(),
   3267                     order_note: "Leave by the cooler".to_owned(),
   3268                 },
   3269             )
   3270             .expect("buyer order review draft should save");
   3271 
   3272         let order_review = repository
   3273             .load_buyer_order_review(&context)
   3274             .expect("buyer order review should load");
   3275         let error = repository
   3276             .place_buyer_order(&context)
   3277             .expect_err("guest order review should require an account");
   3278         let cart_after_order_review = repository
   3279             .load_buyer_cart(&context)
   3280             .expect("buyer cart should remain after blocked order review");
   3281 
   3282         assert!(matches!(error, AppSqliteError::InvalidProjection { .. }));
   3283         assert!(!order_review.can_place_order);
   3284         assert_eq!(
   3285             order_review.place_order_disabled_reason,
   3286             Some(BuyerOrderReviewDisabledReason::AccountRequired)
   3287         );
   3288         assert_eq!(order_review.summary.line_count, 1);
   3289         assert_eq!(cart_after_order_review.lines.len(), 1);
   3290         assert_eq!(cart_after_order_review.farm_id, Some(farm_id));
   3291         assert_eq!(row_count(connection, "orders"), 0);
   3292         assert_eq!(row_count(connection, "order_lines"), 0);
   3293         assert_eq!(row_count(connection, "buyer_order_coordination_records"), 0);
   3294         assert_eq!(row_count(connection, "local_outbox"), 0);
   3295         assert_eq!(row_count(connection, "local_conflicts"), 0);
   3296         assert_eq!(row_count(connection, "sync_checkpoints"), 0);
   3297     }
   3298 
   3299     #[test]
   3300     fn buyer_order_history_derives_repeat_demand_from_current_listing_truth() {
   3301         let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
   3302         let connection = store.connection();
   3303         let repository = AppBuyerRepository::new(connection);
   3304         let context = BuyerContext::account("acct_buyer");
   3305         let farm_id = insert_farm(connection, "Willow Farm", "ready");
   3306         let pickup_location_id = insert_pickup_location(connection, farm_id, "Barn pickup");
   3307         let future_window_id = insert_window(
   3308             connection,
   3309             farm_id,
   3310             Some(pickup_location_id),
   3311             "Friday pickup",
   3312             "2099-04-18T16:00:00Z",
   3313             "2099-04-18T18:00:00Z",
   3314         );
   3315 
   3316         insert_farm_setup_binding(connection, "acct_farmer", farm_id, true, false, false);
   3317         let available_product_id = insert_product(
   3318             connection,
   3319             farm_id,
   3320             SeedProduct {
   3321                 title: "Salad mix",
   3322                 subtitle: "Spring blend",
   3323                 status: "published",
   3324                 unit_label: "bag",
   3325                 price_minor_units: Some(650),
   3326                 price_currency: "USD",
   3327                 stock_count: Some(8),
   3328                 availability_window_id: Some(future_window_id),
   3329             },
   3330         );
   3331         let unavailable_product_id = insert_product(
   3332             connection,
   3333             farm_id,
   3334             SeedProduct {
   3335                 title: "Pea shoots",
   3336                 subtitle: "Tray-grown",
   3337                 status: "published",
   3338                 unit_label: "bag",
   3339                 price_minor_units: Some(450),
   3340                 price_currency: "USD",
   3341                 stock_count: Some(4),
   3342                 availability_window_id: Some(future_window_id),
   3343             },
   3344         );
   3345         let available_listing = repository
   3346             .load_buyer_product_detail(available_product_id)
   3347             .expect("available buyer detail should load")
   3348             .expect("available listing should exist")
   3349             .listing;
   3350         let unavailable_listing = repository
   3351             .load_buyer_product_detail(unavailable_product_id)
   3352             .expect("unavailable buyer detail should load")
   3353             .expect("unavailable listing should exist")
   3354             .listing;
   3355 
   3356         repository
   3357             .replace_buyer_cart(
   3358                 &context,
   3359                 &radroots_app_view::BuyerCartProjection {
   3360                     farm_id: Some(farm_id),
   3361                     farm_display_name: Some("Willow Farm".to_owned()),
   3362                     lines: vec![
   3363                         radroots_app_view::BuyerCartLineProjection {
   3364                             product_id: available_listing.product_id,
   3365                             farm_id: available_listing.farm_id,
   3366                             farm_display_name: available_listing.farm_display_name.clone(),
   3367                             title: available_listing.title.clone(),
   3368                             quantity: 2,
   3369                             unit_price: available_listing.price.clone(),
   3370                             line_total_minor_units: 1300,
   3371                             fulfillment_summary: "Friday pickup".to_owned(),
   3372                         },
   3373                         radroots_app_view::BuyerCartLineProjection {
   3374                             product_id: unavailable_listing.product_id,
   3375                             farm_id: unavailable_listing.farm_id,
   3376                             farm_display_name: unavailable_listing.farm_display_name.clone(),
   3377                             title: unavailable_listing.title.clone(),
   3378                             quantity: 1,
   3379                             unit_price: unavailable_listing.price.clone(),
   3380                             line_total_minor_units: 450,
   3381                             fulfillment_summary: "Friday pickup".to_owned(),
   3382                         },
   3383                     ],
   3384                     subtotal_minor_units: Some(1750),
   3385                     currency_code: Some("USD".to_owned()),
   3386                     replace_confirmation: None,
   3387                 },
   3388             )
   3389             .expect("buyer cart should save");
   3390         repository
   3391             .save_buyer_order_review_draft(
   3392                 &context,
   3393                 &radroots_app_view::BuyerOrderReviewDraft {
   3394                     name: "Casey Buyer".to_owned(),
   3395                     email: "casey@example.com".to_owned(),
   3396                     phone: String::new(),
   3397                     order_note: String::new(),
   3398                 },
   3399             )
   3400             .expect("buyer order review draft should save");
   3401         let order_id = repository
   3402             .place_buyer_order(&context)
   3403             .expect("buyer order review should place order");
   3404 
   3405         connection
   3406             .execute(
   3407                 "update products set status = 'archived' where id = ?1",
   3408                 params![unavailable_product_id.to_string()],
   3409             )
   3410             .expect("product should archive");
   3411 
   3412         let buyer_orders = repository
   3413             .load_buyer_orders(&context)
   3414             .expect("buyer orders should load");
   3415         let buyer_order_detail = repository
   3416             .load_buyer_order_detail(&context, order_id)
   3417             .expect("buyer order detail should load")
   3418             .expect("buyer order detail should exist");
   3419         let row_repeat_demand = buyer_orders.rows[0]
   3420             .repeat_demand
   3421             .as_ref()
   3422             .expect("repeat demand should derive for buyer order row");
   3423         let detail_repeat_demand = buyer_order_detail
   3424             .repeat_demand
   3425             .as_ref()
   3426             .expect("repeat demand should derive for buyer order detail");
   3427 
   3428         assert_eq!(buyer_orders.rows.len(), 1);
   3429         assert_eq!(
   3430             row_repeat_demand.eligibility,
   3431             radroots_app_view::RepeatDemandEligibility::Partial
   3432         );
   3433         assert_eq!(row_repeat_demand.available_item_count, 1);
   3434         assert_eq!(row_repeat_demand.unavailable_item_count, 1);
   3435         assert_eq!(detail_repeat_demand, row_repeat_demand);
   3436         assert_eq!(buyer_order_detail.items.len(), 2);
   3437         assert!(
   3438             buyer_order_detail
   3439                 .items
   3440                 .iter()
   3441                 .any(|item| item.line_total_minor_units == Some(1300))
   3442         );
   3443         assert!(
   3444             buyer_order_detail
   3445                 .items
   3446                 .iter()
   3447                 .any(|item| item.line_total_minor_units == Some(450))
   3448         );
   3449         assert_eq!(buyer_order_detail.economics.total_minor_units, Some(1750));
   3450         assert_eq!(
   3451             buyer_order_detail.economics.currency_code.as_deref(),
   3452             Some("USD")
   3453         );
   3454     }
   3455 
   3456     #[test]
   3457     fn buyer_repeat_demand_from_linked_order_writes_selected_account_cart() {
   3458         let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
   3459         let connection = store.connection();
   3460         let repository = AppBuyerRepository::new(connection);
   3461         let context = BuyerContext::account("acct_buyer");
   3462         let farm_id = insert_farm(connection, "Willow Farm", "ready");
   3463         let pickup_location_id = insert_pickup_location(connection, farm_id, "Barn pickup");
   3464         let future_window_id = insert_window(
   3465             connection,
   3466             farm_id,
   3467             Some(pickup_location_id),
   3468             "Friday pickup",
   3469             "2099-04-18T16:00:00Z",
   3470             "2099-04-18T18:00:00Z",
   3471         );
   3472 
   3473         insert_farm_setup_binding(connection, "acct_farmer", farm_id, true, false, false);
   3474         let product_id = insert_product(
   3475             connection,
   3476             farm_id,
   3477             SeedProduct {
   3478                 title: "Salad mix",
   3479                 subtitle: "Spring blend",
   3480                 status: "published",
   3481                 unit_label: "bag",
   3482                 price_minor_units: Some(650),
   3483                 price_currency: "USD",
   3484                 stock_count: Some(8),
   3485                 availability_window_id: Some(future_window_id),
   3486             },
   3487         );
   3488         let listing = repository
   3489             .load_buyer_product_detail(product_id)
   3490             .expect("buyer detail should load")
   3491             .expect("listing should exist")
   3492             .listing;
   3493 
   3494         repository
   3495             .replace_buyer_cart(
   3496                 &context,
   3497                 &radroots_app_view::BuyerCartProjection {
   3498                     farm_id: Some(farm_id),
   3499                     farm_display_name: Some("Willow Farm".to_owned()),
   3500                     lines: vec![radroots_app_view::BuyerCartLineProjection {
   3501                         product_id: listing.product_id,
   3502                         farm_id: listing.farm_id,
   3503                         farm_display_name: listing.farm_display_name.clone(),
   3504                         title: listing.title.clone(),
   3505                         quantity: 2,
   3506                         unit_price: listing.price.clone(),
   3507                         line_total_minor_units: 1300,
   3508                         fulfillment_summary: "Friday pickup".to_owned(),
   3509                     }],
   3510                     subtotal_minor_units: Some(1300),
   3511                     currency_code: Some("USD".to_owned()),
   3512                     replace_confirmation: None,
   3513                 },
   3514             )
   3515             .expect("buyer cart should save");
   3516         repository
   3517             .save_buyer_order_review_draft(
   3518                 &context,
   3519                 &radroots_app_view::BuyerOrderReviewDraft {
   3520                     name: "Casey Buyer".to_owned(),
   3521                     email: "casey@example.com".to_owned(),
   3522                     phone: String::new(),
   3523                     order_note: String::new(),
   3524                 },
   3525             )
   3526             .expect("buyer order review draft should save");
   3527         let order_id = repository
   3528             .place_buyer_order(&context)
   3529             .expect("buyer order review should place order");
   3530 
   3531         connection
   3532             .execute(
   3533                 "update orders set buyer_context_key = ?1 where id = ?2",
   3534                 params![format!("nostr:{LINKED_BUYER_PUBKEY}"), order_id.to_string()],
   3535             )
   3536             .expect("linked order context should mutate");
   3537 
   3538         let scope =
   3539             SelectedBuyerOrderScope::for_selected_account("acct_buyer", Some(LINKED_BUYER_PUBKEY));
   3540         let linked_detail = repository
   3541             .load_buyer_order_detail_for_scope(&scope, order_id)
   3542             .expect("linked detail should load")
   3543             .expect("linked order should be visible");
   3544         let account_only_detail = repository
   3545             .load_buyer_order_detail(&context, order_id)
   3546             .expect("account-only detail should load");
   3547         let outcome = repository
   3548             .apply_buyer_repeat_demand_from_scope_to_cart(&scope, &context, order_id, false)
   3549             .expect("linked repeat demand should apply");
   3550         let account_cart = repository
   3551             .load_buyer_cart(&context)
   3552             .expect("selected account cart should load");
   3553         let nostr_cart_line_count: u32 = connection
   3554             .query_row(
   3555                 "select count(*) from buyer_cart_lines where buyer_context_key = ?1",
   3556                 params![format!("nostr:{LINKED_BUYER_PUBKEY}")],
   3557                 |row| row.get(0),
   3558             )
   3559             .expect("nostr cart line count should load");
   3560 
   3561         assert!(linked_detail.repeat_demand.is_some());
   3562         assert!(account_only_detail.is_none());
   3563         assert_eq!(outcome, BuyerRepeatDemandApplyOutcome::Applied);
   3564         assert_eq!(account_cart.lines.len(), 1);
   3565         assert_eq!(account_cart.lines[0].product_id, product_id);
   3566         assert_eq!(account_cart.lines[0].quantity, 2);
   3567         assert_eq!(nostr_cart_line_count, 0);
   3568     }
   3569 
   3570     #[test]
   3571     fn buyer_repeat_demand_requires_current_stock_for_full_historical_quantity() {
   3572         let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
   3573         let connection = store.connection();
   3574         let repository = AppBuyerRepository::new(connection);
   3575         let context = BuyerContext::account("acct_buyer");
   3576         let farm_id = insert_farm(connection, "Willow Farm", "ready");
   3577         let pickup_location_id = insert_pickup_location(connection, farm_id, "Barn pickup");
   3578         let future_window_id = insert_window(
   3579             connection,
   3580             farm_id,
   3581             Some(pickup_location_id),
   3582             "Friday pickup",
   3583             "2099-04-18T16:00:00Z",
   3584             "2099-04-18T18:00:00Z",
   3585         );
   3586 
   3587         insert_farm_setup_binding(connection, "acct_farmer", farm_id, true, false, false);
   3588         let product_id = insert_product(
   3589             connection,
   3590             farm_id,
   3591             SeedProduct {
   3592                 title: "Salad mix",
   3593                 subtitle: "Spring blend",
   3594                 status: "published",
   3595                 unit_label: "bag",
   3596                 price_minor_units: Some(650),
   3597                 price_currency: "USD",
   3598                 stock_count: Some(8),
   3599                 availability_window_id: Some(future_window_id),
   3600             },
   3601         );
   3602         let listing = repository
   3603             .load_buyer_product_detail(product_id)
   3604             .expect("buyer detail should load")
   3605             .expect("listing should exist")
   3606             .listing;
   3607 
   3608         repository
   3609             .replace_buyer_cart(
   3610                 &context,
   3611                 &radroots_app_view::BuyerCartProjection {
   3612                     farm_id: Some(farm_id),
   3613                     farm_display_name: Some("Willow Farm".to_owned()),
   3614                     lines: vec![radroots_app_view::BuyerCartLineProjection {
   3615                         product_id: listing.product_id,
   3616                         farm_id: listing.farm_id,
   3617                         farm_display_name: listing.farm_display_name.clone(),
   3618                         title: listing.title.clone(),
   3619                         quantity: 2,
   3620                         unit_price: listing.price.clone(),
   3621                         line_total_minor_units: 1300,
   3622                         fulfillment_summary: "Friday pickup".to_owned(),
   3623                     }],
   3624                     subtotal_minor_units: Some(1300),
   3625                     currency_code: Some("USD".to_owned()),
   3626                     replace_confirmation: None,
   3627                 },
   3628             )
   3629             .expect("buyer cart should save");
   3630         repository
   3631             .save_buyer_order_review_draft(
   3632                 &context,
   3633                 &radroots_app_view::BuyerOrderReviewDraft {
   3634                     name: "Casey Buyer".to_owned(),
   3635                     email: "casey@example.com".to_owned(),
   3636                     phone: String::new(),
   3637                     order_note: String::new(),
   3638                 },
   3639             )
   3640             .expect("buyer order review draft should save");
   3641         let order_id = repository
   3642             .place_buyer_order(&context)
   3643             .expect("buyer order review should place order");
   3644 
   3645         connection
   3646             .execute(
   3647                 "update products set stock_count = 1 where id = ?1",
   3648                 params![product_id.to_string()],
   3649             )
   3650             .expect("product stock should lower");
   3651 
   3652         let repeat_demand = repository
   3653             .load_buyer_order_detail(&context, order_id)
   3654             .expect("buyer order detail should load")
   3655             .expect("buyer order detail should exist")
   3656             .repeat_demand
   3657             .expect("repeat demand should stay visible for unavailable reorder");
   3658 
   3659         assert_eq!(
   3660             repeat_demand.eligibility,
   3661             radroots_app_view::RepeatDemandEligibility::Unavailable
   3662         );
   3663         assert_eq!(repeat_demand.available_item_count, 0);
   3664         assert_eq!(repeat_demand.unavailable_item_count, 1);
   3665         assert_eq!(
   3666             repository
   3667                 .apply_buyer_repeat_demand_to_cart(&context, order_id, false)
   3668                 .expect("repeat demand apply should load"),
   3669             BuyerRepeatDemandApplyOutcome::Unavailable
   3670         );
   3671         assert!(
   3672             repository
   3673                 .load_buyer_cart(&context)
   3674                 .expect("buyer cart should reload")
   3675                 .lines
   3676                 .is_empty()
   3677         );
   3678     }
   3679 
   3680     #[test]
   3681     fn buyer_orders_filter_to_context_and_ignore_seller_orders() {
   3682         let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
   3683         let connection = store.connection();
   3684         let repository = AppBuyerRepository::new(connection);
   3685         let farm_id = insert_farm(connection, "Willow Farm", "ready");
   3686 
   3687         insert_order(
   3688             connection,
   3689             OrderId::new(),
   3690             farm_id,
   3691             "R-100",
   3692             "needs_action",
   3693             None,
   3694             "",
   3695             "",
   3696             "",
   3697         );
   3698         insert_order(
   3699             connection,
   3700             OrderId::new(),
   3701             farm_id,
   3702             "R-101",
   3703             "scheduled",
   3704             Some("guest"),
   3705             "guest@example.com",
   3706             "",
   3707             "",
   3708         );
   3709         insert_order(
   3710             connection,
   3711             OrderId::new(),
   3712             farm_id,
   3713             "R-102",
   3714             "packed",
   3715             Some("account:acct_buyer"),
   3716             "buyer@example.com",
   3717             "",
   3718             "",
   3719         );
   3720 
   3721         let guest_orders = repository
   3722             .load_buyer_orders(&BuyerContext::Guest)
   3723             .expect("guest orders should load");
   3724         let account_orders = repository
   3725             .load_buyer_orders(&BuyerContext::account("acct_buyer"))
   3726             .expect("account orders should load");
   3727 
   3728         assert_eq!(guest_orders.rows.len(), 1);
   3729         assert_eq!(guest_orders.rows[0].order_number, "R-101");
   3730         assert_eq!(account_orders.rows.len(), 1);
   3731         assert_eq!(account_orders.rows[0].order_number, "R-102");
   3732     }
   3733 
   3734     #[test]
   3735     fn buyer_order_projections_fail_closed_for_invalid_workflow_revision() {
   3736         let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
   3737         let connection = store.connection();
   3738         let repository = AppBuyerRepository::new(connection);
   3739         let context = BuyerContext::Guest;
   3740         let farm_id = insert_farm(connection, "Willow Farm", "ready");
   3741         let order_id = OrderId::new();
   3742 
   3743         insert_order(
   3744             connection,
   3745             order_id,
   3746             farm_id,
   3747             "R-100",
   3748             "scheduled",
   3749             Some("guest"),
   3750             "guest@example.com",
   3751             "",
   3752             "",
   3753         );
   3754         set_order_workflow_revision(
   3755             connection,
   3756             order_id,
   3757             TradeRevisionStatus::KeptAsPlaced.storage_key(),
   3758         );
   3759 
   3760         let list = repository
   3761             .load_buyer_orders(&context)
   3762             .expect("valid revision should load in buyer list");
   3763         let detail = repository
   3764             .load_buyer_order_detail(&context, order_id)
   3765             .expect("valid revision should load in buyer detail")
   3766             .expect("buyer detail should exist");
   3767 
   3768         assert_eq!(
   3769             list.rows[0].workflow.revision,
   3770             TradeRevisionStatus::KeptAsPlaced
   3771         );
   3772         assert_eq!(detail.workflow.revision, TradeRevisionStatus::KeptAsPlaced);
   3773 
   3774         corrupt_order_workflow_revision(connection, order_id, "future_revision");
   3775 
   3776         let list_error = repository
   3777             .load_buyer_orders(&context)
   3778             .expect_err("invalid revision should fail buyer list projection");
   3779         let detail_error = repository
   3780             .load_buyer_order_detail(&context, order_id)
   3781             .expect_err("invalid revision should fail buyer detail projection");
   3782 
   3783         assert_decode_enum(list_error, "orders.workflow_revision", "future_revision");
   3784         assert_decode_enum(detail_error, "orders.workflow_revision", "future_revision");
   3785     }
   3786 
   3787     #[test]
   3788     fn buyer_order_projections_read_workflow_display_snapshot() {
   3789         let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
   3790         let connection = store.connection();
   3791         let repository = AppBuyerRepository::new(connection);
   3792         let context = BuyerContext::account("acct_buyer");
   3793         let farm_id = insert_farm(connection, "Willow Farm", "ready");
   3794         let order_id = OrderId::new();
   3795         let product_id = ProductId::new();
   3796 
   3797         insert_order(
   3798             connection,
   3799             order_id,
   3800             farm_id,
   3801             "R-100",
   3802             "needs_action",
   3803             Some("account:acct_buyer"),
   3804             "buyer@example.com",
   3805             "",
   3806             "",
   3807         );
   3808         insert_order_line(
   3809             connection,
   3810             order_id,
   3811             product_id,
   3812             "Salad mix",
   3813             2,
   3814             "bag",
   3815             Some(650),
   3816             "USD",
   3817         );
   3818         set_order_workflow_revision(
   3819             connection,
   3820             order_id,
   3821             TradeRevisionStatus::KeptAsPlaced.storage_key(),
   3822         );
   3823         set_order_workflow_display_projection(
   3824             connection,
   3825             order_id,
   3826             "confirmed",
   3827             "reserved",
   3828             "local_events",
   3829             Some("agreement-event-1"),
   3830         );
   3831 
   3832         let list = repository
   3833             .load_buyer_orders(&context)
   3834             .expect("buyer order list should load");
   3835         let detail = repository
   3836             .load_buyer_order_detail(&context, order_id)
   3837             .expect("buyer order detail should load")
   3838             .expect("buyer order detail should exist");
   3839         let row = &list.rows[0];
   3840 
   3841         assert_eq!(list.rows.len(), 1);
   3842         assert_eq!(row.workflow.agreement, TradeAgreementStatus::Confirmed);
   3843         assert_eq!(row.workflow.inventory, TradeInventoryStatus::Reserved);
   3844         assert_eq!(
   3845             row.workflow.provenance.primary_source,
   3846             TradeWorkflowSource::LocalEvents
   3847         );
   3848         assert_eq!(
   3849             row.workflow.provenance.last_event_id.as_deref(),
   3850             Some("agreement-event-1")
   3851         );
   3852         assert_eq!(row.workflow.economics.total_minor_units, Some(1300));
   3853         assert_eq!(row.workflow.economics.currency_code.as_deref(), Some("USD"));
   3854         assert_eq!(detail.workflow, row.workflow);
   3855     }
   3856 
   3857     #[test]
   3858     fn buyer_order_projections_fail_closed_for_invalid_workflow_snapshot_keys() {
   3859         let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
   3860         let connection = store.connection();
   3861         let repository = AppBuyerRepository::new(connection);
   3862         let context = BuyerContext::account("acct_buyer");
   3863         let farm_id = insert_farm(connection, "Willow Farm", "ready");
   3864         let order_id = OrderId::new();
   3865 
   3866         insert_order(
   3867             connection,
   3868             order_id,
   3869             farm_id,
   3870             "R-100",
   3871             "scheduled",
   3872             Some("account:acct_buyer"),
   3873             "buyer@example.com",
   3874             "",
   3875             "",
   3876         );
   3877         set_order_workflow_display_projection(
   3878             connection,
   3879             order_id,
   3880             "confirmed",
   3881             "reserved",
   3882             "local_events",
   3883             Some("buyer-workflow-event"),
   3884         );
   3885 
   3886         for (column, expected_field) in [
   3887             ("workflow_agreement", "orders.workflow_agreement"),
   3888             ("workflow_inventory", "orders.workflow_inventory"),
   3889             (
   3890                 "workflow_provenance_source",
   3891                 "orders.workflow_provenance_source",
   3892             ),
   3893         ] {
   3894             set_order_workflow_display_projection(
   3895                 connection,
   3896                 order_id,
   3897                 "confirmed",
   3898                 "reserved",
   3899                 "local_events",
   3900                 Some("buyer-workflow-event"),
   3901             );
   3902             corrupt_order_workflow_display_projection(connection, order_id, column, "future_state");
   3903 
   3904             let list_error = repository
   3905                 .load_buyer_orders(&context)
   3906                 .expect_err("invalid workflow snapshot should fail buyer list projection");
   3907             let detail_error = repository
   3908                 .load_buyer_order_detail(&context, order_id)
   3909                 .expect_err("invalid workflow snapshot should fail buyer detail projection");
   3910 
   3911             assert_decode_enum(list_error, expected_field, "future_state");
   3912             assert_decode_enum(detail_error, expected_field, "future_state");
   3913         }
   3914     }
   3915 
   3916     #[test]
   3917     fn buyer_cart_rejects_cross_farm_lines() {
   3918         let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
   3919         let farm_id = FarmId::new();
   3920         let other_farm_id = FarmId::new();
   3921 
   3922         let error = repository_error(&store, farm_id, other_farm_id);
   3923 
   3924         assert!(matches!(error, AppSqliteError::InvalidProjection { .. }));
   3925     }
   3926 
   3927     fn repository_error(
   3928         store: &AppSqliteStore,
   3929         farm_id: FarmId,
   3930         other_farm_id: FarmId,
   3931     ) -> AppSqliteError {
   3932         AppBuyerRepository::new(store.connection())
   3933             .replace_buyer_cart(
   3934                 &BuyerContext::Guest,
   3935                 &radroots_app_view::BuyerCartProjection {
   3936                     farm_id: Some(farm_id),
   3937                     farm_display_name: Some("Willow Farm".to_owned()),
   3938                     lines: vec![radroots_app_view::BuyerCartLineProjection {
   3939                         product_id: ProductId::new(),
   3940                         farm_id: other_farm_id,
   3941                         farm_display_name: "Other Farm".to_owned(),
   3942                         title: "Mismatch".to_owned(),
   3943                         quantity: 1,
   3944                         unit_price: radroots_app_view::ProductPricePresentation {
   3945                             amount_minor_units: 500,
   3946                             currency_code: "USD".to_owned(),
   3947                             unit_label: "bag".to_owned(),
   3948                         },
   3949                         line_total_minor_units: 500,
   3950                         fulfillment_summary: "Friday pickup".to_owned(),
   3951                     }],
   3952                     subtotal_minor_units: Some(500),
   3953                     currency_code: Some("USD".to_owned()),
   3954                     replace_confirmation: None,
   3955                 },
   3956             )
   3957             .expect_err("cross-farm cart should fail")
   3958     }
   3959 
   3960     fn insert_farm(connection: &Connection, display_name: &str, readiness: &str) -> FarmId {
   3961         let farm_id = FarmId::new();
   3962 
   3963         connection
   3964             .execute(
   3965                 "insert into farms (
   3966                     id,
   3967                     display_name,
   3968                     readiness,
   3969                     timezone,
   3970                     currency_code,
   3971                     created_at,
   3972                     updated_at
   3973                  ) values (?1, ?2, ?3, 'UTC', 'USD', '2026-04-20T08:00:00Z', '2026-04-20T08:00:00Z')",
   3974                 params![farm_id.to_string(), display_name, readiness],
   3975             )
   3976             .expect("farm insert should succeed");
   3977 
   3978         farm_id
   3979     }
   3980 
   3981     fn insert_pickup_location(
   3982         connection: &Connection,
   3983         farm_id: FarmId,
   3984         label: &str,
   3985     ) -> PickupLocationId {
   3986         let pickup_location_id = PickupLocationId::new();
   3987 
   3988         connection
   3989             .execute(
   3990                 "insert into pickup_locations (
   3991                     id,
   3992                     farm_id,
   3993                     label,
   3994                     address_line,
   3995                     directions,
   3996                     is_default,
   3997                     created_at,
   3998                     updated_at
   3999                  ) values (?1, ?2, ?3, '14 County Road', null, 1, '2026-04-20T08:00:00Z', '2026-04-20T08:00:00Z')",
   4000                 params![pickup_location_id.to_string(), farm_id.to_string(), label],
   4001             )
   4002             .expect("pickup location insert should succeed");
   4003 
   4004         pickup_location_id
   4005     }
   4006 
   4007     fn insert_window(
   4008         connection: &Connection,
   4009         farm_id: FarmId,
   4010         pickup_location_id: Option<PickupLocationId>,
   4011         label: &str,
   4012         starts_at: &str,
   4013         ends_at: &str,
   4014     ) -> FulfillmentWindowId {
   4015         let fulfillment_window_id = FulfillmentWindowId::new();
   4016 
   4017         connection
   4018             .execute(
   4019                 "insert into fulfillment_windows (
   4020                     id,
   4021                     farm_id,
   4022                     starts_at,
   4023                     ends_at,
   4024                     capacity_limit,
   4025                     created_at,
   4026                     updated_at,
   4027                     pickup_location_id,
   4028                     label,
   4029                     order_cutoff_at
   4030                  ) values (?1, ?2, ?3, ?4, null, ?3, ?3, ?5, ?6, ?3)",
   4031                 params![
   4032                     fulfillment_window_id.to_string(),
   4033                     farm_id.to_string(),
   4034                     starts_at,
   4035                     ends_at,
   4036                     pickup_location_id.map(|id| id.to_string()),
   4037                     label,
   4038                 ],
   4039             )
   4040             .expect("window insert should succeed");
   4041 
   4042         fulfillment_window_id
   4043     }
   4044 
   4045     fn insert_farm_setup_binding(
   4046         connection: &Connection,
   4047         account_id: &str,
   4048         farm_id: FarmId,
   4049         pickup_enabled: bool,
   4050         delivery_enabled: bool,
   4051         shipping_enabled: bool,
   4052     ) {
   4053         connection
   4054             .execute(
   4055                 "insert into account_farm_setups (
   4056                     account_id,
   4057                     farm_name,
   4058                     location_or_service_area,
   4059                     pickup_enabled,
   4060                     delivery_enabled,
   4061                     shipping_enabled,
   4062                     saved_farm_id,
   4063                     saved_farm_display_name,
   4064                     saved_farm_readiness,
   4065                     updated_at
   4066                  ) values (?1, 'Willow Farm', 'County Road', ?2, ?3, ?4, ?5, 'Willow Farm', 'ready', '2026-04-20T08:00:00Z')",
   4067                 params![
   4068                     account_id,
   4069                     i64::from(pickup_enabled),
   4070                     i64::from(delivery_enabled),
   4071                     i64::from(shipping_enabled),
   4072                     farm_id.to_string(),
   4073                 ],
   4074             )
   4075             .expect("farm setup binding insert should succeed");
   4076     }
   4077 
   4078     struct SeedProduct<'a> {
   4079         title: &'a str,
   4080         subtitle: &'a str,
   4081         status: &'a str,
   4082         unit_label: &'a str,
   4083         price_minor_units: Option<u32>,
   4084         price_currency: &'a str,
   4085         stock_count: Option<u32>,
   4086         availability_window_id: Option<FulfillmentWindowId>,
   4087     }
   4088 
   4089     fn insert_product(
   4090         connection: &Connection,
   4091         farm_id: FarmId,
   4092         product: SeedProduct<'_>,
   4093     ) -> ProductId {
   4094         let product_id = ProductId::new();
   4095 
   4096         connection
   4097             .execute(
   4098                 "insert into products (
   4099                     id,
   4100                     farm_id,
   4101                     title,
   4102                     subtitle,
   4103                     status,
   4104                     unit_label,
   4105                     price_minor_units,
   4106                     price_currency,
   4107                     stock_count,
   4108                     availability_window_id,
   4109                     updated_at
   4110                  ) values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, '2026-04-20T09:00:00Z')",
   4111                 params![
   4112                     product_id.to_string(),
   4113                     farm_id.to_string(),
   4114                     product.title,
   4115                     product.subtitle,
   4116                     product.status,
   4117                     product.unit_label,
   4118                     product.price_minor_units,
   4119                     product.price_currency,
   4120                     product.stock_count,
   4121                     product.availability_window_id.map(|id| id.to_string()),
   4122                 ],
   4123             )
   4124             .expect("product insert should succeed");
   4125 
   4126         product_id
   4127     }
   4128 
   4129     fn insert_order(
   4130         connection: &Connection,
   4131         order_id: OrderId,
   4132         farm_id: FarmId,
   4133         order_number: &str,
   4134         status: &str,
   4135         buyer_context_key: Option<&str>,
   4136         buyer_email: &str,
   4137         buyer_phone: &str,
   4138         buyer_order_note: &str,
   4139     ) {
   4140         connection
   4141             .execute(
   4142                 "insert into orders (
   4143                     id,
   4144                     farm_id,
   4145                     fulfillment_window_id,
   4146                     order_number,
   4147                     customer_display_name,
   4148                     status,
   4149                     updated_at,
   4150                     buyer_context_key,
   4151                     buyer_email,
   4152                     buyer_phone,
   4153                     buyer_order_note
   4154                  ) values (?1, ?2, null, ?3, 'Casey', ?4, '2026-04-20T10:00:00Z', ?5, ?6, ?7, ?8)",
   4155                 params![
   4156                     order_id.to_string(),
   4157                     farm_id.to_string(),
   4158                     order_number,
   4159                     status,
   4160                     buyer_context_key,
   4161                     buyer_email,
   4162                     buyer_phone,
   4163                     buyer_order_note,
   4164                 ],
   4165             )
   4166             .expect("order insert should succeed");
   4167     }
   4168 
   4169     fn set_order_workflow_revision(
   4170         connection: &Connection,
   4171         order_id: OrderId,
   4172         workflow_revision: &str,
   4173     ) {
   4174         connection
   4175             .execute(
   4176                 "update orders set workflow_revision = ?1 where id = ?2",
   4177                 params![workflow_revision, order_id.to_string()],
   4178             )
   4179             .expect("order workflow revision update should succeed");
   4180     }
   4181 
   4182     fn set_order_workflow_display_projection(
   4183         connection: &Connection,
   4184         order_id: OrderId,
   4185         agreement: &str,
   4186         inventory: &str,
   4187         provenance_source: &str,
   4188         provenance_last_event_id: Option<&str>,
   4189     ) {
   4190         connection
   4191             .execute(
   4192                 "update orders
   4193                  set workflow_agreement = ?1,
   4194                      workflow_inventory = ?2,
   4195                      workflow_provenance_source = ?3,
   4196                      workflow_provenance_last_event_id = ?4
   4197                  where id = ?5",
   4198                 params![
   4199                     agreement,
   4200                     inventory,
   4201                     provenance_source,
   4202                     provenance_last_event_id,
   4203                     order_id.to_string(),
   4204                 ],
   4205             )
   4206             .expect("order workflow display projection update should succeed");
   4207     }
   4208 
   4209     fn insert_order_line(
   4210         connection: &Connection,
   4211         order_id: OrderId,
   4212         product_id: ProductId,
   4213         title: &str,
   4214         quantity: i64,
   4215         unit_label: &str,
   4216         unit_price_minor_units: Option<u32>,
   4217         price_currency: &str,
   4218     ) {
   4219         connection
   4220             .execute(
   4221                 "insert into order_lines (
   4222                     id,
   4223                     order_id,
   4224                     title,
   4225                     quantity_value,
   4226                     quantity_unit_label,
   4227                     quantity_display,
   4228                     unit_price_minor_units,
   4229                     price_currency,
   4230                     sort_index
   4231                  ) values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, 0)",
   4232                 params![
   4233                     format!("{order_id}:{product_id}"),
   4234                     order_id.to_string(),
   4235                     title,
   4236                     quantity,
   4237                     unit_label,
   4238                     format!("{quantity} {unit_label}"),
   4239                     unit_price_minor_units,
   4240                     price_currency,
   4241                 ],
   4242             )
   4243             .expect("order line insert should succeed");
   4244     }
   4245 
   4246     fn corrupt_order_workflow_revision(
   4247         connection: &Connection,
   4248         order_id: OrderId,
   4249         workflow_revision: &str,
   4250     ) {
   4251         connection
   4252             .execute_batch("pragma ignore_check_constraints = on")
   4253             .expect("check constraints should disable");
   4254         set_order_workflow_revision(connection, order_id, workflow_revision);
   4255         connection
   4256             .execute_batch("pragma ignore_check_constraints = off")
   4257             .expect("check constraints should re-enable");
   4258     }
   4259 
   4260     fn corrupt_order_workflow_display_projection(
   4261         connection: &Connection,
   4262         order_id: OrderId,
   4263         column: &str,
   4264         value: &str,
   4265     ) {
   4266         connection
   4267             .execute_batch("pragma ignore_check_constraints = on")
   4268             .expect("check constraints should disable");
   4269         let statement = match column {
   4270             "workflow_agreement" => "update orders set workflow_agreement = ?1 where id = ?2",
   4271             "workflow_inventory" => "update orders set workflow_inventory = ?1 where id = ?2",
   4272             "workflow_provenance_source" => {
   4273                 "update orders set workflow_provenance_source = ?1 where id = ?2"
   4274             }
   4275             _ => panic!("unsupported workflow display projection column {column}"),
   4276         };
   4277         connection
   4278             .execute(statement, params![value, order_id.to_string()])
   4279             .expect("order workflow display projection corruption should succeed");
   4280         connection
   4281             .execute_batch("pragma ignore_check_constraints = off")
   4282             .expect("check constraints should re-enable");
   4283     }
   4284 
   4285     fn assert_decode_enum(error: AppSqliteError, expected_field: &str, expected_value: &str) {
   4286         match error {
   4287             AppSqliteError::DecodeEnum { field, value } => {
   4288                 assert_eq!(field, expected_field);
   4289                 assert_eq!(value, expected_value);
   4290             }
   4291             other => panic!("expected DecodeEnum error, got {other:?}"),
   4292         }
   4293     }
   4294 
   4295     fn row_count(connection: &Connection, table_name: &str) -> i64 {
   4296         let sql = format!("SELECT COUNT(*) FROM {table_name}");
   4297 
   4298         connection
   4299             .query_row(&sql, [], |row| row.get(0))
   4300             .expect("row count query should succeed")
   4301     }
   4302 }