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:
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",