app

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

orders.rs (79532B)


      1 use std::collections::BTreeMap;
      2 
      3 use radroots_app_view::{
      4     FarmId, FulfillmentWindowId, FulfillmentWindowSummary, OrderDetailItemRow,
      5     OrderDetailProjection, OrderId, OrderPrimaryAction, OrderStatus, OrdersFilter,
      6     OrdersListProjection, OrdersListRow, OrdersListSummary, OrdersScreenQueryState,
      7     PackDayOutputCustomerOrder, PackDayOutputOrderState, PackDayOutputPackListEntry,
      8     PackDayOutputProductTotal, PackDayOutputQuantity, PackDayOutputSource, PackDayOutputWindow,
      9     PackDayPackListRow, PackDayProductTotalRow, PackDayProjection, PackDayRosterRow,
     10     PackDayScreenQueryState, ProductId, TradeWorkflowProjection,
     11 };
     12 use rusqlite::{Connection, OptionalExtension, params};
     13 
     14 use super::{
     15     order_detail::{order_detail_economics, order_detail_item_row, order_validation_receipts},
     16     parse_trade_revision_status,
     17     workflow::{StoredTradeWorkflowSnapshot, trade_workflow_projection_from_storage},
     18 };
     19 use crate::AppSqliteError;
     20 
     21 pub struct AppOrdersRepository<'a> {
     22     connection: &'a Connection,
     23 }
     24 
     25 #[derive(Clone, Debug, Eq, PartialEq)]
     26 pub struct SellerOrderDecisionExport {
     27     pub order_id: OrderId,
     28     pub farm_id: FarmId,
     29     pub status: OrderStatus,
     30     pub lines: Vec<SellerOrderDecisionLineExport>,
     31 }
     32 
     33 #[derive(Clone, Debug, Eq, PartialEq)]
     34 pub struct SellerOrderDecisionLineExport {
     35     pub product_id: ProductId,
     36     pub listing_bin_id: Option<String>,
     37     pub quantity: u32,
     38     pub stock_count: Option<u32>,
     39     pub reserved_quantity: u32,
     40 }
     41 
     42 impl<'a> AppOrdersRepository<'a> {
     43     pub const fn new(connection: &'a Connection) -> Self {
     44         Self { connection }
     45     }
     46 
     47     pub fn load_orders_list(
     48         &self,
     49         farm_id: FarmId,
     50         query: &OrdersScreenQueryState,
     51     ) -> Result<OrdersListProjection, AppSqliteError> {
     52         let mut records = self.load_order_records(farm_id, query.fulfillment_window_id)?;
     53         let summary = summarize_orders(&records);
     54 
     55         records.retain(|record| record.matches_filter(query.filter));
     56 
     57         Ok(OrdersListProjection {
     58             summary,
     59             rows: records
     60                 .into_iter()
     61                 .map(OrderRecord::into_list_row)
     62                 .collect(),
     63         })
     64     }
     65 
     66     pub fn load_order_detail(
     67         &self,
     68         farm_id: FarmId,
     69         order_id: OrderId,
     70     ) -> Result<Option<OrderDetailProjection>, AppSqliteError> {
     71         let record = self
     72             .connection
     73             .query_row(
     74                 "select
     75                     o.id,
     76                     o.farm_id,
     77                     o.order_number,
     78                     o.customer_display_name,
     79                     o.status,
     80                     o.fulfillment_window_id,
     81                     o.workflow_revision,
     82                     o.workflow_agreement,
     83                     o.workflow_inventory,
     84                     o.workflow_provenance_source,
     85                     o.workflow_provenance_last_event_id,
     86                     fw.label,
     87                     pl.label
     88                  from orders o
     89                  left join fulfillment_windows fw on fw.id = o.fulfillment_window_id
     90                  left join pickup_locations pl on pl.id = fw.pickup_location_id
     91                  where o.farm_id = ?1 and o.id = ?2
     92                  limit 1",
     93                 params![farm_id.to_string(), order_id.to_string()],
     94                 |row| {
     95                     Ok((
     96                         row.get::<_, String>(0)?,
     97                         row.get::<_, String>(1)?,
     98                         row.get::<_, String>(2)?,
     99                         row.get::<_, String>(3)?,
    100                         row.get::<_, String>(4)?,
    101                         row.get::<_, Option<String>>(5)?,
    102                         row.get::<_, String>(6)?,
    103                         row.get::<_, String>(7)?,
    104                         row.get::<_, String>(8)?,
    105                         row.get::<_, String>(9)?,
    106                         row.get::<_, Option<String>>(10)?,
    107                         row.get::<_, Option<String>>(11)?,
    108                         row.get::<_, Option<String>>(12)?,
    109                     ))
    110                 },
    111             )
    112             .optional()
    113             .map_err(|source| AppSqliteError::Query {
    114                 operation: "load order detail",
    115                 source,
    116             })?;
    117 
    118         record
    119             .map(
    120                 |(
    121                     order_id,
    122                     farm_id,
    123                     order_number,
    124                     customer_display_name,
    125                     status,
    126                     fulfillment_window_id,
    127                     workflow_revision,
    128                     workflow_agreement,
    129                     workflow_inventory,
    130                     workflow_provenance_source,
    131                     workflow_provenance_last_event_id,
    132                     fulfillment_window_label,
    133                     pickup_location_label,
    134                 )| {
    135                     let order_id: OrderId = parse_typed_id("orders.id", order_id)?;
    136                     let farm_id: FarmId = parse_typed_id("orders.farm_id", farm_id)?;
    137                     let status = parse_order_status("orders.status", status)?;
    138                     let revision =
    139                         parse_trade_revision_status("orders.workflow_revision", workflow_revision)?;
    140                     let items = self.load_order_detail_items(order_id.to_string())?;
    141                     let economics = order_detail_economics(&items)?;
    142                     let workflow =
    143                         trade_workflow_projection_from_storage(StoredTradeWorkflowSnapshot {
    144                             order_id,
    145                             revision,
    146                             economics: economics.clone(),
    147                             agreement: workflow_agreement,
    148                             inventory: workflow_inventory,
    149                             provenance_source: workflow_provenance_source,
    150                             provenance_last_event_id: workflow_provenance_last_event_id,
    151                         })?;
    152                     let validation_receipts = order_validation_receipts(self.connection, order_id)?;
    153                     Ok(OrderDetailProjection {
    154                         order_id,
    155                         farm_id,
    156                         order_number,
    157                         customer_display_name,
    158                         status,
    159                         fulfillment_window_id: parse_optional_typed_id(
    160                             "orders.fulfillment_window_id",
    161                             fulfillment_window_id,
    162                         )?,
    163                         fulfillment_window_label: empty_string_to_none(fulfillment_window_label),
    164                         pickup_location_label: empty_string_to_none(pickup_location_label),
    165                         items,
    166                         economics,
    167                         validation_receipts,
    168                         primary_action: primary_action_for_order(status, &workflow),
    169                         workflow,
    170                     })
    171                 },
    172             )
    173             .transpose()
    174     }
    175 
    176     pub fn load_seller_order_decision_export(
    177         &self,
    178         farm_id: FarmId,
    179         order_id: OrderId,
    180     ) -> Result<Option<SellerOrderDecisionExport>, AppSqliteError> {
    181         let Some((order_id, farm_id, status)) = self
    182             .connection
    183             .query_row(
    184                 "select id, farm_id, status
    185                  from orders
    186                  where farm_id = ?1 and id = ?2
    187                  limit 1",
    188                 params![farm_id.to_string(), order_id.to_string()],
    189                 |row| {
    190                     Ok((
    191                         row.get::<_, String>(0)?,
    192                         row.get::<_, String>(1)?,
    193                         row.get::<_, String>(2)?,
    194                     ))
    195                 },
    196             )
    197             .optional()
    198             .map_err(|source| AppSqliteError::Query {
    199                 operation: "load seller order decision export",
    200                 source,
    201             })?
    202         else {
    203             return Ok(None);
    204         };
    205         let order_id = parse_typed_id("orders.id", order_id)?;
    206         let farm_id = parse_typed_id("orders.farm_id", farm_id)?;
    207         let status = parse_order_status("orders.status", status)?;
    208         let lines = self.load_seller_order_decision_lines(order_id)?;
    209 
    210         Ok(Some(SellerOrderDecisionExport {
    211             order_id,
    212             farm_id,
    213             status,
    214             lines,
    215         }))
    216     }
    217 
    218     pub fn load_pack_day(
    219         &self,
    220         farm_id: FarmId,
    221         query: &PackDayScreenQueryState,
    222     ) -> Result<PackDayProjection, AppSqliteError> {
    223         let fulfillment_window = if let Some(fulfillment_window_id) = query.fulfillment_window_id {
    224             self.load_fulfillment_window_by_id(farm_id, fulfillment_window_id)?
    225         } else {
    226             self.load_next_upcoming_fulfillment_window(farm_id)?
    227                 .or(self.load_first_active_order_window(farm_id)?)
    228         };
    229 
    230         let Some(fulfillment_window) = fulfillment_window else {
    231             return Ok(PackDayProjection::default());
    232         };
    233 
    234         let totals_by_product =
    235             self.load_pack_day_totals(farm_id, fulfillment_window.fulfillment_window_id)?;
    236         let pack_list =
    237             self.load_pack_day_pack_list(farm_id, fulfillment_window.fulfillment_window_id)?;
    238         let pickup_roster =
    239             self.load_pack_day_roster(farm_id, fulfillment_window.fulfillment_window_id)?;
    240 
    241         Ok(PackDayProjection {
    242             fulfillment_window: Some(fulfillment_window),
    243             reminders: Default::default(),
    244             totals_by_product,
    245             pack_list,
    246             pickup_roster,
    247         })
    248     }
    249 
    250     pub fn load_pack_day_output_source(
    251         &self,
    252         farm_id: FarmId,
    253         fulfillment_window_id: FulfillmentWindowId,
    254     ) -> Result<Option<PackDayOutputSource>, AppSqliteError> {
    255         let Some(fulfillment_window) =
    256             self.load_pack_day_output_window(farm_id, fulfillment_window_id)?
    257         else {
    258             return Ok(None);
    259         };
    260 
    261         let totals_by_product = self.load_pack_day_output_totals(farm_id, fulfillment_window_id)?;
    262         let pack_list = self.load_pack_day_output_pack_list(farm_id, fulfillment_window_id)?;
    263         let pickup_roster = self.load_pack_day_output_roster(farm_id, fulfillment_window_id)?;
    264 
    265         Ok(Some(PackDayOutputSource {
    266             fulfillment_window,
    267             totals_by_product,
    268             pack_list,
    269             pickup_roster,
    270         }))
    271     }
    272 
    273     fn load_order_records(
    274         &self,
    275         farm_id: FarmId,
    276         fulfillment_window_id: Option<FulfillmentWindowId>,
    277     ) -> Result<Vec<OrderRecord>, AppSqliteError> {
    278         let mut statement = self
    279             .connection
    280             .prepare(
    281                 "select
    282                     o.id,
    283                     o.farm_id,
    284                     o.fulfillment_window_id,
    285                     o.order_number,
    286                     o.customer_display_name,
    287                     o.status,
    288                     o.workflow_revision,
    289                     o.workflow_agreement,
    290                     o.workflow_inventory,
    291                     o.workflow_provenance_source,
    292                     o.workflow_provenance_last_event_id,
    293                     fw.label,
    294                     pl.label
    295                  from orders o
    296                  left join fulfillment_windows fw on fw.id = o.fulfillment_window_id
    297                  left join pickup_locations pl on pl.id = fw.pickup_location_id
    298                  where o.farm_id = ?1
    299                    and (?2 is null or o.fulfillment_window_id = ?2)
    300                  order by o.updated_at desc, o.id desc",
    301             )
    302             .map_err(|source| AppSqliteError::Query {
    303                 operation: "prepare orders list",
    304                 source,
    305             })?;
    306         let rows = statement
    307             .query_map(
    308                 params![
    309                     farm_id.to_string(),
    310                     fulfillment_window_id.map(|id| id.to_string())
    311                 ],
    312                 |row| {
    313                     Ok((
    314                         row.get::<_, String>(0)?,
    315                         row.get::<_, String>(1)?,
    316                         row.get::<_, Option<String>>(2)?,
    317                         row.get::<_, String>(3)?,
    318                         row.get::<_, String>(4)?,
    319                         row.get::<_, String>(5)?,
    320                         row.get::<_, String>(6)?,
    321                         row.get::<_, String>(7)?,
    322                         row.get::<_, String>(8)?,
    323                         row.get::<_, String>(9)?,
    324                         row.get::<_, Option<String>>(10)?,
    325                         row.get::<_, Option<String>>(11)?,
    326                         row.get::<_, Option<String>>(12)?,
    327                     ))
    328                 },
    329             )
    330             .map_err(|source| AppSqliteError::Query {
    331                 operation: "query orders list",
    332                 source,
    333             })?;
    334         let mut records = Vec::new();
    335 
    336         for row in rows {
    337             let (
    338                 order_id,
    339                 farm_id,
    340                 fulfillment_window_id,
    341                 order_number,
    342                 customer_display_name,
    343                 status,
    344                 workflow_revision,
    345                 workflow_agreement,
    346                 workflow_inventory,
    347                 workflow_provenance_source,
    348                 workflow_provenance_last_event_id,
    349                 fulfillment_window_label,
    350                 pickup_location_label,
    351             ) = row.map_err(|source| AppSqliteError::Query {
    352                 operation: "read orders list",
    353                 source,
    354             })?;
    355             let order_id: OrderId = parse_typed_id("orders.id", order_id)?;
    356             let farm_id: FarmId = parse_typed_id("orders.farm_id", farm_id)?;
    357             let status = parse_order_status("orders.status", status)?;
    358             let revision =
    359                 parse_trade_revision_status("orders.workflow_revision", workflow_revision)?;
    360             let items = self.load_order_detail_items(order_id.to_string())?;
    361             let economics = order_detail_economics(&items)?;
    362             let workflow = trade_workflow_projection_from_storage(StoredTradeWorkflowSnapshot {
    363                 order_id,
    364                 revision,
    365                 economics,
    366                 agreement: workflow_agreement,
    367                 inventory: workflow_inventory,
    368                 provenance_source: workflow_provenance_source,
    369                 provenance_last_event_id: workflow_provenance_last_event_id,
    370             })?;
    371 
    372             records.push(OrderRecord {
    373                 order_id,
    374                 farm_id,
    375                 fulfillment_window_id: parse_optional_typed_id(
    376                     "orders.fulfillment_window_id",
    377                     fulfillment_window_id,
    378                 )?,
    379                 order_number,
    380                 customer_display_name,
    381                 fulfillment_window_label: empty_string_to_none(fulfillment_window_label),
    382                 pickup_location_label: empty_string_to_none(pickup_location_label),
    383                 status,
    384                 workflow,
    385             });
    386         }
    387 
    388         Ok(records)
    389     }
    390 
    391     fn load_order_detail_items(
    392         &self,
    393         order_id: String,
    394     ) -> Result<Vec<OrderDetailItemRow>, AppSqliteError> {
    395         let mut statement = self
    396             .connection
    397             .prepare(
    398                 "select
    399                     title,
    400                     quantity_display,
    401                     quantity_value,
    402                     quantity_unit_label,
    403                     unit_price_minor_units,
    404                     price_currency
    405                  from order_lines
    406                  where order_id = ?1
    407                  order by sort_index asc, id asc",
    408             )
    409             .map_err(|source| AppSqliteError::Query {
    410                 operation: "prepare order detail items",
    411                 source,
    412             })?;
    413         let rows = statement
    414             .query_map(params![order_id], |row| {
    415                 Ok((
    416                     row.get::<_, String>(0)?,
    417                     row.get::<_, String>(1)?,
    418                     row.get::<_, i64>(2)?,
    419                     row.get::<_, String>(3)?,
    420                     row.get::<_, Option<u32>>(4)?,
    421                     row.get::<_, Option<String>>(5)?,
    422                 ))
    423             })
    424             .map_err(|source| AppSqliteError::Query {
    425                 operation: "query order detail items",
    426                 source,
    427             })?;
    428         let mut items = Vec::new();
    429 
    430         for row in rows {
    431             let (
    432                 title,
    433                 quantity_display,
    434                 quantity_value,
    435                 quantity_unit_label,
    436                 unit_price_minor_units,
    437                 price_currency,
    438             ) = row.map_err(|source| AppSqliteError::Query {
    439                 operation: "read order detail items",
    440                 source,
    441             })?;
    442             items.push(order_detail_item_row(
    443                 title,
    444                 quantity_display,
    445                 quantity_value,
    446                 quantity_unit_label,
    447                 unit_price_minor_units,
    448                 price_currency,
    449             )?);
    450         }
    451 
    452         Ok(items)
    453     }
    454 
    455     fn load_seller_order_decision_lines(
    456         &self,
    457         order_id: OrderId,
    458     ) -> Result<Vec<SellerOrderDecisionLineExport>, AppSqliteError> {
    459         let mut statement = self
    460             .connection
    461             .prepare(
    462                 "select id, quantity_value, listing_bin_id
    463                  from order_lines
    464                  where order_id = ?1
    465                  order by sort_index asc, id asc",
    466             )
    467             .map_err(|source| AppSqliteError::Query {
    468                 operation: "prepare seller order decision lines",
    469                 source,
    470             })?;
    471         let rows = statement
    472             .query_map(params![order_id.to_string()], |row| {
    473                 Ok((
    474                     row.get::<_, String>(0)?,
    475                     row.get::<_, i64>(1)?,
    476                     row.get::<_, Option<String>>(2)?,
    477                 ))
    478             })
    479             .map_err(|source| AppSqliteError::Query {
    480                 operation: "query seller order decision lines",
    481                 source,
    482             })?;
    483         let mut lines = Vec::new();
    484 
    485         for row in rows {
    486             let (line_id, quantity, listing_bin_id) =
    487                 row.map_err(|source| AppSqliteError::Query {
    488                     operation: "read seller order decision line",
    489                     source,
    490                 })?;
    491             let product_id = parse_order_line_product_id(line_id.as_str(), order_id)?;
    492             let quantity =
    493                 u32::try_from(quantity).map_err(|_| AppSqliteError::InvalidProjection {
    494                     reason: "seller order decision quantity must be non-negative",
    495                 })?;
    496             if quantity == 0 {
    497                 return Err(AppSqliteError::InvalidProjection {
    498                     reason: "seller order decision quantity must be positive",
    499                 });
    500             }
    501             lines.push(SellerOrderDecisionLineExport {
    502                 product_id,
    503                 listing_bin_id: empty_string_to_none(listing_bin_id),
    504                 quantity,
    505                 stock_count: self.load_product_stock_count(product_id)?,
    506                 reserved_quantity: self.load_reserved_product_quantity(product_id, order_id)?,
    507             });
    508         }
    509 
    510         Ok(lines)
    511     }
    512 
    513     fn load_product_stock_count(
    514         &self,
    515         product_id: ProductId,
    516     ) -> Result<Option<u32>, AppSqliteError> {
    517         let stock_count = self
    518             .connection
    519             .query_row(
    520                 "select stock_count from products where id = ?1 limit 1",
    521                 params![product_id.to_string()],
    522                 |row| row.get::<_, Option<i64>>(0),
    523             )
    524             .optional()
    525             .map_err(|source| AppSqliteError::Query {
    526                 operation: "load seller order decision product stock",
    527                 source,
    528             })?
    529             .flatten();
    530 
    531         stock_count
    532             .map(|value| {
    533                 u32::try_from(value).map_err(|_| AppSqliteError::InvalidProjection {
    534                     reason: "seller order decision product stock must be non-negative",
    535                 })
    536             })
    537             .transpose()
    538     }
    539 
    540     fn load_reserved_product_quantity(
    541         &self,
    542         product_id: ProductId,
    543         excluding_order_id: OrderId,
    544     ) -> Result<u32, AppSqliteError> {
    545         let mut statement = self
    546             .connection
    547             .prepare(
    548                 "select ol.id, ol.quantity_value
    549                  from order_lines ol
    550                  inner join orders o on o.id = ol.order_id
    551                  where o.status in ('scheduled', 'packed')
    552                    and o.id <> ?1",
    553             )
    554             .map_err(|source| AppSqliteError::Query {
    555                 operation: "prepare seller order decision reservations",
    556                 source,
    557             })?;
    558         let rows = statement
    559             .query_map(params![excluding_order_id.to_string()], |row| {
    560                 Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?))
    561             })
    562             .map_err(|source| AppSqliteError::Query {
    563                 operation: "query seller order decision reservations",
    564                 source,
    565             })?;
    566         let mut reserved_quantity = 0_u32;
    567 
    568         for row in rows {
    569             let (line_id, quantity) = row.map_err(|source| AppSqliteError::Query {
    570                 operation: "read seller order decision reservation",
    571                 source,
    572             })?;
    573             let Some(reserved_product_id) = parse_order_line_product_id_lossy(line_id.as_str())
    574             else {
    575                 continue;
    576             };
    577             if reserved_product_id != product_id {
    578                 continue;
    579             }
    580             let quantity =
    581                 u32::try_from(quantity).map_err(|_| AppSqliteError::InvalidProjection {
    582                     reason: "seller order decision reserved quantity must be non-negative",
    583                 })?;
    584             reserved_quantity = reserved_quantity.checked_add(quantity).ok_or(
    585                 AppSqliteError::InvalidProjection {
    586                     reason: "seller order decision reserved quantity overflowed",
    587                 },
    588             )?;
    589         }
    590 
    591         Ok(reserved_quantity)
    592     }
    593 
    594     fn load_fulfillment_window_by_id(
    595         &self,
    596         farm_id: FarmId,
    597         fulfillment_window_id: FulfillmentWindowId,
    598     ) -> Result<Option<FulfillmentWindowSummary>, AppSqliteError> {
    599         self.connection
    600             .query_row(
    601                 "select id, starts_at, ends_at
    602                  from fulfillment_windows
    603                  where farm_id = ?1 and id = ?2
    604                  limit 1",
    605                 params![farm_id.to_string(), fulfillment_window_id.to_string()],
    606                 |row| {
    607                     Ok((
    608                         row.get::<_, String>(0)?,
    609                         row.get::<_, String>(1)?,
    610                         row.get::<_, String>(2)?,
    611                     ))
    612                 },
    613             )
    614             .optional()
    615             .map_err(|source| AppSqliteError::Query {
    616                 operation: "load pack day fulfillment window",
    617                 source,
    618             })?
    619             .map(|(fulfillment_window_id, starts_at, ends_at)| {
    620                 Ok(FulfillmentWindowSummary {
    621                     fulfillment_window_id: parse_typed_id(
    622                         "fulfillment_windows.id",
    623                         fulfillment_window_id,
    624                     )?,
    625                     farm_id,
    626                     starts_at,
    627                     ends_at,
    628                 })
    629             })
    630             .transpose()
    631     }
    632 
    633     fn load_next_upcoming_fulfillment_window(
    634         &self,
    635         farm_id: FarmId,
    636     ) -> Result<Option<FulfillmentWindowSummary>, AppSqliteError> {
    637         self.connection
    638             .query_row(
    639                 "select id, starts_at, ends_at
    640                  from fulfillment_windows
    641                  where farm_id = ?1 and starts_at >= strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
    642                  order by starts_at asc, id asc
    643                  limit 1",
    644                 params![farm_id.to_string()],
    645                 |row| {
    646                     Ok((
    647                         row.get::<_, String>(0)?,
    648                         row.get::<_, String>(1)?,
    649                         row.get::<_, String>(2)?,
    650                     ))
    651                 },
    652             )
    653             .optional()
    654             .map_err(|source| AppSqliteError::Query {
    655                 operation: "load next pack day fulfillment window",
    656                 source,
    657             })?
    658             .map(|(fulfillment_window_id, starts_at, ends_at)| {
    659                 Ok(FulfillmentWindowSummary {
    660                     fulfillment_window_id: parse_typed_id(
    661                         "fulfillment_windows.id",
    662                         fulfillment_window_id,
    663                     )?,
    664                     farm_id,
    665                     starts_at,
    666                     ends_at,
    667                 })
    668             })
    669             .transpose()
    670     }
    671 
    672     fn load_first_active_order_window(
    673         &self,
    674         farm_id: FarmId,
    675     ) -> Result<Option<FulfillmentWindowSummary>, AppSqliteError> {
    676         self.connection
    677             .query_row(
    678                 "select fw.id, fw.starts_at, fw.ends_at
    679                  from orders o
    680                  join fulfillment_windows fw on fw.id = o.fulfillment_window_id
    681                  where o.farm_id = ?1
    682                    and o.status in ('needs_action', 'scheduled', 'packed')
    683                  order by fw.starts_at asc, fw.id asc
    684                  limit 1",
    685                 params![farm_id.to_string()],
    686                 |row| {
    687                     Ok((
    688                         row.get::<_, String>(0)?,
    689                         row.get::<_, String>(1)?,
    690                         row.get::<_, String>(2)?,
    691                     ))
    692                 },
    693             )
    694             .optional()
    695             .map_err(|source| AppSqliteError::Query {
    696                 operation: "load active pack day fulfillment window",
    697                 source,
    698             })?
    699             .map(|(fulfillment_window_id, starts_at, ends_at)| {
    700                 Ok(FulfillmentWindowSummary {
    701                     fulfillment_window_id: parse_typed_id(
    702                         "fulfillment_windows.id",
    703                         fulfillment_window_id,
    704                     )?,
    705                     farm_id,
    706                     starts_at,
    707                     ends_at,
    708                 })
    709             })
    710             .transpose()
    711     }
    712 
    713     fn load_pack_day_output_window(
    714         &self,
    715         farm_id: FarmId,
    716         fulfillment_window_id: FulfillmentWindowId,
    717     ) -> Result<Option<PackDayOutputWindow>, AppSqliteError> {
    718         self.connection
    719             .query_row(
    720                 "select
    721                     fw.id,
    722                     fw.farm_id,
    723                     f.display_name,
    724                     pl.label,
    725                     fw.starts_at,
    726                     fw.ends_at
    727                  from fulfillment_windows fw
    728                  join farms f on f.id = fw.farm_id
    729                  left join pickup_locations pl on pl.id = fw.pickup_location_id
    730                  where fw.farm_id = ?1 and fw.id = ?2
    731                  limit 1",
    732                 params![farm_id.to_string(), fulfillment_window_id.to_string()],
    733                 |row| {
    734                     Ok((
    735                         row.get::<_, String>(0)?,
    736                         row.get::<_, String>(1)?,
    737                         row.get::<_, String>(2)?,
    738                         row.get::<_, Option<String>>(3)?,
    739                         row.get::<_, String>(4)?,
    740                         row.get::<_, String>(5)?,
    741                     ))
    742                 },
    743             )
    744             .optional()
    745             .map_err(|source| AppSqliteError::Query {
    746                 operation: "load pack day output window",
    747                 source,
    748             })?
    749             .map(
    750                 |(
    751                     window_id,
    752                     row_farm_id,
    753                     farm_display_name,
    754                     pickup_location_label,
    755                     starts_at,
    756                     ends_at,
    757                 )| {
    758                     Ok(PackDayOutputWindow {
    759                         fulfillment_window_id: parse_typed_id("fulfillment_windows.id", window_id)?,
    760                         farm_id: parse_typed_id("fulfillment_windows.farm_id", row_farm_id)?,
    761                         farm_display_name,
    762                         pickup_location_label: empty_string_to_none(pickup_location_label),
    763                         starts_at,
    764                         ends_at,
    765                     })
    766                 },
    767             )
    768             .transpose()
    769     }
    770 
    771     fn load_pack_day_totals(
    772         &self,
    773         farm_id: FarmId,
    774         fulfillment_window_id: FulfillmentWindowId,
    775     ) -> Result<Vec<PackDayProductTotalRow>, AppSqliteError> {
    776         let mut statement = self
    777             .connection
    778             .prepare(
    779                 "select l.title, l.quantity_value, l.quantity_unit_label
    780                  from order_lines l
    781                  join orders o on o.id = l.order_id
    782                  where o.farm_id = ?1
    783                    and o.fulfillment_window_id = ?2
    784                    and o.status in ('needs_action', 'scheduled', 'packed')
    785                  order by l.title asc, l.sort_index asc, l.id asc",
    786             )
    787             .map_err(|source| AppSqliteError::Query {
    788                 operation: "prepare pack day totals",
    789                 source,
    790             })?;
    791         let rows = statement
    792             .query_map(
    793                 params![farm_id.to_string(), fulfillment_window_id.to_string()],
    794                 |row| {
    795                     Ok((
    796                         row.get::<_, String>(0)?,
    797                         row.get::<_, u32>(1)?,
    798                         row.get::<_, String>(2)?,
    799                     ))
    800                 },
    801             )
    802             .map_err(|source| AppSqliteError::Query {
    803                 operation: "query pack day totals",
    804                 source,
    805             })?;
    806         let mut totals = BTreeMap::<(String, String), u32>::new();
    807 
    808         for row in rows {
    809             let (title, quantity_value, quantity_unit_label) =
    810                 row.map_err(|source| AppSqliteError::Query {
    811                     operation: "read pack day totals",
    812                     source,
    813                 })?;
    814             *totals.entry((title, quantity_unit_label)).or_insert(0) += quantity_value;
    815         }
    816 
    817         Ok(totals
    818             .into_iter()
    819             .map(
    820                 |((title, quantity_unit_label), quantity_value)| PackDayProductTotalRow {
    821                     title,
    822                     quantity_display: format_quantity_display(quantity_value, &quantity_unit_label),
    823                 },
    824             )
    825             .collect())
    826     }
    827 
    828     fn load_pack_day_pack_list(
    829         &self,
    830         farm_id: FarmId,
    831         fulfillment_window_id: FulfillmentWindowId,
    832     ) -> Result<Vec<PackDayPackListRow>, AppSqliteError> {
    833         let mut statement = self
    834             .connection
    835             .prepare(
    836                 "select o.customer_display_name, l.title, l.quantity_display
    837                  from order_lines l
    838                  join orders o on o.id = l.order_id
    839                  where o.farm_id = ?1
    840                    and o.fulfillment_window_id = ?2
    841                    and o.status in ('needs_action', 'scheduled', 'packed')
    842                  order by l.title asc, o.customer_display_name asc, o.order_number asc, l.sort_index asc, l.id asc",
    843             )
    844             .map_err(|source| AppSqliteError::Query {
    845                 operation: "prepare pack day pack list",
    846                 source,
    847             })?;
    848         let rows = statement
    849             .query_map(
    850                 params![farm_id.to_string(), fulfillment_window_id.to_string()],
    851                 |row| {
    852                     Ok(PackDayPackListRow {
    853                         title: row.get(1)?,
    854                         quantity_display: format!(
    855                             "{}: {}",
    856                             row.get::<_, String>(0)?,
    857                             row.get::<_, String>(2)?
    858                         ),
    859                     })
    860                 },
    861             )
    862             .map_err(|source| AppSqliteError::Query {
    863                 operation: "query pack day pack list",
    864                 source,
    865             })?;
    866 
    867         rows.collect::<Result<Vec<_>, _>>()
    868             .map_err(|source| AppSqliteError::Query {
    869                 operation: "read pack day pack list",
    870                 source,
    871             })
    872     }
    873 
    874     fn load_pack_day_output_totals(
    875         &self,
    876         farm_id: FarmId,
    877         fulfillment_window_id: FulfillmentWindowId,
    878     ) -> Result<Vec<PackDayOutputProductTotal>, AppSqliteError> {
    879         let mut statement = self
    880             .connection
    881             .prepare(
    882                 "select l.title, l.quantity_value, l.quantity_unit_label
    883                  from order_lines l
    884                  join orders o on o.id = l.order_id
    885                  where o.farm_id = ?1
    886                    and o.fulfillment_window_id = ?2
    887                    and o.status in ('needs_action', 'scheduled', 'packed')
    888                  order by l.title asc, l.sort_index asc, l.id asc",
    889             )
    890             .map_err(|source| AppSqliteError::Query {
    891                 operation: "prepare pack day output totals",
    892                 source,
    893             })?;
    894         let rows = statement
    895             .query_map(
    896                 params![farm_id.to_string(), fulfillment_window_id.to_string()],
    897                 |row| {
    898                     Ok((
    899                         row.get::<_, String>(0)?,
    900                         row.get::<_, u32>(1)?,
    901                         row.get::<_, String>(2)?,
    902                     ))
    903                 },
    904             )
    905             .map_err(|source| AppSqliteError::Query {
    906                 operation: "query pack day output totals",
    907                 source,
    908             })?;
    909         let mut totals = BTreeMap::<(String, String), u32>::new();
    910 
    911         for row in rows {
    912             let (title, quantity_value, quantity_unit_label) =
    913                 row.map_err(|source| AppSqliteError::Query {
    914                     operation: "read pack day output totals",
    915                     source,
    916                 })?;
    917             *totals.entry((title, quantity_unit_label)).or_insert(0) += quantity_value;
    918         }
    919 
    920         Ok(totals
    921             .into_iter()
    922             .map(
    923                 |((title, quantity_unit_label), quantity_value)| PackDayOutputProductTotal {
    924                     title,
    925                     quantity: PackDayOutputQuantity::new(quantity_value, quantity_unit_label),
    926                 },
    927             )
    928             .collect())
    929     }
    930 
    931     fn load_pack_day_output_pack_list(
    932         &self,
    933         farm_id: FarmId,
    934         fulfillment_window_id: FulfillmentWindowId,
    935     ) -> Result<Vec<PackDayOutputPackListEntry>, AppSqliteError> {
    936         let mut statement = self
    937             .connection
    938             .prepare(
    939                 "select
    940                     o.id,
    941                     o.order_number,
    942                     o.customer_display_name,
    943                     o.status,
    944                     l.title,
    945                     l.quantity_value,
    946                     l.quantity_unit_label
    947                  from order_lines l
    948                  join orders o on o.id = l.order_id
    949                  where o.farm_id = ?1
    950                    and o.fulfillment_window_id = ?2
    951                    and o.status in ('needs_action', 'scheduled', 'packed')
    952                  order by l.title asc, o.customer_display_name asc, o.order_number asc, l.sort_index asc, l.id asc",
    953             )
    954             .map_err(|source| AppSqliteError::Query {
    955                 operation: "prepare pack day output pack list",
    956                 source,
    957             })?;
    958         let rows = statement
    959             .query_map(
    960                 params![farm_id.to_string(), fulfillment_window_id.to_string()],
    961                 |row| {
    962                     Ok((
    963                         row.get::<_, String>(0)?,
    964                         row.get::<_, String>(1)?,
    965                         row.get::<_, String>(2)?,
    966                         row.get::<_, String>(3)?,
    967                         row.get::<_, String>(4)?,
    968                         row.get::<_, u32>(5)?,
    969                         row.get::<_, String>(6)?,
    970                     ))
    971                 },
    972             )
    973             .map_err(|source| AppSqliteError::Query {
    974                 operation: "query pack day output pack list",
    975                 source,
    976             })?;
    977         let mut pack_list = Vec::new();
    978 
    979         for row in rows {
    980             let (
    981                 order_id,
    982                 order_number,
    983                 customer_display_name,
    984                 status,
    985                 title,
    986                 quantity_value,
    987                 quantity_unit_label,
    988             ) = row.map_err(|source| AppSqliteError::Query {
    989                 operation: "read pack day output pack list",
    990                 source,
    991             })?;
    992             pack_list.push(PackDayOutputPackListEntry {
    993                 order_id: parse_typed_id("orders.id", order_id)?,
    994                 order_number,
    995                 customer_display_name,
    996                 order_state: parse_pack_day_output_order_state("orders.status", status)?,
    997                 title,
    998                 quantity: PackDayOutputQuantity::new(quantity_value, quantity_unit_label),
    999             });
   1000         }
   1001 
   1002         Ok(pack_list)
   1003     }
   1004 
   1005     fn load_pack_day_roster(
   1006         &self,
   1007         farm_id: FarmId,
   1008         fulfillment_window_id: FulfillmentWindowId,
   1009     ) -> Result<Vec<PackDayRosterRow>, AppSqliteError> {
   1010         let mut statement = self
   1011             .connection
   1012             .prepare(
   1013                 "select id, order_number, customer_display_name
   1014                  from orders
   1015                  where farm_id = ?1
   1016                    and fulfillment_window_id = ?2
   1017                    and status in ('needs_action', 'scheduled', 'packed')
   1018                  order by customer_display_name asc, order_number asc, id asc",
   1019             )
   1020             .map_err(|source| AppSqliteError::Query {
   1021                 operation: "prepare pack day roster",
   1022                 source,
   1023             })?;
   1024         let rows = statement
   1025             .query_map(
   1026                 params![farm_id.to_string(), fulfillment_window_id.to_string()],
   1027                 |row| {
   1028                     Ok((
   1029                         row.get::<_, String>(0)?,
   1030                         row.get::<_, String>(1)?,
   1031                         row.get::<_, String>(2)?,
   1032                     ))
   1033                 },
   1034             )
   1035             .map_err(|source| AppSqliteError::Query {
   1036                 operation: "query pack day roster",
   1037                 source,
   1038             })?;
   1039         let mut roster = Vec::new();
   1040 
   1041         for row in rows {
   1042             let (order_id, order_number, customer_display_name) =
   1043                 row.map_err(|source| AppSqliteError::Query {
   1044                     operation: "read pack day roster",
   1045                     source,
   1046                 })?;
   1047             roster.push(PackDayRosterRow {
   1048                 order_id: parse_typed_id("orders.id", order_id)?,
   1049                 order_number,
   1050                 customer_display_name,
   1051             });
   1052         }
   1053 
   1054         Ok(roster)
   1055     }
   1056 
   1057     fn load_pack_day_output_roster(
   1058         &self,
   1059         farm_id: FarmId,
   1060         fulfillment_window_id: FulfillmentWindowId,
   1061     ) -> Result<Vec<PackDayOutputCustomerOrder>, AppSqliteError> {
   1062         let mut statement = self
   1063             .connection
   1064             .prepare(
   1065                 "select id, order_number, customer_display_name, status
   1066                  from orders
   1067                  where farm_id = ?1
   1068                    and fulfillment_window_id = ?2
   1069                    and status in ('needs_action', 'scheduled', 'packed')
   1070                  order by customer_display_name asc, order_number asc, id asc",
   1071             )
   1072             .map_err(|source| AppSqliteError::Query {
   1073                 operation: "prepare pack day output roster",
   1074                 source,
   1075             })?;
   1076         let rows = statement
   1077             .query_map(
   1078                 params![farm_id.to_string(), fulfillment_window_id.to_string()],
   1079                 |row| {
   1080                     Ok((
   1081                         row.get::<_, String>(0)?,
   1082                         row.get::<_, String>(1)?,
   1083                         row.get::<_, String>(2)?,
   1084                         row.get::<_, String>(3)?,
   1085                     ))
   1086                 },
   1087             )
   1088             .map_err(|source| AppSqliteError::Query {
   1089                 operation: "query pack day output roster",
   1090                 source,
   1091             })?;
   1092         let mut roster = Vec::new();
   1093 
   1094         for row in rows {
   1095             let (order_id, order_number, customer_display_name, status) =
   1096                 row.map_err(|source| AppSqliteError::Query {
   1097                     operation: "read pack day output roster",
   1098                     source,
   1099                 })?;
   1100             roster.push(PackDayOutputCustomerOrder {
   1101                 order_id: parse_typed_id("orders.id", order_id)?,
   1102                 order_number,
   1103                 customer_display_name,
   1104                 order_state: parse_pack_day_output_order_state("orders.status", status)?,
   1105             });
   1106         }
   1107 
   1108         Ok(roster)
   1109     }
   1110 }
   1111 
   1112 #[derive(Clone, Debug, Eq, PartialEq)]
   1113 struct OrderRecord {
   1114     order_id: OrderId,
   1115     farm_id: FarmId,
   1116     fulfillment_window_id: Option<FulfillmentWindowId>,
   1117     order_number: String,
   1118     customer_display_name: String,
   1119     fulfillment_window_label: Option<String>,
   1120     pickup_location_label: Option<String>,
   1121     status: OrderStatus,
   1122     workflow: TradeWorkflowProjection,
   1123 }
   1124 
   1125 impl OrderRecord {
   1126     fn matches_filter(&self, filter: OrdersFilter) -> bool {
   1127         match filter {
   1128             OrdersFilter::All => true,
   1129             OrdersFilter::NeedsAction => {
   1130                 matches!(
   1131                     self.status,
   1132                     OrderStatus::NeedsAction | OrderStatus::NeedsReview
   1133                 )
   1134             }
   1135             OrdersFilter::Scheduled => self.status == OrderStatus::Scheduled,
   1136             OrdersFilter::Packed => self.status == OrderStatus::Packed,
   1137             OrdersFilter::Completed => self.status == OrderStatus::Completed,
   1138         }
   1139     }
   1140 
   1141     fn into_list_row(self) -> OrdersListRow {
   1142         OrdersListRow {
   1143             order_id: self.order_id,
   1144             farm_id: self.farm_id,
   1145             fulfillment_window_id: self.fulfillment_window_id,
   1146             order_number: self.order_number,
   1147             customer_display_name: self.customer_display_name,
   1148             fulfillment_window_label: self.fulfillment_window_label,
   1149             pickup_location_label: self.pickup_location_label,
   1150             status: self.status,
   1151             primary_action: primary_action_for_order(self.status, &self.workflow),
   1152             workflow: self.workflow,
   1153         }
   1154     }
   1155 }
   1156 
   1157 fn summarize_orders(records: &[OrderRecord]) -> OrdersListSummary {
   1158     let mut summary = OrdersListSummary {
   1159         total_orders: records.len() as u32,
   1160         ..OrdersListSummary::default()
   1161     };
   1162 
   1163     for record in records {
   1164         match record.status {
   1165             OrderStatus::NeedsAction | OrderStatus::NeedsReview => summary.needs_action_orders += 1,
   1166             OrderStatus::Scheduled => summary.scheduled_orders += 1,
   1167             OrderStatus::Packed => summary.packed_orders += 1,
   1168             OrderStatus::Completed | OrderStatus::Declined => {}
   1169         }
   1170     }
   1171 
   1172     summary
   1173 }
   1174 
   1175 fn primary_action_for_order(
   1176     status: OrderStatus,
   1177     _workflow: &TradeWorkflowProjection,
   1178 ) -> Option<OrderPrimaryAction> {
   1179     match status {
   1180         OrderStatus::NeedsAction => Some(OrderPrimaryAction::Review),
   1181         OrderStatus::Scheduled
   1182         | OrderStatus::Packed
   1183         | OrderStatus::Completed
   1184         | OrderStatus::Declined
   1185         | OrderStatus::NeedsReview => None,
   1186     }
   1187 }
   1188 
   1189 fn format_quantity_display(quantity_value: u32, quantity_unit_label: &str) -> String {
   1190     if quantity_unit_label.trim().is_empty() {
   1191         quantity_value.to_string()
   1192     } else {
   1193         format!("{quantity_value} {}", quantity_unit_label.trim())
   1194     }
   1195 }
   1196 
   1197 fn parse_typed_id<T>(field: &'static str, value: String) -> Result<T, AppSqliteError>
   1198 where
   1199     T: std::str::FromStr,
   1200 {
   1201     value
   1202         .parse()
   1203         .map_err(|_| AppSqliteError::DecodeId { field, value })
   1204 }
   1205 
   1206 fn parse_optional_typed_id<T>(
   1207     field: &'static str,
   1208     value: Option<String>,
   1209 ) -> Result<Option<T>, AppSqliteError>
   1210 where
   1211     T: std::str::FromStr,
   1212 {
   1213     value.map(|value| parse_typed_id(field, value)).transpose()
   1214 }
   1215 
   1216 fn parse_order_line_product_id(
   1217     line_id: &str,
   1218     order_id: OrderId,
   1219 ) -> Result<ProductId, AppSqliteError> {
   1220     let prefix = format!("{order_id}:");
   1221     let Some(product_id) = line_id.strip_prefix(prefix.as_str()) else {
   1222         return Err(AppSqliteError::InvalidProjection {
   1223             reason: "seller order decision line id must include order id prefix",
   1224         });
   1225     };
   1226 
   1227     parse_typed_id("order_lines.product_id", product_id.to_owned())
   1228 }
   1229 
   1230 fn parse_order_line_product_id_lossy(line_id: &str) -> Option<ProductId> {
   1231     line_id
   1232         .rsplit_once(':')
   1233         .and_then(|(_, product_id)| product_id.parse().ok())
   1234 }
   1235 
   1236 fn parse_order_status(field: &'static str, value: String) -> Result<OrderStatus, AppSqliteError> {
   1237     match value.as_str() {
   1238         "needs_action" => Ok(OrderStatus::NeedsAction),
   1239         "scheduled" => Ok(OrderStatus::Scheduled),
   1240         "packed" => Ok(OrderStatus::Packed),
   1241         "completed" => Ok(OrderStatus::Completed),
   1242         "declined" => Ok(OrderStatus::Declined),
   1243         "needs_review" => Ok(OrderStatus::NeedsReview),
   1244         _ => Err(AppSqliteError::DecodeEnum { field, value }),
   1245     }
   1246 }
   1247 
   1248 fn parse_pack_day_output_order_state(
   1249     field: &'static str,
   1250     value: String,
   1251 ) -> Result<PackDayOutputOrderState, AppSqliteError> {
   1252     let status = parse_order_status(field, value)?;
   1253     PackDayOutputOrderState::from_order_status(status).ok_or(AppSqliteError::InvalidProjection {
   1254         reason: "pack day output source may only include needs_action, scheduled, or packed orders",
   1255     })
   1256 }
   1257 
   1258 fn empty_string_to_none(value: Option<String>) -> Option<String> {
   1259     value.and_then(|value| {
   1260         let trimmed = value.trim().to_owned();
   1261         if trimmed.is_empty() {
   1262             None
   1263         } else {
   1264             Some(trimmed)
   1265         }
   1266     })
   1267 }
   1268 
   1269 #[cfg(test)]
   1270 mod tests {
   1271     use radroots_app_view::{
   1272         FarmId, FulfillmentWindowId, OrderId, OrderPrimaryAction, OrderStatus, OrdersFilter,
   1273         OrdersScreenQueryState, PackDayOutputOrderState, PackDayProductTotalRow,
   1274         PackDayScreenQueryState, PickupLocationId, TradeAgreementStatus, TradeInventoryStatus,
   1275         TradeRevisionStatus, TradeWorkflowSource,
   1276     };
   1277     use rusqlite::{Connection, params};
   1278 
   1279     use crate::{AppSqliteError, AppSqliteStore, DatabaseTarget};
   1280 
   1281     #[test]
   1282     fn orders_list_loads_summary_rows_and_window_filter_truthfully() {
   1283         let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
   1284         let connection = store.connection();
   1285         let farm_id = FarmId::new();
   1286         let other_farm_id = FarmId::new();
   1287         let fulfillment_window_id = FulfillmentWindowId::new();
   1288         let other_window_id = FulfillmentWindowId::new();
   1289 
   1290         insert_farm(
   1291             connection,
   1292             farm_id,
   1293             "Willow farm",
   1294             "ready",
   1295             "2026-04-17T08:00:00Z",
   1296         );
   1297         insert_farm(
   1298             connection,
   1299             other_farm_id,
   1300             "Other farm",
   1301             "ready",
   1302             "2026-04-17T08:00:00Z",
   1303         );
   1304         let pickup_location_id = insert_pickup_location(connection, farm_id, "North barn", true);
   1305         insert_window(
   1306             connection,
   1307             fulfillment_window_id,
   1308             farm_id,
   1309             Some(pickup_location_id),
   1310             "Friday pickup",
   1311             "2099-04-18T16:00:00Z",
   1312             "2099-04-18T18:00:00Z",
   1313             "2099-04-17T18:00:00Z",
   1314         );
   1315         insert_window(
   1316             connection,
   1317             other_window_id,
   1318             farm_id,
   1319             None,
   1320             "Saturday pickup",
   1321             "2099-04-19T16:00:00Z",
   1322             "2099-04-19T18:00:00Z",
   1323             "2099-04-18T18:00:00Z",
   1324         );
   1325 
   1326         insert_order(
   1327             connection,
   1328             OrderId::new(),
   1329             farm_id,
   1330             Some(fulfillment_window_id),
   1331             "R-100",
   1332             "Casey",
   1333             "needs_action",
   1334             "2026-04-17T10:00:00Z",
   1335         );
   1336         insert_order(
   1337             connection,
   1338             OrderId::new(),
   1339             farm_id,
   1340             Some(fulfillment_window_id),
   1341             "R-101",
   1342             "Taylor",
   1343             "scheduled",
   1344             "2026-04-17T11:00:00Z",
   1345         );
   1346         insert_order(
   1347             connection,
   1348             OrderId::new(),
   1349             farm_id,
   1350             Some(fulfillment_window_id),
   1351             "R-102",
   1352             "Robin",
   1353             "packed",
   1354             "2026-04-17T12:00:00Z",
   1355         );
   1356         insert_order(
   1357             connection,
   1358             OrderId::new(),
   1359             farm_id,
   1360             Some(other_window_id),
   1361             "R-103",
   1362             "Morgan",
   1363             "completed",
   1364             "2026-04-17T13:00:00Z",
   1365         );
   1366         insert_order(
   1367             connection,
   1368             OrderId::new(),
   1369             farm_id,
   1370             None,
   1371             "R-104",
   1372             "Alex",
   1373             "needs_review",
   1374             "2026-04-17T14:00:00Z",
   1375         );
   1376         insert_order(
   1377             connection,
   1378             OrderId::new(),
   1379             other_farm_id,
   1380             Some(fulfillment_window_id),
   1381             "R-999",
   1382             "Other",
   1383             "needs_action",
   1384             "2026-04-17T15:00:00Z",
   1385         );
   1386 
   1387         let projection = store
   1388             .load_orders_list(
   1389                 farm_id,
   1390                 &OrdersScreenQueryState {
   1391                     filter: OrdersFilter::NeedsAction,
   1392                     fulfillment_window_id: Some(fulfillment_window_id),
   1393                 },
   1394             )
   1395             .expect("orders list should load");
   1396 
   1397         assert_eq!(projection.summary.total_orders, 3);
   1398         assert_eq!(projection.summary.needs_action_orders, 1);
   1399         assert_eq!(projection.summary.scheduled_orders, 1);
   1400         assert_eq!(projection.summary.packed_orders, 1);
   1401         assert_eq!(projection.rows.len(), 1);
   1402         assert_eq!(projection.rows[0].order_number, "R-100");
   1403         assert_eq!(
   1404             projection.rows[0].fulfillment_window_label.as_deref(),
   1405             Some("Friday pickup")
   1406         );
   1407         assert_eq!(
   1408             projection.rows[0].pickup_location_label.as_deref(),
   1409             Some("North barn")
   1410         );
   1411         assert_eq!(
   1412             projection.rows[0].primary_action,
   1413             Some(OrderPrimaryAction::Review)
   1414         );
   1415     }
   1416 
   1417     #[test]
   1418     fn order_detail_loads_items_and_context() {
   1419         let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
   1420         let connection = store.connection();
   1421         let farm_id = FarmId::new();
   1422         let fulfillment_window_id = FulfillmentWindowId::new();
   1423         let order_id = OrderId::new();
   1424 
   1425         insert_farm(
   1426             connection,
   1427             farm_id,
   1428             "Willow farm",
   1429             "ready",
   1430             "2026-04-17T08:00:00Z",
   1431         );
   1432         let pickup_location_id = insert_pickup_location(connection, farm_id, "North barn", true);
   1433         insert_window(
   1434             connection,
   1435             fulfillment_window_id,
   1436             farm_id,
   1437             Some(pickup_location_id),
   1438             "Friday pickup",
   1439             "2099-04-18T16:00:00Z",
   1440             "2099-04-18T18:00:00Z",
   1441             "2099-04-17T18:00:00Z",
   1442         );
   1443         insert_order(
   1444             connection,
   1445             order_id,
   1446             farm_id,
   1447             Some(fulfillment_window_id),
   1448             "R-100",
   1449             "Casey",
   1450             "scheduled",
   1451             "2026-04-17T10:00:00Z",
   1452         );
   1453         insert_order_line(
   1454             connection,
   1455             "line-1",
   1456             order_id,
   1457             "Salad mix",
   1458             2,
   1459             "bags",
   1460             "2 bags",
   1461             0,
   1462         );
   1463         insert_order_line(
   1464             connection, "line-2", order_id, "Carrots", 1, "bunches", "1 bunch", 1,
   1465         );
   1466 
   1467         let detail = store
   1468             .load_order_detail(farm_id, order_id)
   1469             .expect("order detail should load")
   1470             .expect("order detail should exist");
   1471 
   1472         assert_eq!(detail.order_number, "R-100");
   1473         assert_eq!(detail.customer_display_name, "Casey");
   1474         assert_eq!(detail.status, OrderStatus::Scheduled);
   1475         assert_eq!(
   1476             detail.fulfillment_window_label.as_deref(),
   1477             Some("Friday pickup")
   1478         );
   1479         assert_eq!(detail.pickup_location_label.as_deref(), Some("North barn"));
   1480         assert_eq!(detail.items.len(), 2);
   1481         assert_eq!(detail.items[0].title, "Salad mix");
   1482         assert_eq!(
   1483             detail.items[0]
   1484                 .unit_price
   1485                 .as_ref()
   1486                 .map(|price| price.amount_minor_units),
   1487             Some(650)
   1488         );
   1489         assert_eq!(detail.items[0].line_total_minor_units, Some(1300));
   1490         assert_eq!(detail.items[1].quantity_display, "1 bunch");
   1491         assert_eq!(detail.economics.total_minor_units, Some(1950));
   1492         assert_eq!(detail.economics.currency_code.as_deref(), Some("USD"));
   1493         assert_eq!(detail.primary_action, None);
   1494     }
   1495 
   1496     #[test]
   1497     fn seller_order_projections_fail_closed_for_invalid_workflow_revision() {
   1498         let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
   1499         let connection = store.connection();
   1500         let farm_id = FarmId::new();
   1501         let order_id = OrderId::new();
   1502 
   1503         insert_farm(
   1504             connection,
   1505             farm_id,
   1506             "Willow farm",
   1507             "ready",
   1508             "2026-04-17T08:00:00Z",
   1509         );
   1510         insert_order(
   1511             connection,
   1512             order_id,
   1513             farm_id,
   1514             None,
   1515             "R-100",
   1516             "Casey",
   1517             "scheduled",
   1518             "2026-04-17T10:00:00Z",
   1519         );
   1520         set_order_workflow_revision(
   1521             connection,
   1522             order_id,
   1523             TradeRevisionStatus::Updated.storage_key(),
   1524         );
   1525 
   1526         let list = store
   1527             .load_orders_list(
   1528                 farm_id,
   1529                 &OrdersScreenQueryState {
   1530                     filter: OrdersFilter::All,
   1531                     fulfillment_window_id: None,
   1532                 },
   1533             )
   1534             .expect("valid revision should load in seller list");
   1535         let detail = store
   1536             .load_order_detail(farm_id, order_id)
   1537             .expect("valid revision should load in seller detail")
   1538             .expect("seller detail should exist");
   1539 
   1540         assert_eq!(list.rows[0].workflow.revision, TradeRevisionStatus::Updated);
   1541         assert_eq!(detail.workflow.revision, TradeRevisionStatus::Updated);
   1542 
   1543         corrupt_order_workflow_revision(connection, order_id, "future_revision");
   1544 
   1545         let list_error = store
   1546             .load_orders_list(
   1547                 farm_id,
   1548                 &OrdersScreenQueryState {
   1549                     filter: OrdersFilter::All,
   1550                     fulfillment_window_id: None,
   1551                 },
   1552             )
   1553             .expect_err("invalid revision should fail seller list projection");
   1554         let detail_error = store
   1555             .load_order_detail(farm_id, order_id)
   1556             .expect_err("invalid revision should fail seller detail projection");
   1557 
   1558         assert_decode_enum(list_error, "orders.workflow_revision", "future_revision");
   1559         assert_decode_enum(detail_error, "orders.workflow_revision", "future_revision");
   1560     }
   1561 
   1562     #[test]
   1563     fn seller_order_projections_read_workflow_display_snapshot() {
   1564         let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
   1565         let connection = store.connection();
   1566         let farm_id = FarmId::new();
   1567         let order_id = OrderId::new();
   1568 
   1569         insert_farm(
   1570             connection,
   1571             farm_id,
   1572             "Willow farm",
   1573             "ready",
   1574             "2026-04-17T08:00:00Z",
   1575         );
   1576         insert_order(
   1577             connection,
   1578             order_id,
   1579             farm_id,
   1580             None,
   1581             "R-100",
   1582             "Casey",
   1583             "scheduled",
   1584             "2026-04-17T10:00:00Z",
   1585         );
   1586         insert_order_line(
   1587             connection,
   1588             "line-1",
   1589             order_id,
   1590             "Salad mix",
   1591             2,
   1592             "bags",
   1593             "2 bags",
   1594             0,
   1595         );
   1596         set_order_workflow_revision(
   1597             connection,
   1598             order_id,
   1599             TradeRevisionStatus::Updated.storage_key(),
   1600         );
   1601         set_order_workflow_display_projection(
   1602             connection,
   1603             order_id,
   1604             "confirmed",
   1605             "reserved",
   1606             "local_events",
   1607             Some("seller-workflow-event"),
   1608         );
   1609 
   1610         let list = store
   1611             .load_orders_list(
   1612                 farm_id,
   1613                 &OrdersScreenQueryState {
   1614                     filter: OrdersFilter::All,
   1615                     fulfillment_window_id: None,
   1616                 },
   1617             )
   1618             .expect("seller list should load");
   1619         let detail = store
   1620             .load_order_detail(farm_id, order_id)
   1621             .expect("seller detail should load")
   1622             .expect("seller detail should exist");
   1623         let workflow = &list.rows[0].workflow;
   1624 
   1625         assert_eq!(workflow.agreement, TradeAgreementStatus::Confirmed);
   1626         assert_eq!(workflow.revision, TradeRevisionStatus::Updated);
   1627         assert_eq!(workflow.inventory, TradeInventoryStatus::Reserved);
   1628         assert_eq!(
   1629             workflow.provenance.primary_source,
   1630             TradeWorkflowSource::LocalEvents
   1631         );
   1632         assert_eq!(
   1633             workflow.provenance.last_event_id.as_deref(),
   1634             Some("seller-workflow-event")
   1635         );
   1636         assert_eq!(workflow.economics.total_minor_units, Some(1300));
   1637         assert_eq!(workflow.economics.currency_code.as_deref(), Some("USD"));
   1638         assert_eq!(detail.workflow, *workflow);
   1639         assert_eq!(list.rows[0].primary_action, None);
   1640         assert_eq!(detail.primary_action, None);
   1641     }
   1642 
   1643     #[test]
   1644     fn seller_order_projections_fail_closed_for_invalid_workflow_snapshot_keys() {
   1645         let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
   1646         let connection = store.connection();
   1647         let farm_id = FarmId::new();
   1648         let order_id = OrderId::new();
   1649 
   1650         insert_farm(
   1651             connection,
   1652             farm_id,
   1653             "Willow farm",
   1654             "ready",
   1655             "2026-04-17T08:00:00Z",
   1656         );
   1657         insert_order(
   1658             connection,
   1659             order_id,
   1660             farm_id,
   1661             None,
   1662             "R-100",
   1663             "Casey",
   1664             "scheduled",
   1665             "2026-04-17T10:00:00Z",
   1666         );
   1667         set_order_workflow_display_projection(
   1668             connection,
   1669             order_id,
   1670             "confirmed",
   1671             "reserved",
   1672             "local_events",
   1673             Some("seller-workflow-event"),
   1674         );
   1675 
   1676         for (column, expected_field) in [
   1677             ("workflow_agreement", "orders.workflow_agreement"),
   1678             ("workflow_inventory", "orders.workflow_inventory"),
   1679             (
   1680                 "workflow_provenance_source",
   1681                 "orders.workflow_provenance_source",
   1682             ),
   1683         ] {
   1684             set_order_workflow_display_projection(
   1685                 connection,
   1686                 order_id,
   1687                 "confirmed",
   1688                 "reserved",
   1689                 "local_events",
   1690                 Some("seller-workflow-event"),
   1691             );
   1692             corrupt_order_workflow_display_projection(connection, order_id, column, "future_state");
   1693 
   1694             let list_error = store
   1695                 .load_orders_list(
   1696                     farm_id,
   1697                     &OrdersScreenQueryState {
   1698                         filter: OrdersFilter::All,
   1699                         fulfillment_window_id: None,
   1700                     },
   1701                 )
   1702                 .expect_err("invalid workflow snapshot should fail seller list projection");
   1703             let detail_error = store
   1704                 .load_order_detail(farm_id, order_id)
   1705                 .expect_err("invalid workflow snapshot should fail seller detail projection");
   1706 
   1707             assert_decode_enum(list_error, expected_field, "future_state");
   1708             assert_decode_enum(detail_error, expected_field, "future_state");
   1709         }
   1710     }
   1711 
   1712     #[test]
   1713     fn pack_day_defaults_to_next_window_and_projects_totals_pack_list_and_roster() {
   1714         let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
   1715         let connection = store.connection();
   1716         let farm_id = FarmId::new();
   1717         let next_window_id = FulfillmentWindowId::new();
   1718         let later_window_id = FulfillmentWindowId::new();
   1719         let scheduled_order_id = OrderId::new();
   1720         let packed_order_id = OrderId::new();
   1721 
   1722         insert_farm(
   1723             connection,
   1724             farm_id,
   1725             "Willow farm",
   1726             "ready",
   1727             "2026-04-17T08:00:00Z",
   1728         );
   1729         let pickup_location_id = insert_pickup_location(connection, farm_id, "North barn", true);
   1730         insert_window(
   1731             connection,
   1732             next_window_id,
   1733             farm_id,
   1734             Some(pickup_location_id),
   1735             "Friday pickup",
   1736             "2099-04-18T16:00:00Z",
   1737             "2099-04-18T18:00:00Z",
   1738             "2099-04-17T18:00:00Z",
   1739         );
   1740         insert_window(
   1741             connection,
   1742             later_window_id,
   1743             farm_id,
   1744             Some(pickup_location_id),
   1745             "Saturday pickup",
   1746             "2099-04-19T16:00:00Z",
   1747             "2099-04-19T18:00:00Z",
   1748             "2099-04-18T18:00:00Z",
   1749         );
   1750         insert_order(
   1751             connection,
   1752             scheduled_order_id,
   1753             farm_id,
   1754             Some(next_window_id),
   1755             "R-100",
   1756             "Casey",
   1757             "scheduled",
   1758             "2026-04-17T10:00:00Z",
   1759         );
   1760         insert_order(
   1761             connection,
   1762             packed_order_id,
   1763             farm_id,
   1764             Some(next_window_id),
   1765             "R-101",
   1766             "Taylor",
   1767             "packed",
   1768             "2026-04-17T11:00:00Z",
   1769         );
   1770         insert_order(
   1771             connection,
   1772             OrderId::new(),
   1773             farm_id,
   1774             Some(next_window_id),
   1775             "R-102",
   1776             "Robin",
   1777             "completed",
   1778             "2026-04-17T12:00:00Z",
   1779         );
   1780         insert_order(
   1781             connection,
   1782             OrderId::new(),
   1783             farm_id,
   1784             Some(later_window_id),
   1785             "R-200",
   1786             "Morgan",
   1787             "scheduled",
   1788             "2026-04-17T13:00:00Z",
   1789         );
   1790         insert_order_line(
   1791             connection,
   1792             "line-1",
   1793             scheduled_order_id,
   1794             "Salad mix",
   1795             2,
   1796             "bags",
   1797             "2 bags",
   1798             0,
   1799         );
   1800         insert_order_line(
   1801             connection,
   1802             "line-2",
   1803             packed_order_id,
   1804             "Salad mix",
   1805             1,
   1806             "bags",
   1807             "1 bag",
   1808             0,
   1809         );
   1810         insert_order_line(
   1811             connection,
   1812             "line-3",
   1813             packed_order_id,
   1814             "Carrots",
   1815             3,
   1816             "bunches",
   1817             "3 bunches",
   1818             1,
   1819         );
   1820 
   1821         let projection = store
   1822             .load_pack_day(farm_id, &PackDayScreenQueryState::default())
   1823             .expect("pack day should load");
   1824 
   1825         assert_eq!(
   1826             projection
   1827                 .fulfillment_window
   1828                 .expect("window should exist")
   1829                 .fulfillment_window_id,
   1830             next_window_id
   1831         );
   1832         assert_eq!(
   1833             projection.totals_by_product,
   1834             vec![
   1835                 PackDayProductTotalRow {
   1836                     title: "Carrots".to_owned(),
   1837                     quantity_display: "3 bunches".to_owned(),
   1838                 },
   1839                 PackDayProductTotalRow {
   1840                     title: "Salad mix".to_owned(),
   1841                     quantity_display: "3 bags".to_owned(),
   1842                 },
   1843             ]
   1844         );
   1845         assert_eq!(projection.pack_list.len(), 3);
   1846         assert_eq!(projection.pack_list[0].title, "Carrots");
   1847         assert_eq!(
   1848             projection.pack_list[0].quantity_display,
   1849             "Taylor: 3 bunches"
   1850         );
   1851         assert_eq!(projection.pickup_roster.len(), 2);
   1852         assert_eq!(projection.pickup_roster[0].order_id, scheduled_order_id);
   1853         assert_eq!(projection.pickup_roster[1].order_id, packed_order_id);
   1854     }
   1855 
   1856     #[test]
   1857     fn pack_day_output_source_projects_canonical_records_without_screen_strings() {
   1858         let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
   1859         let connection = store.connection();
   1860         let farm_id = FarmId::new();
   1861         let fulfillment_window_id = FulfillmentWindowId::new();
   1862         let scheduled_order_id = OrderId::new();
   1863         let packed_order_id = OrderId::new();
   1864 
   1865         insert_farm(
   1866             connection,
   1867             farm_id,
   1868             "Willow farm",
   1869             "ready",
   1870             "2026-04-17T08:00:00Z",
   1871         );
   1872         let pickup_location_id = insert_pickup_location(connection, farm_id, "North barn", true);
   1873         insert_window(
   1874             connection,
   1875             fulfillment_window_id,
   1876             farm_id,
   1877             Some(pickup_location_id),
   1878             "Friday pickup",
   1879             "2099-04-18T16:00:00Z",
   1880             "2099-04-18T18:00:00Z",
   1881             "2099-04-17T18:00:00Z",
   1882         );
   1883         insert_order(
   1884             connection,
   1885             scheduled_order_id,
   1886             farm_id,
   1887             Some(fulfillment_window_id),
   1888             "R-100",
   1889             "Casey",
   1890             "scheduled",
   1891             "2026-04-17T10:00:00Z",
   1892         );
   1893         insert_order(
   1894             connection,
   1895             packed_order_id,
   1896             farm_id,
   1897             Some(fulfillment_window_id),
   1898             "R-101",
   1899             "Taylor",
   1900             "packed",
   1901             "2026-04-17T11:00:00Z",
   1902         );
   1903         insert_order(
   1904             connection,
   1905             OrderId::new(),
   1906             farm_id,
   1907             Some(fulfillment_window_id),
   1908             "R-102",
   1909             "Robin",
   1910             "completed",
   1911             "2026-04-17T12:00:00Z",
   1912         );
   1913         insert_order_line(
   1914             connection,
   1915             "line-export-1",
   1916             scheduled_order_id,
   1917             "Salad mix",
   1918             2,
   1919             "bags",
   1920             "Casey should not leak into export quantity",
   1921             0,
   1922         );
   1923         insert_order_line(
   1924             connection,
   1925             "line-export-2",
   1926             packed_order_id,
   1927             "Salad mix",
   1928             1,
   1929             "bags",
   1930             "1 bag",
   1931             0,
   1932         );
   1933         insert_order_line(
   1934             connection,
   1935             "line-export-3",
   1936             packed_order_id,
   1937             "Carrots",
   1938             3,
   1939             "bunches",
   1940             "3 bunches",
   1941             1,
   1942         );
   1943 
   1944         let source = store
   1945             .load_pack_day_output_source(farm_id, fulfillment_window_id)
   1946             .expect("output source should load")
   1947             .expect("output source should exist");
   1948 
   1949         assert_eq!(source.fulfillment_window.farm_display_name, "Willow farm");
   1950         assert_eq!(
   1951             source.fulfillment_window.pickup_location_label.as_deref(),
   1952             Some("North barn")
   1953         );
   1954         assert_eq!(source.totals_by_product.len(), 2);
   1955         assert_eq!(source.totals_by_product[0].title, "Carrots");
   1956         assert_eq!(source.totals_by_product[0].quantity.value, 3);
   1957         assert_eq!(source.totals_by_product[0].quantity.unit_label, "bunches");
   1958         assert_eq!(source.totals_by_product[1].title, "Salad mix");
   1959         assert_eq!(source.totals_by_product[1].quantity.value, 3);
   1960         assert_eq!(source.totals_by_product[1].quantity.unit_label, "bags");
   1961         assert_eq!(source.pack_list.len(), 3);
   1962         assert_eq!(source.pack_list[0].customer_display_name, "Taylor");
   1963         assert_eq!(
   1964             source.pack_list[0].order_state,
   1965             PackDayOutputOrderState::Packed
   1966         );
   1967         assert_eq!(source.pack_list[0].quantity.value, 3);
   1968         assert_eq!(source.pack_list[1].customer_display_name, "Casey");
   1969         assert_eq!(source.pack_list[1].quantity.value, 2);
   1970         assert_eq!(source.pickup_roster.len(), 2);
   1971         assert_eq!(
   1972             source
   1973                 .pickup_roster
   1974                 .iter()
   1975                 .map(|row| row.order_state)
   1976                 .collect::<Vec<_>>(),
   1977             vec![
   1978                 PackDayOutputOrderState::Scheduled,
   1979                 PackDayOutputOrderState::Packed,
   1980             ]
   1981         );
   1982         assert!(
   1983             source
   1984                 .pack_list
   1985                 .iter()
   1986                 .all(|row| row.order_number != "R-102")
   1987         );
   1988     }
   1989 
   1990     #[test]
   1991     fn orders_list_stays_aligned_with_today_needs_action_order_boundary() {
   1992         let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
   1993         let connection = store.connection();
   1994         let farm_id = FarmId::new();
   1995         let fulfillment_window_id = FulfillmentWindowId::new();
   1996 
   1997         insert_farm(
   1998             connection,
   1999             farm_id,
   2000             "Willow farm",
   2001             "ready",
   2002             "2026-04-17T08:00:00Z",
   2003         );
   2004         insert_window(
   2005             connection,
   2006             fulfillment_window_id,
   2007             farm_id,
   2008             None,
   2009             "Friday pickup",
   2010             "2099-04-18T16:00:00Z",
   2011             "2099-04-18T18:00:00Z",
   2012             "2099-04-17T18:00:00Z",
   2013         );
   2014         for index in 0..5 {
   2015             insert_order(
   2016                 connection,
   2017                 OrderId::new(),
   2018                 farm_id,
   2019                 Some(fulfillment_window_id),
   2020                 &format!("R-10{index}"),
   2021                 "Casey",
   2022                 "needs_action",
   2023                 &format!("2026-04-17T0{index}:00:00Z"),
   2024             );
   2025         }
   2026 
   2027         let today = store
   2028             .load_today_agenda(Some(farm_id))
   2029             .expect("today agenda should load");
   2030         let orders = store
   2031             .load_orders_list(
   2032                 farm_id,
   2033                 &OrdersScreenQueryState {
   2034                     filter: OrdersFilter::NeedsAction,
   2035                     fulfillment_window_id: None,
   2036                 },
   2037             )
   2038             .expect("orders list should load");
   2039 
   2040         assert_eq!(today.orders_needing_action.len(), 4);
   2041         assert_eq!(orders.rows.len(), 5);
   2042         let today_numbers = today
   2043             .orders_needing_action
   2044             .iter()
   2045             .map(|row| row.order_number.as_str())
   2046             .collect::<Vec<_>>();
   2047         let orders_numbers = orders
   2048             .rows
   2049             .iter()
   2050             .take(4)
   2051             .map(|row| row.order_number.as_str())
   2052             .collect::<Vec<_>>();
   2053         assert_eq!(today_numbers, orders_numbers);
   2054     }
   2055 
   2056     fn insert_farm(
   2057         connection: &Connection,
   2058         farm_id: FarmId,
   2059         display_name: &str,
   2060         readiness: &str,
   2061         created_at: &str,
   2062     ) {
   2063         connection
   2064             .execute(
   2065                 "insert into farms (
   2066                     id,
   2067                     display_name,
   2068                     readiness,
   2069                     timezone,
   2070                     currency_code,
   2071                     created_at,
   2072                     updated_at
   2073                  ) values (?1, ?2, ?3, 'UTC', 'USD', ?4, ?4)",
   2074                 params![farm_id.to_string(), display_name, readiness, created_at],
   2075             )
   2076             .expect("farm insert should succeed");
   2077     }
   2078 
   2079     fn insert_pickup_location(
   2080         connection: &Connection,
   2081         farm_id: FarmId,
   2082         label: &str,
   2083         is_default: bool,
   2084     ) -> PickupLocationId {
   2085         let pickup_location_id = PickupLocationId::new();
   2086 
   2087         connection
   2088             .execute(
   2089                 "insert into pickup_locations (
   2090                     id,
   2091                     farm_id,
   2092                     label,
   2093                     address_line,
   2094                     directions,
   2095                     is_default,
   2096                     created_at,
   2097                     updated_at
   2098                  ) values (?1, ?2, ?3, '14 County Road', null, ?4, '2026-04-17T08:00:00Z', '2026-04-17T08:00:00Z')",
   2099                 params![
   2100                     pickup_location_id.to_string(),
   2101                     farm_id.to_string(),
   2102                     label,
   2103                     if is_default { 1_i64 } else { 0_i64 },
   2104                 ],
   2105             )
   2106             .expect("pickup location insert should succeed");
   2107 
   2108         pickup_location_id
   2109     }
   2110 
   2111     fn insert_window(
   2112         connection: &Connection,
   2113         fulfillment_window_id: FulfillmentWindowId,
   2114         farm_id: FarmId,
   2115         pickup_location_id: Option<PickupLocationId>,
   2116         label: &str,
   2117         starts_at: &str,
   2118         ends_at: &str,
   2119         order_cutoff_at: &str,
   2120     ) {
   2121         connection
   2122             .execute(
   2123                 "insert into fulfillment_windows (
   2124                     id,
   2125                     farm_id,
   2126                     starts_at,
   2127                     ends_at,
   2128                     capacity_limit,
   2129                     created_at,
   2130                     updated_at,
   2131                     pickup_location_id,
   2132                     label,
   2133                     order_cutoff_at
   2134                  ) values (?1, ?2, ?3, ?4, null, ?3, ?3, ?5, ?6, ?7)",
   2135                 params![
   2136                     fulfillment_window_id.to_string(),
   2137                     farm_id.to_string(),
   2138                     starts_at,
   2139                     ends_at,
   2140                     pickup_location_id.map(|value| value.to_string()),
   2141                     label,
   2142                     order_cutoff_at,
   2143                 ],
   2144             )
   2145             .expect("fulfillment window insert should succeed");
   2146     }
   2147 
   2148     fn insert_order(
   2149         connection: &Connection,
   2150         order_id: OrderId,
   2151         farm_id: FarmId,
   2152         fulfillment_window_id: Option<FulfillmentWindowId>,
   2153         order_number: &str,
   2154         customer_display_name: &str,
   2155         status: &str,
   2156         updated_at: &str,
   2157     ) {
   2158         connection
   2159             .execute(
   2160                 "insert into orders (
   2161                     id,
   2162                     farm_id,
   2163                     fulfillment_window_id,
   2164                     order_number,
   2165                     customer_display_name,
   2166                     status,
   2167                     updated_at
   2168                  ) values (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
   2169                 params![
   2170                     order_id.to_string(),
   2171                     farm_id.to_string(),
   2172                     fulfillment_window_id.map(|id| id.to_string()),
   2173                     order_number,
   2174                     customer_display_name,
   2175                     status,
   2176                     updated_at,
   2177                 ],
   2178             )
   2179             .expect("order insert should succeed");
   2180     }
   2181 
   2182     fn set_order_workflow_revision(
   2183         connection: &Connection,
   2184         order_id: OrderId,
   2185         workflow_revision: &str,
   2186     ) {
   2187         connection
   2188             .execute(
   2189                 "update orders set workflow_revision = ?1 where id = ?2",
   2190                 params![workflow_revision, order_id.to_string()],
   2191             )
   2192             .expect("order workflow revision update should succeed");
   2193     }
   2194 
   2195     fn corrupt_order_workflow_revision(
   2196         connection: &Connection,
   2197         order_id: OrderId,
   2198         workflow_revision: &str,
   2199     ) {
   2200         connection
   2201             .execute_batch("pragma ignore_check_constraints = on")
   2202             .expect("check constraints should disable");
   2203         set_order_workflow_revision(connection, order_id, workflow_revision);
   2204         connection
   2205             .execute_batch("pragma ignore_check_constraints = off")
   2206             .expect("check constraints should re-enable");
   2207     }
   2208 
   2209     fn set_order_workflow_display_projection(
   2210         connection: &Connection,
   2211         order_id: OrderId,
   2212         agreement: &str,
   2213         inventory: &str,
   2214         provenance_source: &str,
   2215         provenance_last_event_id: Option<&str>,
   2216     ) {
   2217         connection
   2218             .execute(
   2219                 "update orders
   2220                  set workflow_agreement = ?1,
   2221                      workflow_inventory = ?2,
   2222                      workflow_provenance_source = ?3,
   2223                      workflow_provenance_last_event_id = ?4
   2224                  where id = ?5",
   2225                 params![
   2226                     agreement,
   2227                     inventory,
   2228                     provenance_source,
   2229                     provenance_last_event_id,
   2230                     order_id.to_string(),
   2231                 ],
   2232             )
   2233             .expect("order workflow display projection update should succeed");
   2234     }
   2235 
   2236     fn corrupt_order_workflow_display_projection(
   2237         connection: &Connection,
   2238         order_id: OrderId,
   2239         column: &str,
   2240         value: &str,
   2241     ) {
   2242         connection
   2243             .execute_batch("pragma ignore_check_constraints = on")
   2244             .expect("check constraints should disable");
   2245         let statement = match column {
   2246             "workflow_agreement" => "update orders set workflow_agreement = ?1 where id = ?2",
   2247             "workflow_inventory" => "update orders set workflow_inventory = ?1 where id = ?2",
   2248             "workflow_provenance_source" => {
   2249                 "update orders set workflow_provenance_source = ?1 where id = ?2"
   2250             }
   2251             _ => panic!("unsupported workflow display projection column {column}"),
   2252         };
   2253         connection
   2254             .execute(statement, params![value, order_id.to_string()])
   2255             .expect("order workflow display projection corruption should succeed");
   2256         connection
   2257             .execute_batch("pragma ignore_check_constraints = off")
   2258             .expect("check constraints should re-enable");
   2259     }
   2260 
   2261     fn assert_decode_enum(error: AppSqliteError, expected_field: &str, expected_value: &str) {
   2262         match error {
   2263             AppSqliteError::DecodeEnum { field, value } => {
   2264                 assert_eq!(field, expected_field);
   2265                 assert_eq!(value, expected_value);
   2266             }
   2267             other => panic!("expected DecodeEnum error, got {other:?}"),
   2268         }
   2269     }
   2270 
   2271     fn insert_order_line(
   2272         connection: &Connection,
   2273         line_id: &str,
   2274         order_id: OrderId,
   2275         title: &str,
   2276         quantity_value: u32,
   2277         quantity_unit_label: &str,
   2278         quantity_display: &str,
   2279         sort_index: i64,
   2280     ) {
   2281         connection
   2282             .execute(
   2283                 "insert into order_lines (
   2284                     id,
   2285                     order_id,
   2286                     title,
   2287                     quantity_value,
   2288                     quantity_unit_label,
   2289                     quantity_display,
   2290                     unit_price_minor_units,
   2291                     price_currency,
   2292                     sort_index
   2293                  ) values (?1, ?2, ?3, ?4, ?5, ?6, 650, 'USD', ?7)",
   2294                 params![
   2295                     line_id,
   2296                     order_id.to_string(),
   2297                     title,
   2298                     quantity_value,
   2299                     quantity_unit_label,
   2300                     quantity_display,
   2301                     sort_index,
   2302                 ],
   2303             )
   2304             .expect("order line insert should succeed");
   2305     }
   2306 }