commit cc3f3a6922fa5c1899e7af7aa346be9925f546da
parent 3d36437c70bd86cfcd4ae87a91c0296d8448f825
Author: triesap <tyson@radroots.org>
Date: Fri, 17 Apr 2026 20:55:36 +0000
sqlite: add the today agenda query path
Diffstat:
6 files changed, 757 insertions(+), 1 deletion(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -4649,6 +4649,7 @@ dependencies = [
name = "radroots_app_sqlite"
version = "0.1.0"
dependencies = [
+ "radroots_app_models",
"rusqlite",
"thiserror 2.0.18",
]
diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs
@@ -119,12 +119,15 @@ impl DesktopAppRuntimeState {
let roots = AppRuntimeRoots::current_desktop()?;
let database_path = roots.data.join(APP_DATABASE_FILE_NAME);
let sqlite_store = AppSqliteStore::open(DatabaseTarget::Path(database_path.clone()))?;
- let state_store = AppStateStore::load(InMemoryAppStateRepository::default())?;
+ let mut state_store = AppStateStore::load(InMemoryAppStateRepository::default())?;
+ let today_projection = sqlite_store.load_today_agenda(None)?;
let sync_projection = AppSyncProjection {
checkpoint: SyncCheckpointStatus::never_synced(),
conflict_status: SyncConflictStatus::clear(),
..AppSyncProjection::default()
};
+ let _ =
+ state_store.apply_in_memory(AppStateCommand::replace_today_agenda(today_projection));
Ok(Self {
data_dir: Some(roots.data),
diff --git a/crates/shared/sqlite/Cargo.toml b/crates/shared/sqlite/Cargo.toml
@@ -8,6 +8,7 @@ license.workspace = true
publish = false
[dependencies]
+radroots_app_models.workspace = true
rusqlite.workspace = true
thiserror.workspace = true
diff --git a/crates/shared/sqlite/src/error.rs b/crates/shared/sqlite/src/error.rs
@@ -65,4 +65,14 @@ pub enum AppSqliteError {
#[source]
source: rusqlite::Error,
},
+ #[error("failed to execute sqlite query for {operation}")]
+ Query {
+ operation: &'static str,
+ #[source]
+ source: rusqlite::Error,
+ },
+ #[error("invalid sqlite id in `{field}`: `{value}`")]
+ DecodeId { field: &'static str, value: String },
+ #[error("invalid sqlite enum value in `{field}`: `{value}`")]
+ DecodeEnum { field: &'static str, value: String },
}
diff --git a/crates/shared/sqlite/src/lib.rs b/crates/shared/sqlite/src/lib.rs
@@ -2,13 +2,18 @@
mod error;
mod migrations;
+mod today;
use std::{fs, path::PathBuf, time::Duration};
+use radroots_app_models::{FarmId, TodayAgendaProjection};
use rusqlite::Connection;
pub use error::AppSqliteError;
pub use migrations::latest_schema_version;
+pub use today::{
+ AppTodayAgendaRepository, TODAY_AGENDA_LIST_LIMIT, TODAY_AGENDA_LOW_STOCK_THRESHOLD,
+};
const SQLITE_BUSY_TIMEOUT_MS: u64 = 5_000;
@@ -41,6 +46,17 @@ impl AppSqliteStore {
pub fn schema_version(&self) -> Result<u32, AppSqliteError> {
schema_version(&self.connection)
}
+
+ pub fn today_agenda_repository(&self) -> AppTodayAgendaRepository<'_> {
+ AppTodayAgendaRepository::new(&self.connection)
+ }
+
+ pub fn load_today_agenda(
+ &self,
+ farm_id: Option<FarmId>,
+ ) -> Result<TodayAgendaProjection, AppSqliteError> {
+ self.today_agenda_repository().load(farm_id)
+ }
}
fn open_connection(target: &DatabaseTarget) -> Result<Connection, AppSqliteError> {
diff --git a/crates/shared/sqlite/src/today.rs b/crates/shared/sqlite/src/today.rs
@@ -0,0 +1,725 @@
+use radroots_app_models::{
+ FarmId, FarmReadiness, FarmSummary, FulfillmentWindowSummary, OrderListRow, OrderStatus,
+ ProductListRow, ProductStatus, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind,
+ TodaySummary,
+};
+use rusqlite::{Connection, OptionalExtension, Params, params};
+
+use crate::AppSqliteError;
+
+pub const TODAY_AGENDA_LIST_LIMIT: i64 = 4;
+pub const TODAY_AGENDA_LOW_STOCK_THRESHOLD: u32 = 3;
+
+pub struct AppTodayAgendaRepository<'a> {
+ connection: &'a Connection,
+}
+
+impl<'a> AppTodayAgendaRepository<'a> {
+ pub const fn new(connection: &'a Connection) -> Self {
+ Self { connection }
+ }
+
+ pub fn load(&self, farm_id: Option<FarmId>) -> Result<TodayAgendaProjection, AppSqliteError> {
+ let Some(farm) = self.load_farm_summary(farm_id)? else {
+ return Ok(TodayAgendaProjection::default());
+ };
+
+ Ok(TodayAgendaProjection {
+ farm: Some(farm.clone()),
+ summary: Some(self.load_today_summary(farm.farm_id)?),
+ orders_needing_action: self.load_orders_needing_action(farm.farm_id)?,
+ low_stock_products: self.load_low_stock_products(farm.farm_id)?,
+ draft_products: self.load_draft_products(farm.farm_id)?,
+ next_fulfillment_window: self.load_next_fulfillment_window(farm.farm_id)?,
+ setup_checklist: self.load_setup_checklist(&farm)?,
+ })
+ }
+
+ fn load_farm_summary(
+ &self,
+ farm_id: Option<FarmId>,
+ ) -> Result<Option<FarmSummary>, AppSqliteError> {
+ let farm_row = if let Some(farm_id) = farm_id {
+ self.connection
+ .query_row(
+ "select id, display_name, readiness from farms where id = ?1 limit 1",
+ params![farm_id.to_string()],
+ |row| {
+ Ok((
+ row.get::<_, String>(0)?,
+ row.get::<_, String>(1)?,
+ row.get::<_, String>(2)?,
+ ))
+ },
+ )
+ .optional()
+ .map_err(|source| AppSqliteError::Query {
+ operation: "load today farm summary",
+ source,
+ })?
+ } else {
+ self.connection
+ .query_row(
+ "select id, display_name, readiness from farms order by created_at asc, id asc limit 1",
+ [],
+ |row| {
+ Ok((
+ row.get::<_, String>(0)?,
+ row.get::<_, String>(1)?,
+ row.get::<_, String>(2)?,
+ ))
+ },
+ )
+ .optional()
+ .map_err(|source| AppSqliteError::Query {
+ operation: "load today farm summary",
+ source,
+ })?
+ };
+
+ farm_row
+ .map(|(farm_id, display_name, readiness)| {
+ Ok(FarmSummary {
+ farm_id: parse_typed_id("farms.id", farm_id)?,
+ display_name,
+ readiness: parse_farm_readiness("farms.readiness", readiness)?,
+ })
+ })
+ .transpose()
+ }
+
+ fn load_today_summary(&self, farm_id: FarmId) -> Result<TodaySummary, AppSqliteError> {
+ Ok(TodaySummary {
+ farm_id,
+ orders_needing_action: self.count_u32(
+ "count today orders needing action",
+ "select count(*) from orders where farm_id = ?1 and status = 'needs_action'",
+ params![farm_id.to_string()],
+ )?,
+ low_stock_products: self.count_u32(
+ "count today low-stock products",
+ "select count(*) from products where farm_id = ?1 and status = 'published' and stock_count <= ?2",
+ params![farm_id.to_string(), TODAY_AGENDA_LOW_STOCK_THRESHOLD],
+ )?,
+ draft_products: self.count_u32(
+ "count today draft products",
+ "select count(*) from products where farm_id = ?1 and status = 'draft'",
+ params![farm_id.to_string()],
+ )?,
+ })
+ }
+
+ fn load_orders_needing_action(
+ &self,
+ farm_id: FarmId,
+ ) -> Result<Vec<OrderListRow>, AppSqliteError> {
+ let mut statement = self
+ .connection
+ .prepare(
+ "select id, fulfillment_window_id, order_number, customer_display_name \
+ from orders \
+ where farm_id = ?1 and status = 'needs_action' \
+ order by updated_at desc, id desc \
+ limit ?2",
+ )
+ .map_err(|source| AppSqliteError::Query {
+ operation: "prepare today orders needing action",
+ source,
+ })?;
+ let rows = statement
+ .query_map(
+ params![farm_id.to_string(), TODAY_AGENDA_LIST_LIMIT],
+ |row| {
+ Ok((
+ row.get::<_, String>(0)?,
+ row.get::<_, Option<String>>(1)?,
+ row.get::<_, String>(2)?,
+ row.get::<_, String>(3)?,
+ ))
+ },
+ )
+ .map_err(|source| AppSqliteError::Query {
+ operation: "query today orders needing action",
+ source,
+ })?;
+ let mut orders = Vec::new();
+
+ for row in rows {
+ let (order_id, fulfillment_window_id, order_number, customer_display_name) = row
+ .map_err(|source| AppSqliteError::Query {
+ operation: "read today orders needing action",
+ source,
+ })?;
+
+ orders.push(OrderListRow {
+ order_id: parse_typed_id("orders.id", order_id)?,
+ farm_id,
+ fulfillment_window_id: parse_optional_typed_id(
+ "orders.fulfillment_window_id",
+ fulfillment_window_id,
+ )?,
+ order_number,
+ customer_display_name,
+ status: OrderStatus::NeedsAction,
+ });
+ }
+
+ Ok(orders)
+ }
+
+ fn load_low_stock_products(
+ &self,
+ farm_id: FarmId,
+ ) -> Result<Vec<ProductListRow>, AppSqliteError> {
+ let mut statement = self
+ .connection
+ .prepare(
+ "select id, title, stock_count \
+ from products \
+ where farm_id = ?1 and status = 'published' and stock_count <= ?2 \
+ order by stock_count asc, updated_at desc, id desc \
+ limit ?3",
+ )
+ .map_err(|source| AppSqliteError::Query {
+ operation: "prepare today low-stock products",
+ source,
+ })?;
+ let rows = statement
+ .query_map(
+ params![
+ farm_id.to_string(),
+ TODAY_AGENDA_LOW_STOCK_THRESHOLD,
+ TODAY_AGENDA_LIST_LIMIT
+ ],
+ |row| {
+ Ok((
+ row.get::<_, String>(0)?,
+ row.get::<_, String>(1)?,
+ row.get::<_, u32>(2)?,
+ ))
+ },
+ )
+ .map_err(|source| AppSqliteError::Query {
+ operation: "query today low-stock products",
+ source,
+ })?;
+ let mut products = Vec::new();
+
+ for row in rows {
+ let (product_id, title, stock_count) = row.map_err(|source| AppSqliteError::Query {
+ operation: "read today low-stock products",
+ source,
+ })?;
+
+ products.push(ProductListRow {
+ product_id: parse_typed_id("products.id", product_id)?,
+ farm_id,
+ title,
+ status: ProductStatus::Published,
+ stock_count,
+ });
+ }
+
+ Ok(products)
+ }
+
+ fn load_draft_products(&self, farm_id: FarmId) -> Result<Vec<ProductListRow>, AppSqliteError> {
+ let mut statement = self
+ .connection
+ .prepare(
+ "select id, title, stock_count \
+ from products \
+ where farm_id = ?1 and status = 'draft' \
+ order by updated_at desc, id desc \
+ limit ?2",
+ )
+ .map_err(|source| AppSqliteError::Query {
+ operation: "prepare today draft products",
+ source,
+ })?;
+ let rows = statement
+ .query_map(
+ params![farm_id.to_string(), TODAY_AGENDA_LIST_LIMIT],
+ |row| {
+ Ok((
+ row.get::<_, String>(0)?,
+ row.get::<_, String>(1)?,
+ row.get::<_, u32>(2)?,
+ ))
+ },
+ )
+ .map_err(|source| AppSqliteError::Query {
+ operation: "query today draft products",
+ source,
+ })?;
+ let mut products = Vec::new();
+
+ for row in rows {
+ let (product_id, title, stock_count) = row.map_err(|source| AppSqliteError::Query {
+ operation: "read today draft products",
+ source,
+ })?;
+
+ products.push(ProductListRow {
+ product_id: parse_typed_id("products.id", product_id)?,
+ farm_id,
+ title,
+ status: ProductStatus::Draft,
+ stock_count,
+ });
+ }
+
+ Ok(products)
+ }
+
+ fn load_next_fulfillment_window(
+ &self,
+ farm_id: FarmId,
+ ) -> Result<Option<FulfillmentWindowSummary>, AppSqliteError> {
+ self.connection
+ .query_row(
+ "select id, starts_at, ends_at \
+ from fulfillment_windows \
+ where farm_id = ?1 and starts_at >= strftime('%Y-%m-%dT%H:%M:%SZ', 'now') \
+ order by starts_at asc, id asc \
+ limit 1",
+ params![farm_id.to_string()],
+ |row| {
+ Ok((
+ row.get::<_, String>(0)?,
+ row.get::<_, String>(1)?,
+ row.get::<_, String>(2)?,
+ ))
+ },
+ )
+ .optional()
+ .map_err(|source| AppSqliteError::Query {
+ operation: "load today next fulfillment window",
+ source,
+ })?
+ .map(|(fulfillment_window_id, starts_at, ends_at)| {
+ Ok(FulfillmentWindowSummary {
+ fulfillment_window_id: parse_typed_id(
+ "fulfillment_windows.id",
+ fulfillment_window_id,
+ )?,
+ farm_id,
+ starts_at,
+ ends_at,
+ })
+ })
+ .transpose()
+ }
+
+ fn load_setup_checklist(
+ &self,
+ farm: &FarmSummary,
+ ) -> Result<Vec<TodaySetupTask>, AppSqliteError> {
+ if farm.readiness != FarmReadiness::Incomplete {
+ return Ok(Vec::new());
+ }
+
+ Ok(vec![
+ TodaySetupTask {
+ kind: TodaySetupTaskKind::AddFulfillmentWindow,
+ is_complete: self.exists(
+ "check today fulfillment window setup",
+ "select exists(select 1 from fulfillment_windows where farm_id = ?1)",
+ params![farm.farm_id.to_string()],
+ )?,
+ },
+ TodaySetupTask {
+ kind: TodaySetupTaskKind::PublishProduct,
+ is_complete: self.exists(
+ "check today published product setup",
+ "select exists(select 1 from products where farm_id = ?1 and status = 'published')",
+ params![farm.farm_id.to_string()],
+ )?,
+ },
+ ])
+ }
+
+ fn count_u32<P: Params>(
+ &self,
+ operation: &'static str,
+ sql: &'static str,
+ params: P,
+ ) -> Result<u32, AppSqliteError> {
+ self.connection
+ .query_row(sql, params, |row| row.get::<_, u32>(0))
+ .map_err(|source| AppSqliteError::Query { operation, source })
+ }
+
+ fn exists<P: Params>(
+ &self,
+ operation: &'static str,
+ sql: &'static str,
+ params: P,
+ ) -> Result<bool, AppSqliteError> {
+ self.connection
+ .query_row(sql, params, |row| row.get::<_, i64>(0))
+ .map(|value| value == 1)
+ .map_err(|source| AppSqliteError::Query { operation, source })
+ }
+}
+
+fn parse_typed_id<T>(field: &'static str, value: String) -> Result<T, AppSqliteError>
+where
+ T: std::str::FromStr,
+{
+ value
+ .parse()
+ .map_err(|_| AppSqliteError::DecodeId { field, value })
+}
+
+fn parse_optional_typed_id<T>(
+ field: &'static str,
+ value: Option<String>,
+) -> Result<Option<T>, AppSqliteError>
+where
+ T: std::str::FromStr,
+{
+ value.map(|value| parse_typed_id(field, value)).transpose()
+}
+
+fn parse_farm_readiness(
+ field: &'static str,
+ value: String,
+) -> Result<FarmReadiness, AppSqliteError> {
+ match value.as_str() {
+ "incomplete" => Ok(FarmReadiness::Incomplete),
+ "ready" => Ok(FarmReadiness::Ready),
+ _ => Err(AppSqliteError::DecodeEnum { field, value }),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use radroots_app_models::{FarmId, FulfillmentWindowId, ProductId, TodaySetupTaskKind};
+ use rusqlite::{Connection, params};
+
+ use crate::{AppSqliteStore, DatabaseTarget};
+
+ use super::{TODAY_AGENDA_LIST_LIMIT, TODAY_AGENDA_LOW_STOCK_THRESHOLD};
+
+ #[test]
+ fn today_agenda_returns_default_when_no_farm_exists() {
+ let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
+
+ let projection = store
+ .load_today_agenda(None)
+ .expect("empty today agenda should load");
+
+ assert_eq!(
+ projection,
+ radroots_app_models::TodayAgendaProjection::default()
+ );
+ }
+
+ #[test]
+ fn today_agenda_loads_truthful_projection_for_selected_farm() {
+ let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
+ let connection = store.connection();
+ let farm_id = FarmId::new();
+ let other_farm_id = FarmId::new();
+ let earliest_window_id = FulfillmentWindowId::new();
+ let later_window_id = FulfillmentWindowId::new();
+
+ insert_farm(
+ connection,
+ farm_id,
+ "Willow Farm",
+ "ready",
+ "2026-04-17T08:00:00Z",
+ );
+ insert_farm(
+ connection,
+ other_farm_id,
+ "Other Farm",
+ "ready",
+ "2026-04-18T08:00:00Z",
+ );
+ insert_window(
+ connection,
+ earliest_window_id,
+ farm_id,
+ "2099-04-18T16:00:00Z",
+ "2099-04-18T18:00:00Z",
+ );
+ insert_window(
+ connection,
+ later_window_id,
+ farm_id,
+ "2099-04-19T16:00:00Z",
+ "2099-04-19T18:00:00Z",
+ );
+ insert_window(
+ connection,
+ FulfillmentWindowId::new(),
+ other_farm_id,
+ "2099-04-17T10:00:00Z",
+ "2099-04-17T12:00:00Z",
+ );
+
+ for index in 0..5 {
+ insert_order(
+ connection,
+ farm_id,
+ Some(earliest_window_id),
+ &format!("R-10{index}"),
+ "Casey",
+ "needs_action",
+ &format!("2026-04-17T0{index}:00:00Z"),
+ );
+ }
+ insert_order(
+ connection,
+ farm_id,
+ Some(earliest_window_id),
+ "R-200",
+ "Taylor",
+ "scheduled",
+ "2026-04-17T11:00:00Z",
+ );
+ insert_order(
+ connection,
+ other_farm_id,
+ None,
+ "R-999",
+ "Other",
+ "needs_action",
+ "2026-04-17T12:00:00Z",
+ );
+
+ insert_product(
+ connection,
+ farm_id,
+ "Carrots",
+ "published",
+ 1,
+ "2026-04-17T10:00:00Z",
+ );
+ insert_product(
+ connection,
+ farm_id,
+ "Greens",
+ "published",
+ TODAY_AGENDA_LOW_STOCK_THRESHOLD,
+ "2026-04-17T09:00:00Z",
+ );
+ insert_product(
+ connection,
+ farm_id,
+ "Tomatoes",
+ "published",
+ TODAY_AGENDA_LOW_STOCK_THRESHOLD + 1,
+ "2026-04-17T08:00:00Z",
+ );
+ for index in 0..5 {
+ insert_product(
+ connection,
+ farm_id,
+ &format!("Draft {index}"),
+ "draft",
+ 0,
+ &format!("2026-04-17T1{index}:00:00Z"),
+ );
+ }
+ insert_product(
+ connection,
+ other_farm_id,
+ "Other Draft",
+ "draft",
+ 0,
+ "2026-04-17T14:00:00Z",
+ );
+
+ let projection = store
+ .load_today_agenda(Some(farm_id))
+ .expect("today agenda should load");
+ let summary = projection.summary.expect("summary should exist");
+ let farm = projection.farm.expect("farm should exist");
+ let next_window = projection
+ .next_fulfillment_window
+ .expect("next window should exist");
+
+ assert_eq!(farm.farm_id, farm_id);
+ assert_eq!(farm.display_name, "Willow Farm");
+ assert_eq!(summary.orders_needing_action, 5);
+ assert_eq!(summary.low_stock_products, 2);
+ assert_eq!(summary.draft_products, 5);
+ assert_eq!(
+ projection.orders_needing_action.len() as i64,
+ TODAY_AGENDA_LIST_LIMIT
+ );
+ assert_eq!(projection.orders_needing_action[0].order_number, "R-104");
+ assert_eq!(projection.low_stock_products.len(), 2);
+ assert_eq!(projection.low_stock_products[0].title, "Carrots");
+ assert_eq!(projection.low_stock_products[1].title, "Greens");
+ assert_eq!(
+ projection.draft_products.len() as i64,
+ TODAY_AGENDA_LIST_LIMIT
+ );
+ assert_eq!(projection.draft_products[0].title, "Draft 4");
+ assert_eq!(next_window.fulfillment_window_id, earliest_window_id);
+ assert_eq!(next_window.starts_at, "2099-04-18T16:00:00Z");
+ assert!(projection.setup_checklist.is_empty());
+ }
+
+ #[test]
+ fn today_agenda_uses_primary_farm_and_builds_setup_checklist_for_incomplete_farm() {
+ let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
+ let connection = store.connection();
+ let primary_farm_id = FarmId::new();
+ let secondary_farm_id = FarmId::new();
+
+ insert_farm(
+ connection,
+ primary_farm_id,
+ "First Farm",
+ "incomplete",
+ "2026-04-17T08:00:00Z",
+ );
+ insert_farm(
+ connection,
+ secondary_farm_id,
+ "Second Farm",
+ "ready",
+ "2026-04-18T08:00:00Z",
+ );
+ insert_product(
+ connection,
+ primary_farm_id,
+ "Unpublished Lettuce",
+ "draft",
+ 0,
+ "2026-04-17T09:00:00Z",
+ );
+ insert_product(
+ connection,
+ secondary_farm_id,
+ "Published Beets",
+ "published",
+ 5,
+ "2026-04-17T10:00:00Z",
+ );
+ insert_window(
+ connection,
+ FulfillmentWindowId::new(),
+ secondary_farm_id,
+ "2099-04-20T16:00:00Z",
+ "2099-04-20T18:00:00Z",
+ );
+
+ let projection = store
+ .load_today_agenda(None)
+ .expect("default farm today agenda should load");
+ let farm = projection.farm.expect("farm should exist");
+
+ assert_eq!(farm.farm_id, primary_farm_id);
+ assert_eq!(projection.summary.expect("summary").draft_products, 1);
+ assert_eq!(projection.setup_checklist.len(), 2);
+ assert_eq!(
+ projection.setup_checklist[0].kind,
+ TodaySetupTaskKind::AddFulfillmentWindow
+ );
+ assert!(!projection.setup_checklist[0].is_complete);
+ assert_eq!(
+ projection.setup_checklist[1].kind,
+ TodaySetupTaskKind::PublishProduct
+ );
+ assert!(!projection.setup_checklist[1].is_complete);
+ assert!(projection.next_fulfillment_window.is_none());
+ }
+
+ fn insert_farm(
+ connection: &Connection,
+ farm_id: FarmId,
+ display_name: &str,
+ readiness: &str,
+ created_at: &str,
+ ) {
+ connection
+ .execute(
+ "insert into farms (id, display_name, readiness, created_at, updated_at) \
+ values (?1, ?2, ?3, ?4, ?4)",
+ params![farm_id.to_string(), display_name, readiness, created_at],
+ )
+ .expect("farm insert should succeed");
+ }
+
+ fn insert_window(
+ connection: &Connection,
+ fulfillment_window_id: FulfillmentWindowId,
+ farm_id: FarmId,
+ starts_at: &str,
+ ends_at: &str,
+ ) {
+ connection
+ .execute(
+ "insert into fulfillment_windows (id, farm_id, starts_at, ends_at, capacity_limit, created_at, updated_at) \
+ values (?1, ?2, ?3, ?4, null, ?3, ?3)",
+ params![
+ fulfillment_window_id.to_string(),
+ farm_id.to_string(),
+ starts_at,
+ ends_at
+ ],
+ )
+ .expect("fulfillment window insert should succeed");
+ }
+
+ fn insert_product(
+ connection: &Connection,
+ farm_id: FarmId,
+ title: &str,
+ status: &str,
+ stock_count: u32,
+ updated_at: &str,
+ ) -> ProductId {
+ let product_id = ProductId::new();
+
+ connection
+ .execute(
+ "insert into products (id, farm_id, title, status, stock_count, updated_at) \
+ values (?1, ?2, ?3, ?4, ?5, ?6)",
+ params![
+ product_id.to_string(),
+ farm_id.to_string(),
+ title,
+ status,
+ stock_count,
+ updated_at
+ ],
+ )
+ .expect("product insert should succeed");
+
+ product_id
+ }
+
+ fn insert_order(
+ connection: &Connection,
+ farm_id: FarmId,
+ fulfillment_window_id: Option<FulfillmentWindowId>,
+ order_number: &str,
+ customer_display_name: &str,
+ status: &str,
+ updated_at: &str,
+ ) {
+ connection
+ .execute(
+ "insert into orders (id, farm_id, fulfillment_window_id, order_number, customer_display_name, status, updated_at) \
+ values (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
+ params![
+ radroots_app_models::OrderId::new().to_string(),
+ farm_id.to_string(),
+ fulfillment_window_id.map(|id| id.to_string()),
+ order_number,
+ customer_display_name,
+ status,
+ updated_at
+ ],
+ )
+ .expect("order insert should succeed");
+ }
+}