app

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

commit 78329f6de531bbf75ce3120863fa6a3e221e6141
parent e8759939caa98be78c342ffd6bde7008efe6235a
Author: triesap <tyson@radroots.org>
Date:   Sun, 24 May 2026 20:20:48 +0000

app: show saved orders after coordination failure

- refresh buyer order projections before shared local work append
- distinguish saved-local coordination failures from full order failures
- keep failed coordination orders visible in Orders
- update checkout copy and focused buyer order tests

Diffstat:
Mcrates/launchers/desktop/src/runtime.rs | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mcrates/launchers/desktop/src/source_guards.rs | 1+
Mcrates/launchers/desktop/src/window.rs | 24++++++++++++++++++++++--
Mcrates/shared/i18n/src/keys.rs | 1+
Mcrates/shared/i18n/src/lib.rs | 4++++
Mi18n/locales/en/messages.json | 1+
6 files changed, 104 insertions(+), 39 deletions(-)

diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs @@ -1400,42 +1400,54 @@ impl DesktopAppRuntimeState { } fn place_personal_order(&mut self) -> Result<bool, AppSqliteError> { - let Some(sqlite_store) = self.sqlite_store.as_ref() else { - return Ok(false); - }; let buyer_context = self.state_store.identity_projection().buyer_context(); - let order_id = sqlite_store.place_buyer_order(&buyer_context)?; - let refreshed_cart = sqlite_store.load_buyer_cart(&buyer_context)?; - let refreshed_checkout = sqlite_store.load_buyer_checkout(&buyer_context)?; - let refreshed_orders = sqlite_store.load_buyer_orders(&buyer_context)?; - if !refreshed_orders - .rows - .iter() - .any(|row| row.order_id == order_id) - { - return Err(AppSqliteError::InvalidProjection { - reason: "buyer order write did not surface in buyer order history", - }); - } - let Some(order_detail) = sqlite_store.load_buyer_order_detail(&buyer_context, order_id)? - else { - return Err(AppSqliteError::InvalidProjection { - reason: "buyer order write did not surface in buyer order detail", - }); - }; - let Some(order_export) = - sqlite_store.load_buyer_order_local_event_export(&buyer_context, order_id)? - else { - return Err(AppSqliteError::InvalidProjection { - reason: "buyer order write did not surface in buyer order local event export", - }); + let ( + order_id, + refreshed_cart, + refreshed_checkout, + refreshed_orders, + order_detail, + order_export, + ) = { + let Some(sqlite_store) = self.sqlite_store.as_ref() else { + return Ok(false); + }; + let order_id = sqlite_store.place_buyer_order(&buyer_context)?; + let refreshed_cart = sqlite_store.load_buyer_cart(&buyer_context)?; + let refreshed_checkout = sqlite_store.load_buyer_checkout(&buyer_context)?; + let refreshed_orders = sqlite_store.load_buyer_orders(&buyer_context)?; + if !refreshed_orders + .rows + .iter() + .any(|row| row.order_id == order_id) + { + return Err(AppSqliteError::InvalidProjection { + reason: "buyer order write did not surface in buyer order history", + }); + } + let Some(order_detail) = + sqlite_store.load_buyer_order_detail(&buyer_context, order_id)? + else { + return Err(AppSqliteError::InvalidProjection { + reason: "buyer order write did not surface in buyer order detail", + }); + }; + let Some(order_export) = + sqlite_store.load_buyer_order_local_event_export(&buyer_context, order_id)? + else { + return Err(AppSqliteError::InvalidProjection { + reason: "buyer order write did not surface in buyer order local event export", + }); + }; + ( + order_id, + refreshed_cart, + refreshed_checkout, + refreshed_orders, + order_detail, + order_export, + ) }; - self.append_app_buyer_order_request_local_work_record( - sqlite_store, - &buyer_context, - &order_export, - )?; - let personal_changed = self.mutate_personal_projection(|projection| { let mut changed = false; if projection.cart.cart != refreshed_cart { @@ -1458,6 +1470,19 @@ impl DesktopAppRuntimeState { changed }); let section_changed = self.select_personal_section(PersonalSection::Orders)?; + { + let sqlite_store = + self.sqlite_store + .as_ref() + .ok_or_else(|| AppSqliteError::InvalidProjection { + reason: "sqlite store became unavailable during buyer order placement", + })?; + self.append_app_buyer_order_request_local_work_record( + sqlite_store, + &buyer_context, + &order_export, + )?; + } let pending_changed = if matches!(buyer_context, BuyerContext::Account(_)) { self.enqueue_selected_account_sync_operations(vec![pending_sync_upsert( SyncAggregateRef::Order(order_id), @@ -8717,13 +8742,25 @@ mod tests { assert!(matches!(error, AppSqliteError::LocalEventsSql { .. })); let summary = runtime.summary(); - assert_ne!( + assert_eq!( summary.shell_projection.selected_section, ShellSection::Personal(PersonalSection::Orders) ); - assert!(summary.personal_projection.orders.list.rows.is_empty()); - assert!(summary.personal_projection.orders.detail.is_none()); + assert!(summary.personal_projection.cart.cart.lines.is_empty()); + assert!(!summary.personal_projection.cart.checkout.can_place_order); + assert_eq!(summary.personal_projection.orders.list.rows.len(), 1); let order_id = { + let visible_order_id = summary.personal_projection.orders.list.rows[0].order_id; + assert_eq!( + summary + .personal_projection + .orders + .detail + .as_ref() + .expect("buyer order detail should remain visible after coordination failure") + .order_id, + visible_order_id + ); let state = runtime.lock_state_mut(); let buyer_context = state.state_store.identity_projection().buyer_context(); let sqlite_store = state.sqlite_store.as_ref().expect("sqlite store"); @@ -8732,6 +8769,7 @@ mod tests { .expect("buyer order should persist after coordination failure"); assert_eq!(buyer_orders.rows.len(), 1); let order_id = buyer_orders.rows[0].order_id; + assert_eq!(order_id, visible_order_id); let coordination = sqlite_store .load_buyer_order_coordination_record(&buyer_context, order_id) .expect("buyer order coordination should load") diff --git a/crates/launchers/desktop/src/source_guards.rs b/crates/launchers/desktop/src/source_guards.rs @@ -381,6 +381,7 @@ const REQUIRED_WINDOW_COPY_KEYS: &[&str] = &[ "AppTextKey::PersonalMarketplaceRefreshFailedNotice", "AppTextKey::PersonalDetailOpenFailedNotice", "AppTextKey::PersonalOrderPlaceFailedNotice", + "AppTextKey::PersonalOrderCoordinationFailedNotice", "AppTextKey::PersonalCartPlaceholderBody", "AppTextKey::PersonalOrdersSurfaceBody", "AppTextKey::PersonalOrdersEmptyTitle", diff --git a/crates/launchers/desktop/src/window.rs b/crates/launchers/desktop/src/window.rs @@ -38,7 +38,7 @@ use radroots_app_remote_signer::{ radroots_app_remote_signer_poll_pending_session_with_progress, radroots_app_remote_signer_preview, radroots_app_remote_signer_requested_permissions, }; -use radroots_app_sqlite::derive_farm_rules_readiness; +use radroots_app_sqlite::{AppSqliteError, derive_farm_rules_readiness}; use radroots_app_state::{ FarmSetupFlowStage, FarmWorkspaceStatus, HomeRoute, PackDayBatchPrintRequest, PackDayExportProjection, PackDayHostHandoffRequest, PackDayPrintRequest, @@ -304,6 +304,7 @@ enum BuyerWorkspaceNotice { MarketplaceRefreshFailed, DetailOpenFailed, OrderPlaceFailed, + OrderCoordinationFailed, } impl BuyerWorkspaceNotice { @@ -312,6 +313,7 @@ impl BuyerWorkspaceNotice { Self::MarketplaceRefreshFailed => AppTextKey::PersonalMarketplaceRefreshFailedNotice, Self::DetailOpenFailed => AppTextKey::PersonalDetailOpenFailedNotice, Self::OrderPlaceFailed => AppTextKey::PersonalOrderPlaceFailedNotice, + Self::OrderCoordinationFailed => AppTextKey::PersonalOrderCoordinationFailedNotice, } } @@ -1558,13 +1560,17 @@ impl HomeView { } Ok(false) => false, Err(runtime_error) => { + let notice = buyer_order_place_failure_notice(&runtime_error); + if notice == BuyerWorkspaceNotice::OrderCoordinationFailed { + self.buyer_checkout_form = None; + } error!( target: "buyer", event = "buyer.checkout_place_failed", error = %runtime_error, "failed to place buyer order" ); - self.set_buyer_workspace_notice(BuyerWorkspaceNotice::OrderPlaceFailed) + self.set_buyer_workspace_notice(notice) } } } @@ -12771,6 +12777,15 @@ fn home_empty_state_card(title_key: AppTextKey, body_key: AppTextKey) -> impl In ) } +fn buyer_order_place_failure_notice(error: &AppSqliteError) -> BuyerWorkspaceNotice { + match error { + AppSqliteError::LocalEventsSql { .. } | AppSqliteError::LocalEvents { .. } => { + BuyerWorkspaceNotice::OrderCoordinationFailed + } + _ => BuyerWorkspaceNotice::OrderPlaceFailed, + } +} + fn buyer_workspace_notice_card(notice: String) -> impl IntoElement { app_surface_card(home_body_text(notice)) } @@ -13061,6 +13076,11 @@ mod tests { view.buyer_workspace_notice.as_deref(), Some(app_text(AppTextKey::PersonalOrderPlaceFailedNotice).as_str()) ); + assert!(view.set_buyer_workspace_notice(BuyerWorkspaceNotice::OrderCoordinationFailed)); + assert_eq!( + view.buyer_workspace_notice.as_deref(), + Some(app_text(AppTextKey::PersonalOrderCoordinationFailedNotice).as_str()) + ); assert!(view.clear_buyer_workspace_notice()); assert_eq!(view.buyer_workspace_notice, None); diff --git a/crates/shared/i18n/src/keys.rs b/crates/shared/i18n/src/keys.rs @@ -125,6 +125,7 @@ define_app_text_keys! { PersonalMarketplaceRefreshFailedNotice => "personal.marketplace.refresh_failed.notice", PersonalDetailOpenFailedNotice => "personal.detail.open_failed.notice", PersonalOrderPlaceFailedNotice => "personal.order_place_failed.notice", + PersonalOrderCoordinationFailedNotice => "personal.order_coordination_failed.notice", PersonalCartPlaceholderBody => "personal.cart.placeholder.body", PersonalOrdersSurfaceBody => "personal.orders.surface.body", PersonalOrdersEmptyTitle => "personal.orders.empty.title", diff --git a/crates/shared/i18n/src/lib.rs b/crates/shared/i18n/src/lib.rs @@ -366,6 +366,10 @@ mod tests { app_text(AppTextKey::PersonalOrderPlaceFailedNotice), "Couldn't place that order. Nothing was sent; check the order and try again." ); + assert_eq!( + app_text(AppTextKey::PersonalOrderCoordinationFailedNotice), + "Order saved locally. It still needs to be shared with your order tools; open Orders and try again." + ); } #[test] diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json @@ -105,6 +105,7 @@ "personal.marketplace.refresh_failed.notice": "Couldn't refresh marketplace listings. Your saved local state is still here; try again in a moment.", "personal.detail.open_failed.notice": "Couldn't open that listing. Refresh the marketplace and try again.", "personal.order_place_failed.notice": "Couldn't place that order. Nothing was sent; check the order and try again.", + "personal.order_coordination_failed.notice": "Order saved locally. It still needs to be shared with your order tools; open Orders and try again.", "personal.cart.placeholder.body": "Add items from one farm to start an order.", "personal.orders.surface.body": "Review orders placed on this device.", "personal.orders.empty.title": "No orders yet",