app

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

commit 7c7a999058f779e63889d927d651e4e1a9cea5c7
parent 36bbbfe808fc7b9043daa27e98ec6f4a548c13ba
Author: triesap <tyson@radroots.org>
Date:   Wed,  3 Jun 2026 18:06:14 -0700

orders: align farmer workflow projection

Diffstat:
Mcrates/desktop/src/window.rs | 37+++++++++++++++++++++++++++++--------
Mcrates/store/src/repo/buyer.rs | 143+++++++++++++-------------------------------------------------------------------
Mcrates/store/src/repo/mod.rs | 1+
Mcrates/store/src/repo/orders.rs | 322+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Acrates/store/src/repo/workflow.rs | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 464 insertions(+), 150 deletions(-)

diff --git a/crates/desktop/src/window.rs b/crates/desktop/src/window.rs @@ -10311,22 +10311,32 @@ fn orders_table_header() -> impl IntoElement { )) .child(products_table_header_column( AppTextKey::OrdersColumnStatus, - Some(160.0), + Some(144.0), + false, + )) + .child(products_table_header_column( + AppTextKey::OrdersDetailTotalLabel, + Some(112.0), + false, + )) + .child(products_table_header_column( + AppTextKey::TradeWorkflowAxisPayment, + Some(128.0), false, )) .child(products_table_header_column( AppTextKey::OrdersColumnWindow, - Some(196.0), + Some(160.0), false, )) .child(products_table_header_column( AppTextKey::OrdersColumnPickup, - Some(196.0), + Some(160.0), false, )) .child(products_table_header_column( AppTextKey::OrdersColumnAction, - Some(132.0), + Some(120.0), false, )) } @@ -10344,7 +10354,7 @@ fn orders_table_row( .child(order) .child( div() - .w(px(160.0)) + .w(px(144.0)) .flex() .items_start() .gap(px(6.0)) @@ -10353,7 +10363,18 @@ fn orders_table_row( ) .child( div() - .w(px(196.0)) + .w(px(112.0)) + .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px)) + .line_height(relative(1.2)) + .text_color(rgb(APP_UI_THEME.foundation.text.primary)) + .child(trade_economics_total_text(&row.workflow.economics)), + ) + .child(div().w(px(128.0)).child(trade_workflow_value_badge( + trade_payment_display_status_key(row.workflow.payment), + ))) + .child( + div() + .w(px(160.0)) .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px)) .line_height(relative(1.2)) .text_color(rgb(APP_UI_THEME.foundation.text.primary)) @@ -10361,13 +10382,13 @@ fn orders_table_row( ) .child( div() - .w(px(196.0)) + .w(px(160.0)) .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px)) .line_height(relative(1.2)) .text_color(rgb(APP_UI_THEME.foundation.text.primary)) .child(order_optional_text(row.pickup_location_label.as_deref())), ) - .child(div().w(px(132.0)).flex().justify_end().child(action)) + .child(div().w(px(120.0)).flex().justify_end().child(action)) } fn orders_table_action( diff --git a/crates/store/src/repo/buyer.rs b/crates/store/src/repo/buyer.rs @@ -8,10 +8,7 @@ use radroots_app_view::{ BuyerProductDetailProjection, FarmId, FarmOrderMethod, FulfillmentWindowId, OrderDetailItemRow, OrderId, OrderStatus, ProductAvailabilityState, ProductAvailabilitySummary, ProductId, ProductPricePresentation, ProductStatus, ProductStockState, ProductStockSummary, - RepeatDemandEligibility, RepeatDemandHandoffProjection, TradeAgreementStatus, - TradeEconomicsProjection, TradeFulfillmentStatus, TradeInventoryStatus, - TradePaymentDisplayStatus, TradeProvenanceProjection, TradeRevisionStatus, - TradeWorkflowProjection, TradeWorkflowSource, + RepeatDemandEligibility, RepeatDemandHandoffProjection, }; use rusqlite::{Connection, OptionalExtension, params}; use serde_json::Value; @@ -19,6 +16,7 @@ use serde_json::Value; use super::{ order_detail::{order_detail_economics, order_detail_item_row}, parse_trade_revision_status, + workflow::{StoredTradeWorkflowSnapshot, trade_workflow_projection_from_storage}, }; use crate::AppSqliteError; @@ -824,17 +822,17 @@ impl<'a> AppBuyerRepository<'a> { parse_trade_revision_status("orders.workflow_revision", workflow_revision)?; let items = self.load_order_detail_items(order_id.to_string())?; let economics = order_detail_economics(&items)?; - let workflow = trade_workflow_projection_from_storage( + let workflow = trade_workflow_projection_from_storage(StoredTradeWorkflowSnapshot { order_id, revision, economics, - workflow_agreement, - workflow_fulfillment, - workflow_inventory, - workflow_payment, - workflow_provenance_source, - workflow_provenance_last_event_id, - )?; + agreement: workflow_agreement, + fulfillment: workflow_fulfillment, + inventory: workflow_inventory, + payment: workflow_payment, + provenance_source: workflow_provenance_source, + provenance_last_event_id: workflow_provenance_last_event_id, + })?; orders.push(BuyerOrdersListRow { order_id, @@ -949,17 +947,18 @@ impl<'a> AppBuyerRepository<'a> { parse_trade_revision_status("orders.workflow_revision", workflow_revision)?; let items = self.load_order_detail_items(order_id.to_string())?; let economics = order_detail_economics(&items)?; - let workflow = trade_workflow_projection_from_storage( - order_id, - revision, - economics.clone(), - workflow_agreement, - workflow_fulfillment, - workflow_inventory, - workflow_payment, - workflow_provenance_source, - workflow_provenance_last_event_id, - )?; + let workflow = + trade_workflow_projection_from_storage(StoredTradeWorkflowSnapshot { + order_id, + revision, + economics: economics.clone(), + agreement: workflow_agreement, + fulfillment: workflow_fulfillment, + inventory: workflow_inventory, + payment: workflow_payment, + provenance_source: workflow_provenance_source, + provenance_last_event_id: workflow_provenance_last_event_id, + })?; let payment = workflow.payment; Ok(BuyerOrderDetailProjection { order_id, @@ -2535,104 +2534,6 @@ fn refresh_buyer_cart_summary(cart: &mut BuyerCartProjection) -> Result<(), AppS Ok(()) } -fn trade_workflow_projection_from_storage( - order_id: OrderId, - revision: TradeRevisionStatus, - economics: TradeEconomicsProjection, - agreement: String, - fulfillment: Option<String>, - inventory: String, - payment: String, - provenance_source: String, - provenance_last_event_id: Option<String>, -) -> Result<TradeWorkflowProjection, AppSqliteError> { - Ok(TradeWorkflowProjection { - order_id, - agreement: parse_trade_agreement_status("orders.workflow_agreement", agreement)?, - revision, - fulfillment: fulfillment - .map(|value| parse_trade_fulfillment_status("orders.workflow_fulfillment", value)) - .transpose()?, - economics, - inventory: parse_trade_inventory_status("orders.workflow_inventory", inventory)?, - payment: parse_trade_payment_display_status("orders.workflow_payment", payment)?, - provenance: TradeProvenanceProjection::from_primary_source(parse_trade_workflow_source( - "orders.workflow_provenance_source", - provenance_source, - )?) - .with_last_event_id(provenance_last_event_id), - }) -} - -fn parse_trade_agreement_status( - field: &'static str, - value: String, -) -> Result<TradeAgreementStatus, AppSqliteError> { - match value.as_str() { - "ordered" => Ok(TradeAgreementStatus::Ordered), - "confirmed" => Ok(TradeAgreementStatus::Confirmed), - "declined" => Ok(TradeAgreementStatus::Declined), - "cancelled" => Ok(TradeAgreementStatus::Cancelled), - "completed" => Ok(TradeAgreementStatus::Completed), - "needs_review" => Ok(TradeAgreementStatus::NeedsReview), - _ => Err(AppSqliteError::DecodeEnum { field, value }), - } -} - -fn parse_trade_fulfillment_status( - field: &'static str, - value: String, -) -> Result<TradeFulfillmentStatus, AppSqliteError> { - match value.as_str() { - "confirmed" => Ok(TradeFulfillmentStatus::Confirmed), - "preparing" => Ok(TradeFulfillmentStatus::Preparing), - "ready_for_pickup" => Ok(TradeFulfillmentStatus::ReadyForPickup), - "out_for_delivery" => Ok(TradeFulfillmentStatus::OutForDelivery), - "delivered" => Ok(TradeFulfillmentStatus::Delivered), - "cancelled" => Ok(TradeFulfillmentStatus::Cancelled), - _ => Err(AppSqliteError::DecodeEnum { field, value }), - } -} - -fn parse_trade_inventory_status( - field: &'static str, - value: String, -) -> Result<TradeInventoryStatus, AppSqliteError> { - match value.as_str() { - "available" => Ok(TradeInventoryStatus::Available), - "reserved" => Ok(TradeInventoryStatus::Reserved), - "sold_out" => Ok(TradeInventoryStatus::SoldOut), - "needs_review" => Ok(TradeInventoryStatus::NeedsReview), - _ => Err(AppSqliteError::DecodeEnum { field, value }), - } -} - -fn parse_trade_payment_display_status( - field: &'static str, - value: String, -) -> Result<TradePaymentDisplayStatus, AppSqliteError> { - match value.as_str() { - "not_recorded" => Ok(TradePaymentDisplayStatus::NotRecorded), - "recorded" => Ok(TradePaymentDisplayStatus::Recorded), - "needs_review" => Ok(TradePaymentDisplayStatus::NeedsReview), - _ => Err(AppSqliteError::DecodeEnum { field, value }), - } -} - -fn parse_trade_workflow_source( - field: &'static str, - value: String, -) -> Result<TradeWorkflowSource, AppSqliteError> { - match value.as_str() { - "app" => Ok(TradeWorkflowSource::App), - "cli" => Ok(TradeWorkflowSource::Cli), - "relay" => Ok(TradeWorkflowSource::Relay), - "local_events" => Ok(TradeWorkflowSource::LocalEvents), - "unknown" => Ok(TradeWorkflowSource::Unknown), - _ => Err(AppSqliteError::DecodeEnum { field, value }), - } -} - fn shared_fulfillment_summary(lines: &[BuyerCartLineProjection]) -> Option<String> { let first = lines.first()?.fulfillment_summary.clone(); diff --git a/crates/store/src/repo/mod.rs b/crates/store/src/repo/mod.rs @@ -8,6 +8,7 @@ pub(crate) mod orders; pub(crate) mod products; pub(crate) mod reminders; pub(crate) mod today; +pub(crate) mod workflow; use radroots_app_view::TradeRevisionStatus; diff --git a/crates/store/src/repo/orders.rs b/crates/store/src/repo/orders.rs @@ -7,14 +7,14 @@ use radroots_app_view::{ PackDayOutputCustomerOrder, PackDayOutputOrderState, PackDayOutputPackListEntry, PackDayOutputProductTotal, PackDayOutputQuantity, PackDayOutputSource, PackDayOutputWindow, PackDayPackListRow, PackDayProductTotalRow, PackDayProjection, PackDayRosterRow, - PackDayScreenQueryState, ProductId, TradePaymentDisplayStatus, TradeRevisionStatus, - TradeWorkflowProjection, + PackDayScreenQueryState, ProductId, TradeWorkflowProjection, }; use rusqlite::{Connection, OptionalExtension, params}; use super::{ order_detail::{order_detail_economics, order_detail_item_row}, parse_trade_revision_status, + workflow::{StoredTradeWorkflowSnapshot, trade_workflow_projection_from_storage}, }; use crate::AppSqliteError; @@ -79,6 +79,12 @@ impl<'a> AppOrdersRepository<'a> { o.status, o.fulfillment_window_id, o.workflow_revision, + o.workflow_agreement, + o.workflow_fulfillment, + o.workflow_inventory, + o.workflow_payment, + o.workflow_provenance_source, + o.workflow_provenance_last_event_id, fw.label, pl.label from orders o @@ -96,8 +102,14 @@ impl<'a> AppOrdersRepository<'a> { row.get::<_, String>(4)?, row.get::<_, Option<String>>(5)?, row.get::<_, String>(6)?, - row.get::<_, Option<String>>(7)?, + row.get::<_, String>(7)?, row.get::<_, Option<String>>(8)?, + row.get::<_, String>(9)?, + row.get::<_, String>(10)?, + row.get::<_, String>(11)?, + row.get::<_, Option<String>>(12)?, + row.get::<_, Option<String>>(13)?, + row.get::<_, Option<String>>(14)?, )) }, ) @@ -117,6 +129,12 @@ impl<'a> AppOrdersRepository<'a> { status, fulfillment_window_id, workflow_revision, + workflow_agreement, + workflow_fulfillment, + workflow_inventory, + workflow_payment, + workflow_provenance_source, + workflow_provenance_last_event_id, fulfillment_window_label, pickup_location_label, )| { @@ -127,10 +145,19 @@ impl<'a> AppOrdersRepository<'a> { parse_trade_revision_status("orders.workflow_revision", workflow_revision)?; let items = self.load_order_detail_items(order_id.to_string())?; let economics = order_detail_economics(&items)?; - let payment = TradePaymentDisplayStatus::NotRecorded; - let workflow = TradeWorkflowProjection::from_order_status(order_id, status) - .with_revision(revision) - .with_economics_and_payment(economics.clone(), payment); + let workflow = + trade_workflow_projection_from_storage(StoredTradeWorkflowSnapshot { + order_id, + revision, + economics: economics.clone(), + agreement: workflow_agreement, + fulfillment: workflow_fulfillment, + inventory: workflow_inventory, + payment: workflow_payment, + provenance_source: workflow_provenance_source, + provenance_last_event_id: workflow_provenance_last_event_id, + })?; + let payment = workflow.payment; Ok(OrderDetailProjection { order_id, farm_id, @@ -296,6 +323,12 @@ impl<'a> AppOrdersRepository<'a> { o.customer_display_name, o.status, o.workflow_revision, + o.workflow_agreement, + o.workflow_fulfillment, + o.workflow_inventory, + o.workflow_payment, + o.workflow_provenance_source, + o.workflow_provenance_last_event_id, fw.label, pl.label from orders o @@ -324,8 +357,14 @@ impl<'a> AppOrdersRepository<'a> { row.get::<_, String>(4)?, row.get::<_, String>(5)?, row.get::<_, String>(6)?, - row.get::<_, Option<String>>(7)?, + row.get::<_, String>(7)?, row.get::<_, Option<String>>(8)?, + row.get::<_, String>(9)?, + row.get::<_, String>(10)?, + row.get::<_, String>(11)?, + row.get::<_, Option<String>>(12)?, + row.get::<_, Option<String>>(13)?, + row.get::<_, Option<String>>(14)?, )) }, ) @@ -344,16 +383,40 @@ impl<'a> AppOrdersRepository<'a> { customer_display_name, status, workflow_revision, + workflow_agreement, + workflow_fulfillment, + workflow_inventory, + workflow_payment, + workflow_provenance_source, + workflow_provenance_last_event_id, fulfillment_window_label, pickup_location_label, ) = row.map_err(|source| AppSqliteError::Query { operation: "read orders list", source, })?; + let order_id: OrderId = parse_typed_id("orders.id", order_id)?; + let farm_id: FarmId = parse_typed_id("orders.farm_id", farm_id)?; + let status = parse_order_status("orders.status", status)?; + let revision = + parse_trade_revision_status("orders.workflow_revision", workflow_revision)?; + let items = self.load_order_detail_items(order_id.to_string())?; + let economics = order_detail_economics(&items)?; + let workflow = trade_workflow_projection_from_storage(StoredTradeWorkflowSnapshot { + order_id, + revision, + economics, + agreement: workflow_agreement, + fulfillment: workflow_fulfillment, + inventory: workflow_inventory, + payment: workflow_payment, + provenance_source: workflow_provenance_source, + provenance_last_event_id: workflow_provenance_last_event_id, + })?; records.push(OrderRecord { - order_id: parse_typed_id("orders.id", order_id)?, - farm_id: parse_typed_id("orders.farm_id", farm_id)?, + order_id, + farm_id, fulfillment_window_id: parse_optional_typed_id( "orders.fulfillment_window_id", fulfillment_window_id, @@ -362,11 +425,8 @@ impl<'a> AppOrdersRepository<'a> { customer_display_name, fulfillment_window_label: empty_string_to_none(fulfillment_window_label), pickup_location_label: empty_string_to_none(pickup_location_label), - status: parse_order_status("orders.status", status)?, - revision: parse_trade_revision_status( - "orders.workflow_revision", - workflow_revision, - )?, + status, + workflow, }); } @@ -1134,7 +1194,7 @@ struct OrderRecord { fulfillment_window_label: Option<String>, pickup_location_label: Option<String>, status: OrderStatus, - revision: TradeRevisionStatus, + workflow: TradeWorkflowProjection, } impl OrderRecord { @@ -1150,9 +1210,6 @@ impl OrderRecord { } fn into_list_row(self) -> OrdersListRow { - let workflow = TradeWorkflowProjection::from_order_status(self.order_id, self.status) - .with_revision(self.revision); - OrdersListRow { order_id: self.order_id, farm_id: self.farm_id, @@ -1162,7 +1219,7 @@ impl OrderRecord { fulfillment_window_label: self.fulfillment_window_label, pickup_location_label: self.pickup_location_label, status: self.status, - workflow, + workflow: self.workflow, primary_action: primary_action_for_status(self.status), } } @@ -1280,7 +1337,8 @@ mod tests { use radroots_app_view::{ FarmId, FulfillmentWindowId, OrderId, OrderPrimaryAction, OrderStatus, OrdersFilter, OrdersScreenQueryState, PackDayOutputOrderState, PackDayProductTotalRow, - PackDayScreenQueryState, PickupLocationId, TradePaymentDisplayStatus, TradeRevisionStatus, + PackDayScreenQueryState, PickupLocationId, TradeAgreementStatus, TradeFulfillmentStatus, + TradeInventoryStatus, TradePaymentDisplayStatus, TradeRevisionStatus, TradeWorkflowSource, }; use rusqlite::{Connection, params}; @@ -1569,6 +1627,168 @@ mod tests { } #[test] + fn seller_order_projections_read_workflow_display_snapshot() { + let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); + let connection = store.connection(); + let farm_id = FarmId::new(); + let order_id = OrderId::new(); + + insert_farm( + connection, + farm_id, + "Willow farm", + "ready", + "2026-04-17T08:00:00Z", + ); + insert_order( + connection, + order_id, + farm_id, + None, + "R-100", + "Casey", + "scheduled", + "2026-04-17T10:00:00Z", + ); + insert_order_line( + connection, + "line-1", + order_id, + "Salad mix", + 2, + "bags", + "2 bags", + 0, + ); + set_order_workflow_revision( + connection, + order_id, + TradeRevisionStatus::Updated.storage_key(), + ); + set_order_workflow_display_projection( + connection, + order_id, + "confirmed", + Some("ready_for_pickup"), + "reserved", + "recorded", + "local_events", + Some("seller-workflow-event"), + ); + + let list = store + .load_orders_list( + farm_id, + &OrdersScreenQueryState { + filter: OrdersFilter::All, + fulfillment_window_id: None, + }, + ) + .expect("seller list should load"); + let detail = store + .load_order_detail(farm_id, order_id) + .expect("seller detail should load") + .expect("seller detail should exist"); + let workflow = &list.rows[0].workflow; + + assert_eq!(workflow.agreement, TradeAgreementStatus::Confirmed); + assert_eq!(workflow.revision, TradeRevisionStatus::Updated); + assert_eq!( + workflow.fulfillment, + Some(TradeFulfillmentStatus::ReadyForPickup) + ); + assert_eq!(workflow.inventory, TradeInventoryStatus::Reserved); + assert_eq!(workflow.payment, TradePaymentDisplayStatus::Recorded); + assert_eq!( + workflow.provenance.primary_source, + TradeWorkflowSource::LocalEvents + ); + assert_eq!( + workflow.provenance.last_event_id.as_deref(), + Some("seller-workflow-event") + ); + assert_eq!(workflow.economics.total_minor_units, Some(1300)); + assert_eq!(workflow.economics.currency_code.as_deref(), Some("USD")); + assert_eq!(detail.payment, TradePaymentDisplayStatus::Recorded); + assert_eq!(detail.workflow, *workflow); + } + + #[test] + fn seller_order_projections_fail_closed_for_invalid_workflow_snapshot_keys() { + let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); + let connection = store.connection(); + let farm_id = FarmId::new(); + let order_id = OrderId::new(); + + insert_farm( + connection, + farm_id, + "Willow farm", + "ready", + "2026-04-17T08:00:00Z", + ); + insert_order( + connection, + order_id, + farm_id, + None, + "R-100", + "Casey", + "scheduled", + "2026-04-17T10:00:00Z", + ); + set_order_workflow_display_projection( + connection, + order_id, + "confirmed", + Some("ready_for_pickup"), + "reserved", + "recorded", + "local_events", + Some("seller-workflow-event"), + ); + + for (column, expected_field) in [ + ("workflow_agreement", "orders.workflow_agreement"), + ("workflow_fulfillment", "orders.workflow_fulfillment"), + ("workflow_inventory", "orders.workflow_inventory"), + ("workflow_payment", "orders.workflow_payment"), + ( + "workflow_provenance_source", + "orders.workflow_provenance_source", + ), + ] { + set_order_workflow_display_projection( + connection, + order_id, + "confirmed", + Some("ready_for_pickup"), + "reserved", + "recorded", + "local_events", + Some("seller-workflow-event"), + ); + corrupt_order_workflow_display_projection(connection, order_id, column, "future_state"); + + let list_error = store + .load_orders_list( + farm_id, + &OrdersScreenQueryState { + filter: OrdersFilter::All, + fulfillment_window_id: None, + }, + ) + .expect_err("invalid workflow snapshot should fail seller list projection"); + let detail_error = store + .load_order_detail(farm_id, order_id) + .expect_err("invalid workflow snapshot should fail seller detail projection"); + + assert_decode_enum(list_error, expected_field, "future_state"); + assert_decode_enum(detail_error, expected_field, "future_state"); + } + } + + #[test] fn pack_day_defaults_to_next_window_and_projects_totals_pack_list_and_roster() { let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); let connection = store.connection(); @@ -2126,6 +2346,66 @@ mod tests { .expect("check constraints should re-enable"); } + fn set_order_workflow_display_projection( + connection: &Connection, + order_id: OrderId, + agreement: &str, + fulfillment: Option<&str>, + inventory: &str, + payment: &str, + provenance_source: &str, + provenance_last_event_id: Option<&str>, + ) { + connection + .execute( + "update orders + set workflow_agreement = ?1, + workflow_fulfillment = ?2, + workflow_inventory = ?3, + workflow_payment = ?4, + workflow_provenance_source = ?5, + workflow_provenance_last_event_id = ?6 + where id = ?7", + params![ + agreement, + fulfillment, + inventory, + payment, + provenance_source, + provenance_last_event_id, + order_id.to_string(), + ], + ) + .expect("order workflow display projection update should succeed"); + } + + fn corrupt_order_workflow_display_projection( + connection: &Connection, + order_id: OrderId, + column: &str, + value: &str, + ) { + connection + .execute_batch("pragma ignore_check_constraints = on") + .expect("check constraints should disable"); + let statement = match column { + "workflow_agreement" => "update orders set workflow_agreement = ?1 where id = ?2", + "workflow_fulfillment" => "update orders set workflow_fulfillment = ?1 where id = ?2", + "workflow_inventory" => "update orders set workflow_inventory = ?1 where id = ?2", + "workflow_payment" => "update orders set workflow_payment = ?1 where id = ?2", + "workflow_provenance_source" => { + "update orders set workflow_provenance_source = ?1 where id = ?2" + } + _ => panic!("unsupported workflow display projection column {column}"), + }; + connection + .execute(statement, params![value, order_id.to_string()]) + .expect("order workflow display projection corruption should succeed"); + connection + .execute_batch("pragma ignore_check_constraints = off") + .expect("check constraints should re-enable"); + } + fn assert_decode_enum(error: AppSqliteError, expected_field: &str, expected_value: &str) { match error { AppSqliteError::DecodeEnum { field, value } => { diff --git a/crates/store/src/repo/workflow.rs b/crates/store/src/repo/workflow.rs @@ -0,0 +1,111 @@ +use radroots_app_view::{ + OrderId, TradeAgreementStatus, TradeEconomicsProjection, TradeFulfillmentStatus, + TradeInventoryStatus, TradePaymentDisplayStatus, TradeProvenanceProjection, + TradeRevisionStatus, TradeWorkflowProjection, TradeWorkflowSource, +}; + +use crate::AppSqliteError; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(super) struct StoredTradeWorkflowSnapshot { + pub order_id: OrderId, + pub revision: TradeRevisionStatus, + pub economics: TradeEconomicsProjection, + pub agreement: String, + pub fulfillment: Option<String>, + pub inventory: String, + pub payment: String, + pub provenance_source: String, + pub provenance_last_event_id: Option<String>, +} + +pub(super) fn trade_workflow_projection_from_storage( + snapshot: StoredTradeWorkflowSnapshot, +) -> Result<TradeWorkflowProjection, AppSqliteError> { + Ok(TradeWorkflowProjection { + order_id: snapshot.order_id, + agreement: parse_trade_agreement_status("orders.workflow_agreement", snapshot.agreement)?, + revision: snapshot.revision, + fulfillment: snapshot + .fulfillment + .map(|value| parse_trade_fulfillment_status("orders.workflow_fulfillment", value)) + .transpose()?, + economics: snapshot.economics, + inventory: parse_trade_inventory_status("orders.workflow_inventory", snapshot.inventory)?, + payment: parse_trade_payment_display_status("orders.workflow_payment", snapshot.payment)?, + provenance: TradeProvenanceProjection::from_primary_source(parse_trade_workflow_source( + "orders.workflow_provenance_source", + snapshot.provenance_source, + )?) + .with_last_event_id(snapshot.provenance_last_event_id), + }) +} + +fn parse_trade_agreement_status( + field: &'static str, + value: String, +) -> Result<TradeAgreementStatus, AppSqliteError> { + match value.as_str() { + "ordered" => Ok(TradeAgreementStatus::Ordered), + "confirmed" => Ok(TradeAgreementStatus::Confirmed), + "declined" => Ok(TradeAgreementStatus::Declined), + "cancelled" => Ok(TradeAgreementStatus::Cancelled), + "completed" => Ok(TradeAgreementStatus::Completed), + "needs_review" => Ok(TradeAgreementStatus::NeedsReview), + _ => Err(AppSqliteError::DecodeEnum { field, value }), + } +} + +fn parse_trade_fulfillment_status( + field: &'static str, + value: String, +) -> Result<TradeFulfillmentStatus, AppSqliteError> { + match value.as_str() { + "confirmed" => Ok(TradeFulfillmentStatus::Confirmed), + "preparing" => Ok(TradeFulfillmentStatus::Preparing), + "ready_for_pickup" => Ok(TradeFulfillmentStatus::ReadyForPickup), + "out_for_delivery" => Ok(TradeFulfillmentStatus::OutForDelivery), + "delivered" => Ok(TradeFulfillmentStatus::Delivered), + "cancelled" => Ok(TradeFulfillmentStatus::Cancelled), + _ => Err(AppSqliteError::DecodeEnum { field, value }), + } +} + +fn parse_trade_inventory_status( + field: &'static str, + value: String, +) -> Result<TradeInventoryStatus, AppSqliteError> { + match value.as_str() { + "available" => Ok(TradeInventoryStatus::Available), + "reserved" => Ok(TradeInventoryStatus::Reserved), + "sold_out" => Ok(TradeInventoryStatus::SoldOut), + "needs_review" => Ok(TradeInventoryStatus::NeedsReview), + _ => Err(AppSqliteError::DecodeEnum { field, value }), + } +} + +fn parse_trade_payment_display_status( + field: &'static str, + value: String, +) -> Result<TradePaymentDisplayStatus, AppSqliteError> { + match value.as_str() { + "not_recorded" => Ok(TradePaymentDisplayStatus::NotRecorded), + "recorded" => Ok(TradePaymentDisplayStatus::Recorded), + "needs_review" => Ok(TradePaymentDisplayStatus::NeedsReview), + _ => Err(AppSqliteError::DecodeEnum { field, value }), + } +} + +fn parse_trade_workflow_source( + field: &'static str, + value: String, +) -> Result<TradeWorkflowSource, AppSqliteError> { + match value.as_str() { + "app" => Ok(TradeWorkflowSource::App), + "cli" => Ok(TradeWorkflowSource::Cli), + "relay" => Ok(TradeWorkflowSource::Relay), + "local_events" => Ok(TradeWorkflowSource::LocalEvents), + "unknown" => Ok(TradeWorkflowSource::Unknown), + _ => Err(AppSqliteError::DecodeEnum { field, value }), + } +}